RT-Thread Version
master @ bd53bba The vulnerable code was introduced by commit 8b887e7 ("[net][at] Add AT commands component", 2018-07-25) and is present in at least v3.1.0 through v5.2.2, as well as current master.
Hardware Type/Architectures
Any RT-Thread BSP / architecture that enables the AT socket resolver path
Develop Toolchain
GCC
Describe the bug
I verified the bug by source audit on current master and by checking historical tags/commits. The impact is typically strongest on GCC ARM / RISC-V / AArch64 builds without strong stack hardening, but the bug is not toolchain-specific.
Describe the bug
The AT socket resolver in components/net/at/at_socket/at_socket.c treats hostnames with no alphabetic characters as "numeric" and copies them into a fixed-size stack buffer using an unbounded length:
char ipstr[16] = { 0 };
...
strncpy(ipstr, name, strlen(name));
This happens in _gethostbyname_by_device() before inet_aton() validates the string. As a result:
- a numeric-looking hostname of 17 bytes or more causes a stack write overflow in kernel context;
- a 16-byte numeric-looking hostname exactly fills
ipstr[16] without a terminator, which can still trigger a subsequent out-of-bounds read in inet_aton().
The bug is reachable through public resolver APIs, not only AT-private helpers:
getaddrinfo() -> sal_getaddrinfo() -> at_getaddrinfo() -> _gethostbyname_by_device()
gethostbyname() / gethostbyname_r() -> SAL netdb -> at_gethostbyname*() -> _gethostbyname_by_device()
On MMU/LWP builds, this is a real user-to-kernel trust-boundary crossing because unprivileged userspace can reach the vulnerable kernel resolver path through standard network APIs.
Important scope clarification:
rt_memcpy(ai->ai_canonname, nodename, namelen) in at_getaddrinfo() is on the same affected wrapper path, but it is not the root overflow primitive here. The primary memory corruption sink is the numeric-host branch in _gethostbyname_by_device().
Code locations
Primary vulnerable sink:
components/net/at/at_socket/at_socket.c
_gethostbyname_by_device()
Affected wrappers:
at_gethostbyname()
at_gethostbyname_r()
at_getaddrinfo()
Relevant standard-entry bridges:
components/net/sal/socket/net_netdb.c
components/net/sal/src/sal_socket.c
components/lwp/lwp_syscall.c
- Steps to reproduce the behavior
PoC 1: strongest path, unprivileged userspace -> kernel stack corruption (MMU/LWP builds)
Build a target with:
RT_USING_AT=y
AT_USING_CLIENT=y
AT_USING_SOCKET=y
RT_USING_SAL=y
- LWP/MMU enabled
- an AT-backed netdev that becomes the default or first-up resolver backend
Then run an unprivileged userspace program that calls standard POSIX resolution APIs with a numeric-looking hostname longer than 16 bytes and containing no alphabetic characters:
#include <string.h>
#include <netdb.h>
int main(void)
{
struct addrinfo *ai = 0;
char host[128];
memset(host, '1', sizeof(host) - 1);
host[sizeof(host) - 1] = '\0';
/* No alphabetic chars, so _gethostbyname_by_device() takes the local numeric branch */
getaddrinfo(host, "80", NULL, &ai);
return 0;
}
Expected kernel-side path:
sys_getaddrinfo()
sal_getaddrinfo()
at_getaddrinfo()
_gethostbyname_by_device()
strncpy(ipstr, name, strlen(name))
Expected result:
- kernel crash, stack smash, return-address corruption, or other memory-corruption symptoms depending on architecture and hardening.
PoC 2: direct kernel task / application task trigger
Even without MMU/LWP, the bug is directly reachable from local RT-Thread tasks using public resolver APIs:
#include <string.h>
#include <netdb.h>
static void poc(void)
{
struct addrinfo *ai = RT_NULL;
const char *host = "11111111111111111"; /* 17 bytes: first actual stack overwrite */
getaddrinfo(host, "80", RT_NULL, &ai);
}
or AT-specific direct call:
struct hostent *h = at_gethostbyname("11111111111111111");
Expected result:
- local kernel stack corruption in
_gethostbyname_by_device() before address parsing completes.
PoC 3: exact-fit 16-byte edge case
This is useful to demonstrate boundary semantics precisely:
at_gethostbyname("1111111111111111"); /* 16 bytes */
This may not perform a stack write beyond ipstr[16], but it removes the terminator and can still drive a subsequent out-of-bounds read in inet_aton().
- Expected behavior
The resolver should reject overlong numeric-looking host strings before copying them into ipstr[16], and all wrapper APIs should validate hostname length before calling _gethostbyname_by_device().
At a minimum:
- inputs with
strlen(name) >= sizeof(ipstr) must fail safely;
- no resolver path should rely on
strncpy(dst, src, strlen(src)) for fixed-size stack buffers.
- Add screenshot / media if you have them
Not available yet. This report is based on code audit and version-history verification.
Other additional context
impact
This is not dependent on a malicious AT module callback violating its contract. The bug is triggered on the local numeric-host branch before at_domain_resolve() is involved.
This also is not just an "API misuse" issue in private code:
- standard exported resolver APIs (
getaddrinfo(), gethostbyname*()) can route into the vulnerable AT backend;
- on MMU/LWP builds, that route crosses a real unprivileged-userspace to kernel boundary.
So the bug is better described as:
- local user-controlled kernel stack overflow in the AT resolver bridge;
- with realistic reachability through standard network resolution APIs when an AT-backed netdev is active.
Precise overflow boundary
- 16 bytes: no terminator left in
ipstr[16], may cause later OOB read
- 17+ bytes: actual stack write overflow begins here
Fix suggestion
The minimum safe fix is:
- fully bound and terminate the numeric-host local copy in
_gethostbyname_by_device();
- reject overlong hostnames before entering
_gethostbyname_by_device() from at_getaddrinfo();
- optionally clean up
at_gethostbyname() to avoid repeated strlen() and use explicit length-based copy after validation.
Suggested replacement for _gethostbyname_by_device()
static int _gethostbyname_by_device(const char *name, ip_addr_t *addr)
{
static rt_mutex_t at_dlock = RT_NULL;
struct at_device *device = RT_NULL;
char ipstr[16] = { 0 };
size_t idx = 0;
size_t name_len = 0;
if (name == RT_NULL || addr == RT_NULL)
{
return -1;
}
device = at_device_get_first_initialized();
if (device == RT_NULL)
{
return -1;
}
if (!netdev_is_link_up(device->netdev))
{
return -1;
}
name_len = strlen(name);
for (idx = 0; idx < name_len && !isalpha((unsigned char)name[idx]); idx++)
;
if (idx < name_len)
{
if (device->class == RT_NULL ||
device->class->socket_ops == RT_NULL ||
device->class->socket_ops->at_domain_resolve == RT_NULL)
{
return -1;
}
if (at_dlock == RT_NULL)
{
at_dlock = rt_mutex_create("at_dlock", RT_IPC_FLAG_PRIO);
if (at_dlock == RT_NULL)
{
return -1;
}
}
rt_mutex_take(at_dlock, RT_WAITING_FOREVER);
if (device->class->socket_ops->at_domain_resolve(name, ipstr) < 0)
{
rt_mutex_release(at_dlock);
return -2;
}
rt_mutex_release(at_dlock);
ipstr[sizeof(ipstr) - 1] = '\0';
}
else
{
if (name_len >= sizeof(ipstr))
{
return -1;
}
rt_memcpy(ipstr, name, name_len);
ipstr[name_len] = '\0';
}
#if NETDEV_IPV4 && NETDEV_IPV6
addr->type = IPADDR_TYPE_V4;
if (inet_aton(ipstr, addr) == 0)
{
return -1;
}
#elif NETDEV_IPV4
if (inet_aton(ipstr, addr) == 0)
{
return -1;
}
#elif NETDEV_IPV6
#error "not support IPV6."
#endif /* NETDEV_IPV4 && NETDEV_IPV6 */
return 0;
}
Suggested replacement for at_getaddrinfo()
int at_getaddrinfo(const char *nodename, const char *servname,
const struct addrinfo *hints, struct addrinfo **res)
{
int port_nr = 0;
ip_addr_t addr = {0};
struct addrinfo *ai;
struct sockaddr_storage *sa;
size_t total_size = 0;
size_t namelen = 0;
int ai_family = 0;
if (res == RT_NULL)
{
return EAI_FAIL;
}
*res = RT_NULL;
if ((nodename == RT_NULL) && (servname == RT_NULL))
{
return EAI_NONAME;
}
if (nodename != RT_NULL)
{
namelen = strlen(nodename);
if (namelen == 0 || namelen > DNS_MAX_NAME_LENGTH)
{
return EAI_FAIL;
}
}
if (hints != RT_NULL)
{
ai_family = hints->ai_family;
if (hints->ai_family != AF_AT && hints->ai_family != AF_INET && hints->ai_family != AF_UNSPEC)
{
return EAI_FAMILY;
}
}
else
{
ai_family = AF_UNSPEC;
}
if (servname != RT_NULL)
{
port_nr = atoi(servname);
if ((port_nr <= 0) || (port_nr > 0xffff))
{
return EAI_SERVICE;
}
}
if (nodename != RT_NULL)
{
if ((hints != RT_NULL) && (hints->ai_flags & AI_NUMERICHOST))
{
if (ai_family == AF_AT || ai_family == AF_INET)
{
return EAI_NONAME;
}
if (!inet_aton(nodename, &addr))
{
return EAI_NONAME;
}
}
else
{
int domain_err = _gethostbyname_by_device(nodename, &addr);
if (domain_err != 0)
{
if (domain_err == -2)
{
return HOST_NOT_FOUND;
}
return NO_DATA;
}
}
}
else
{
inet_aton("127.0.0.1", &addr);
}
total_size = sizeof(struct addrinfo) + sizeof(struct sockaddr_storage);
if (nodename != RT_NULL)
{
total_size += namelen + 1;
}
if (total_size > sizeof(struct addrinfo) + sizeof(struct sockaddr_storage) + DNS_MAX_NAME_LENGTH + 1)
{
return EAI_FAIL;
}
ai = (struct addrinfo *)rt_malloc(total_size);
if (ai == RT_NULL)
{
return EAI_MEMORY;
}
rt_memset(ai, 0, total_size);
sa = (struct sockaddr_storage *)(void *)((uint8_t *)ai + sizeof(struct addrinfo));
{
struct sockaddr_in *sa4 = (struct sockaddr_in *)sa;
#if NETDEV_IPV4 && NETDEV_IPV6
sa4->sin_addr.s_addr = addr.u_addr.ip4.addr;
sa4->type = IPADDR_TYPE_V4;
#elif NETDEV_IPV4
sa4->sin_addr.s_addr = addr.addr;
#elif NETDEV_IPV6
#error "not support IPV6."
#endif
sa4->sin_family = AF_INET;
sa4->sin_len = sizeof(struct sockaddr_in);
sa4->sin_port = htons((uint16_t)port_nr);
}
ai->ai_family = AF_INET;
if (hints != RT_NULL)
{
ai->ai_socktype = hints->ai_socktype;
ai->ai_protocol = hints->ai_protocol;
}
if (nodename != RT_NULL)
{
ai->ai_canonname = ((char *)ai + sizeof(struct addrinfo) + sizeof(struct sockaddr_storage));
rt_memcpy(ai->ai_canonname, nodename, namelen);
ai->ai_canonname[namelen] = '\0';
}
ai->ai_addrlen = sizeof(struct sockaddr_storage);
ai->ai_addr = (struct sockaddr *)sa;
*res = ai;
return 0;
}
Optional cleanup for at_gethostbyname()
This is not the root overflow fix, but it avoids repeated strlen() and keeps the wrapper consistent:
struct hostent *at_gethostbyname(const char *name)
{
ip_addr_t addr = {0};
static struct hostent s_hostent;
static char *s_aliases;
static ip_addr_t s_hostent_addr;
static ip_addr_t *s_phostent_addr[2];
static char s_hostname[DNS_MAX_NAME_LENGTH + 1];
size_t name_len = 0;
if (name == RT_NULL)
{
LOG_E("AT gethostbyname input name error!");
return RT_NULL;
}
name_len = strlen(name);
if (name_len > DNS_MAX_NAME_LENGTH)
{
return RT_NULL;
}
if (_gethostbyname_by_device(name, &addr) != 0)
{
return RT_NULL;
}
s_hostent_addr = addr;
s_phostent_addr[0] = &s_hostent_addr;
s_phostent_addr[1] = RT_NULL;
rt_memcpy(s_hostname, name, name_len);
s_hostname[name_len] = '\0';
s_aliases = RT_NULL;
s_hostent.h_name = s_hostname;
s_hostent.h_aliases = &s_aliases;
s_hostent.h_addrtype = AF_AT;
s_hostent.h_length = sizeof(ip_addr_t);
s_hostent.h_addr_list = (char **)&s_phostent_addr;
return &s_hostent;
}
Why I believe a CVE-level fix is warranted
- The bug has been present since
v3.1.0
- It is still present in current master
- It can corrupt kernel stack memory
- It is reachable through standard exported resolver APIs on AT-backed deployments
- On MMU/LWP builds it crosses a real unprivileged userspace -> kernel trust boundary
Kindly let me know if you intend to request a CVE ID upon confirmation of the vulnerability.
Other additional context
No response
RT-Thread Version
master @ bd53bba The vulnerable code was introduced by commit 8b887e7 ("[net][at] Add AT commands component", 2018-07-25) and is present in at least v3.1.0 through v5.2.2, as well as current master.
Hardware Type/Architectures
Any RT-Thread BSP / architecture that enables the AT socket resolver path
Develop Toolchain
GCC
Describe the bug
I verified the bug by source audit on current master and by checking historical tags/commits. The impact is typically strongest on GCC ARM / RISC-V / AArch64 builds without strong stack hardening, but the bug is not toolchain-specific.
Describe the bug
The AT socket resolver in
components/net/at/at_socket/at_socket.ctreats hostnames with no alphabetic characters as "numeric" and copies them into a fixed-size stack buffer using an unbounded length:This happens in
_gethostbyname_by_device()beforeinet_aton()validates the string. As a result:ipstr[16]without a terminator, which can still trigger a subsequent out-of-bounds read ininet_aton().The bug is reachable through public resolver APIs, not only AT-private helpers:
getaddrinfo()->sal_getaddrinfo()->at_getaddrinfo()->_gethostbyname_by_device()gethostbyname()/gethostbyname_r()-> SAL netdb ->at_gethostbyname*()->_gethostbyname_by_device()On MMU/LWP builds, this is a real user-to-kernel trust-boundary crossing because unprivileged userspace can reach the vulnerable kernel resolver path through standard network APIs.
Important scope clarification:
rt_memcpy(ai->ai_canonname, nodename, namelen)inat_getaddrinfo()is on the same affected wrapper path, but it is not the root overflow primitive here. The primary memory corruption sink is the numeric-host branch in_gethostbyname_by_device().Code locations
Primary vulnerable sink:
components/net/at/at_socket/at_socket.c_gethostbyname_by_device()Affected wrappers:
at_gethostbyname()at_gethostbyname_r()at_getaddrinfo()Relevant standard-entry bridges:
components/net/sal/socket/net_netdb.ccomponents/net/sal/src/sal_socket.ccomponents/lwp/lwp_syscall.cPoC 1: strongest path, unprivileged userspace -> kernel stack corruption (MMU/LWP builds)
Build a target with:
RT_USING_AT=yAT_USING_CLIENT=yAT_USING_SOCKET=yRT_USING_SAL=yThen run an unprivileged userspace program that calls standard POSIX resolution APIs with a numeric-looking hostname longer than 16 bytes and containing no alphabetic characters:
Expected kernel-side path:
sys_getaddrinfo()sal_getaddrinfo()at_getaddrinfo()_gethostbyname_by_device()strncpy(ipstr, name, strlen(name))Expected result:
PoC 2: direct kernel task / application task trigger
Even without MMU/LWP, the bug is directly reachable from local RT-Thread tasks using public resolver APIs:
or AT-specific direct call:
Expected result:
_gethostbyname_by_device()before address parsing completes.PoC 3: exact-fit 16-byte edge case
This is useful to demonstrate boundary semantics precisely:
This may not perform a stack write beyond
ipstr[16], but it removes the terminator and can still drive a subsequent out-of-bounds read ininet_aton().The resolver should reject overlong numeric-looking host strings before copying them into
ipstr[16], and all wrapper APIs should validate hostname length before calling_gethostbyname_by_device().At a minimum:
strlen(name) >= sizeof(ipstr)must fail safely;strncpy(dst, src, strlen(src))for fixed-size stack buffers.Not available yet. This report is based on code audit and version-history verification.
Other additional context
impact
This is not dependent on a malicious AT module callback violating its contract. The bug is triggered on the local numeric-host branch before
at_domain_resolve()is involved.This also is not just an "API misuse" issue in private code:
getaddrinfo(),gethostbyname*()) can route into the vulnerable AT backend;So the bug is better described as:
Precise overflow boundary
ipstr[16], may cause later OOB readFix suggestion
The minimum safe fix is:
_gethostbyname_by_device();_gethostbyname_by_device()fromat_getaddrinfo();at_gethostbyname()to avoid repeatedstrlen()and use explicit length-based copy after validation.Suggested replacement for
_gethostbyname_by_device()Suggested replacement for
at_getaddrinfo()Optional cleanup for
at_gethostbyname()This is not the root overflow fix, but it avoids repeated
strlen()and keeps the wrapper consistent:Why I believe a CVE-level fix is warranted
v3.1.0Kindly let me know if you intend to request a CVE ID upon confirmation of the vulnerability.
Other additional context
No response