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

Aperta
#11,332 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
48/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Tranquilla
Stack tecnologico
c

Direzione di ricerca

Inizia da components/net/at/at_socket/at_socket.c, in _gethostbyname_by_device(), quindi segui i punti di ingresso del resolver attraverso at_getaddrinfo(), at_gethostbyname*(), components/net/sal/socket/net_netdb.c, components/net/sal/src/sal_socket.c e components/lwp/lwp_syscall.c. Riproduci il problema con gli esempi di host numerici dell’issue su una configurazione socket AT. Il lavoro è completo quando gli input troppo lunghi falliscono in modo sicuro, senza corruzione dello stack, lungo tutti i percorsi pubblici del resolver.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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

Lingua principale
C
Stelle
12.2k
Fork
5.4k
Merge medio
4g 12h
PR unite (30g)
40

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di RT-Thread/rt-thread

Tutte le issue di RT-Thread/rt-thread

Issue simili

Altre issue su C

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.