Skip to content

[Bug] Security-related Bug: sys_getaddrinfo trusts nested user pointer and can write outside validated user memory #11428

@zephyr-saxon

Description

@zephyr-saxon

RT-Thread Version

v5.2.2

Hardware Type/Architectures

N/A

Develop Toolchain

Other

Describe the bug

sys_getaddrinfo trusts nested user pointer and can write outside validated user memory

Summary

I identified an insufficient user-pointer validation issue in the sys_getaddrinfo system call in RT-Thread Smart.

When ARCH_MM_MMU is enabled, sys_getaddrinfo() validates that the outer res pointer is user-accessible. However, after a successful name lookup it reads res->ai_addr directly from that user-controlled structure and passes it to sockaddr_tomusl(). sockaddr_tomusl() then writes a struct musl_sockaddr result through that nested pointer without checking that the nested destination is valid user memory.

This gives an unprivileged user process control over the kernel write destination used for the returned socket address. If the nested pointer targets mapped kernel or otherwise sensitive memory, the syscall can perform an unauthorized write. If it targets unmapped or faulting memory, the direct kernel write can crash the system, causing a denial of service.

Vulnerable Code Location

The vulnerable syscall is registered in the LWP syscall table:

components/lwp/lwp_syscall.c:11085  SYSCALL_NET(SYSCALL_SIGN(sys_getaddrinfo)),

The affected code is in:

components/lwp/lwp_syscall.c:7051  sys_getaddrinfo(...)
components/lwp/lwp_syscall.c:7163  sockaddr_tomusl(k_res->ai_addr, res->ai_addr)
components/lwp/lwp_syscall.c:314   sockaddr_tomusl(...)
components/lwp/lwp_sys_socket.h:105 struct musl_sockaddr

The important data flow is:

user-controlled res
  -> sys_getaddrinfo(..., struct musl_addrinfo *res)
  -> lwp_user_accessable(res, sizeof(*res)) checks only the outer object
  -> res->ai_addr is read directly from user memory
  -> sockaddr_tomusl(k_res->ai_addr, res->ai_addr)
  -> std->sa_family write and memcpy(std->sa_data, ...)
  -> attacker-selected destination write or kernel fault

Relevant code:

struct musl_addrinfo
{
    int ai_flags;
    int ai_family;
    int ai_socktype;
    int ai_protocol;
    socklen_t ai_addrlen;

    struct musl_sockaddr *ai_addr;
    char *ai_canonname;

    struct musl_addrinfo *ai_next;
};

sysret_t sys_getaddrinfo(const char *nodename,
        const char *servname,
        const struct musl_addrinfo *hints,
        struct musl_addrinfo *res)
{
    ...
#ifdef ARCH_MM_MMU
    if (!lwp_user_accessable((void *)res, sizeof(*res)))
    {
        SET_ERRNO(EFAULT);
        goto exit;
    }
#endif
    ...
    ret = sal_getaddrinfo(k_nodename, k_servname, k_hints, &k_res);
    if (ret == 0)
    {
        /* res is validated, but res->ai_addr is not. */
        sockaddr_tomusl(k_res->ai_addr, res->ai_addr);
        res->ai_addrlen = k_res->ai_addrlen;

        res->ai_family = k_res->ai_family;
        res->ai_flags  = k_res->ai_flags;
        res->ai_next = NULL;
        ...
    }
    ...
}

sockaddr_tomusl() only checks for NULL. It does not validate that std points to writable user memory and it does not use lwp_put_to_user():

static void sockaddr_tomusl(const struct sockaddr *lwip, struct musl_sockaddr *std)
{
    if (std && lwip)
    {
        std->sa_family = (uint16_t) lwip->sa_family;
        memcpy(std->sa_data, lwip->sa_data, sizeof(std->sa_data));
    }
}

The destination type is 16 bytes:

struct musl_sockaddr
{
    uint16_t sa_family;
    char     sa_data[14];
};

Vulnerability Description

The syscall validates only the top-level res pointer:

lwp_user_accessable((void *)res, sizeof(*res))

That check proves only that the struct musl_addrinfo object itself is in accessible user memory. It does not prove that the pointer stored inside res->ai_addr is safe to write to.

After sal_getaddrinfo() succeeds, the kernel directly evaluates res->ai_addr and uses it as the output destination for the resolved socket address:

sockaddr_tomusl(k_res->ai_addr, res->ai_addr);

Because res is user-controlled, res->ai_addr is also user-controlled. A malicious process can set it to:

  1. A kernel or sensitive memory address that should not be writable from user mode, causing an unauthorized kernel write.
  2. An unmapped or invalid address, causing a kernel fault and denial of service.
  3. Another user process's mapped memory, if such mappings are reachable in the current address-space configuration.

The bug is similar in shape to other nested-pointer syscall bugs: checking the outer structure is not enough. Each user-supplied pointer contained inside that structure must be copied into kernel memory and validated before use, and writes back to user space should go through the kernel's user-copy helpers.

Steps to Reproduce

I have not reproduced this on physical RT-Thread Smart hardware. The issue is visible from source-level inspection of the syscall path.

Required configuration:

RT_USING_SMART or RT_USING_LWP enabled
ARCH_MM_MMU enabled
RT_USING_SAL enabled
SAL_USING_POSIX enabled

Several Smart/MMU board configurations in the checked tree enable the relevant pieces, including:

bsp/nxp/imx/imx6ull-smart/rtconfig.h
bsp/k230/rtconfig.h
bsp/cvitek/cv18xx_risc-v/rtconfig.h
bsp/rockchip/rk3500/rtconfig.h
bsp/raspberry-pi/raspi-dm2.0/rtconfig.h

Conceptual trigger from an unprivileged process:

struct musl_addrinfo res;

memset(&res, 0, sizeof(res));

/*
 * The outer object &res is a valid user pointer, so sys_getaddrinfo()
 * accepts it. The nested pointer is attacker-controlled.
 */
res.ai_addr = (struct musl_sockaddr *)ATTACKER_CHOSEN_ADDRESS;

sys_getaddrinfo("127.0.0.1", "80", NULL, &res);

If ATTACKER_CHOSEN_ADDRESS is writable kernel memory, the syscall writes the returned address family and 14 bytes of address data there. If it is invalid or unmapped, the same direct write can fault in kernel context.

Expected safe behavior:

sys_getaddrinfo() should reject an inaccessible nested res->ai_addr pointer with EFAULT,
or copy the result through a validated kernel temporary and lwp_put_to_user().

Current behavior:

sys_getaddrinfo() validates only res, then writes through res->ai_addr directly.

Impact

This vulnerability can allow an unprivileged process to influence where the kernel writes the resolved socket address returned by sys_getaddrinfo().

Potential impacts include:

Unauthorized memory write:
  The attacker controls res->ai_addr, and sockaddr_tomusl() writes a 16-byte
  musl_sockaddr result to that address.

Denial of service:
  If res->ai_addr points to unmapped or otherwise faulting memory, the direct
  kernel write can crash or destabilize the system.

Privilege escalation risk:
  On targets where user-controlled syscall writes can reach sensitive kernel
  state, the write may corrupt kernel objects or control data.

The exact exploitability depends on the target MMU mapping, kernel address exposure, and RT-Thread Smart memory protection configuration. The core issue is that the syscall crosses the user/kernel boundary with a nested attacker-controlled pointer and writes through it without validation.

Suggested Fix

Do not read or write nested user pointers directly from res.

One safer pattern is:

1. Copy the outer musl_addrinfo from user memory into a kernel temporary.
2. Validate copied_res.ai_addr with lwp_user_accessable(copied_res.ai_addr, sizeof(struct musl_sockaddr)).
3. Convert the kernel sockaddr into a kernel musl_sockaddr temporary.
4. Use lwp_put_to_user(copied_res.ai_addr, &tmp_sockaddr, sizeof(tmp_sockaddr)).
5. Use lwp_put_to_user(res, &tmp_addrinfo, sizeof(tmp_addrinfo)) for the outer result fields.

The same pattern should also be used for reading hints: copy it once with lwp_get_from_user() and use the kernel copy, instead of repeatedly reading fields from the user pointer.

Environment

Initial RT-Thread commit checked: c39e92f4c1
Version: 5.2.2
Affected file: components/lwp/lwp_syscall.c
Affected syscall: sys_getaddrinfo
Affected configuration: RT-Thread Smart / LWP with ARCH_MM_MMU and SAL POSIX networking
Target hardware: not tested on board yet
Verification: source-level syscall data-flow review

Other additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions