RT-Thread / RT-Thread/rt-thread

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

Open
#11,332 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C
Stars
12.2k
Forks
5.4k
Avg merge
4d 12h
Merged PRs (30d)
40

Description

RT-Thread Version

master @ bd53bbaa7674e8cc0334290ff61cc743ab727202 The vulnerable code was introduced by commit 8b887e76e929904a365dff64c1507afc47b93e3f ("[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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in components/net/at/at_socket/at_socket.c at _gethostbyname_by_device(), then trace the resolver entry points through at_getaddrinfo(), at_gethostbyname*(), components/net/sal/socket/net_netdb.c, components/net/sal/src/sal_socket.c, and components/lwp/lwp_syscall.c. Reproduce with the numeric-host examples in the issue on an AT socket configuration. Done means overlong inputs fail safely without stack corruption across the public resolver paths.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
embedded-iot, operating-systems, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.