Skip to content

[Bug] RT-Thread AT socket resolver: user-controlled numeric hostname triggers kernel stack overflow via getaddrinfo()/gethostbyname*() #11332

@XueDugu

Description

@XueDugu

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
  1. 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().

  1. 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.
  1. 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:

  1. fully bound and terminate the numeric-host local copy in _gethostbyname_by_device();
  2. reject overlong hostnames before entering _gethostbyname_by_device() from at_getaddrinfo();
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions