Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integer underflow when parsing zeroed out RTA_MULTIPATH packets #152

Closed
aknobloch opened this issue Mar 22, 2025 · 5 comments · Fixed by #153
Closed

Integer underflow when parsing zeroed out RTA_MULTIPATH packets #152

aknobloch opened this issue Mar 22, 2025 · 5 comments · Fixed by #153

Comments

@aknobloch
Copy link

aknobloch commented Mar 22, 2025

Hello, I work on Starnix which employs the rust-netlink library. We have a fuzzer that helps catch bugs in our kernel implementation, and recently it identified a bug that I've traced down to rust-netlink. Apologies that I don't have a better reproduction environment, but do let me know if it's a barrier and I can draft something up. In any case, here's a sample C program which exemplifies the issue:

#define _GNU_SOURCE

#include <endian.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/socket.h>

int main(void)
{
        const uintptr_t BASE_ADDRESS = 0x20000000ul;
        const size_t MAP_SIZE = 0x1000000ul;
        const uintptr_t POINTER_TO_MSGVEC = BASE_ADDRESS + 0x2d0;
        const uintptr_t MSGVEC_COUNT_ADDRESS = BASE_ADDRESS + 0x2d8;
        const uintptr_t MSGVEC_ADDRESS = BASE_ADDRESS + 0x280;
        const uintptr_t MESSAGE_DATA_ADDRESS = BASE_ADDRESS + 0x300;
        const uintptr_t MESSAGE_LENGTH_ADDRESS = BASE_ADDRESS + 0x288;
        const uintptr_t MMSGHDR_ADDRESS = BASE_ADDRESS + 0x2c0;
        const size_t MESSAGE_DATA_SIZE = 55;

        // Allocate executable memory using mmap
        syscall(__NR_mmap, 
                BASE_ADDRESS, 
                MAP_SIZE,
                PROT_WRITE | PROT_READ | PROT_EXEC,
                MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE,
                /* file descriptor, irrelevant since we're MAP_ANONYMOUS */ -1,
                /* offset, also irrelevant */ 0);

        // Construct the message vector object for sendmmsg
        // struct mmsghdr {
        //     struct msghdr msg_hdr;  /* Message header */
        //     unsigned int  msg_len;  /* Number of bytes transmitted */
        // };
        //
        // struct msghdr {
        //     void         *msg_name;       /* optional address */
        //     socklen_t     msg_namelen;    /* size of address */
        //     struct iovec *msg_iov;        /* scatter/gather array */
        //     size_t        msg_iovlen;     /* # elements in msg_iov */
        //     void         *msg_control;    /* ancillary data, see below */
        //     size_t        msg_controllen; /* ancillary data buffer len */
        //     int           msg_flags;      /* flags on received message */
        // };
        *(uint64_t *) POINTER_TO_MSGVEC = MSGVEC_ADDRESS;
        *(uint64_t *) MSGVEC_COUNT_ADDRESS = 1;
        *(uint64_t *) MSGVEC_ADDRESS = MESSAGE_DATA_ADDRESS;
        const unsigned char messageData[] = {
                0x4c, 0x00, 0x00, 0x00, 0x18, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x85, 0x0a, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x05, 0x00, 0x00, 0x00, 0x14, 0x00, 0x05, 0x00, 0x20, 0x01, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x1c, 0x00, 0x09, 0x00, 0x08, 0x00, 0x00
            };
        memcpy((void *) MESSAGE_DATA_ADDRESS,
                messageData,
                MESSAGE_DATA_SIZE);

        // Create a Netlink socket and send the message over said socket.
        *(uint64_t *) MESSAGE_LENGTH_ADDRESS = 0x4c;
        intptr_t socket_fd = syscall(__NR_socket, AF_NETLINK, SOCK_RAW, 0);

        // Print mmsghdr before sendmmsg
        struct mmsghdr *mmsghdr_ptr = (struct mmsghdr *) MMSGHDR_ADDRESS;
        printf("mmsghdr before sendmmsg:\n");
        printf("  msg_hdr.msg_name: %p\n", mmsghdr_ptr->msg_hdr.msg_name);
        printf("  msg_hdr.msg_namelen: %u\n", mmsghdr_ptr->msg_hdr.msg_namelen);
        printf("  msg_hdr.msg_iov: %p\n", mmsghdr_ptr->msg_hdr.msg_iov);
        printf("  msg_hdr.msg_iovlen: %lu\n", mmsghdr_ptr->msg_hdr.msg_iovlen);
        printf("  msg_hdr.msg_control: %p\n", mmsghdr_ptr->msg_hdr.msg_control);
        printf("  msg_hdr.msg_controllen: %lu\n", mmsghdr_ptr->msg_hdr.msg_controllen);
        printf("  msg_hdr.msg_flags: %d\n", mmsghdr_ptr->msg_hdr.msg_flags);
        printf("  msg_len: %u\n", mmsghdr_ptr->msg_len);

        syscall(__NR_sendmmsg, 
                socket_fd,
                /* mmsghdr, which should be a struct with hdr and len of bytes from hdr to send */ MMSGHDR_ADDRESS,
                /* vlen, which is an upper limit on messages sent  */ 0x40000000000009ful,
                MSG_CONFIRM | /* ARPHRD_TUNNEL */ 0x300);
        perror("sendmmsg done!");
        return 0;
}

On Linux, this program cleanly completes (including the sendmmsg invocation). When using the rust-netlink library as the socket implementation for sendmmsg, we see an integer underflow. Our forked code is slightly different from the current source, so I'll skip sharing the exact stacktrace. But the issue lies in the RTA_MULTIPATH implementation, specifically here.'

With the above program, the input payload for RouteAttribute parsing will be:

[8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

In this case, the first packet is successfully deconstructed. We end up with a remaining buffer like so:

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

From my reading of the code, the current algorithm interprets the first two bytes as the length, which is zero. This appeases the RouteNextHopBuffer::check_buffer_length() code, so we attempt to parse the next hop. While performing this parsing, we will attempt to slice out the attributes by taking the length of the input buffer and slicing out the payload offset (source). Since the self-reported length() is actually zero, we end up with the equation 0 - 8, which causes the integer underflow panic.

Identifying the correct fix is a little more tricky than slapping in some guards, since I'm not sure how the Netlink spec indicates we should handle this payload. It seems intuitive to me that we should just breeze past the remainder of the data, since the multi packet self-reported claim is a nothing-packet. But I figured it'd be wise to start a thread here to discuss. Any thoughts?

@cathay4t
Copy link
Member

The RouteNextHopBuffer::check_buffer_length() will not invoke RouteNextHopBuffer::attributes(). And its check_buffer_length() already confirmed the self.length() >= PAYLOAD_OFFSET.

I don't have much time now, will check later.

@cathay4t
Copy link
Member

Oh, you are right. Now I get it. Will fix it once I got free time.

@cathay4t
Copy link
Member

cathay4t commented Mar 22, 2025

The iproute code print_rta_multipath() has

while (len >= sizeof(*nh)) {
    ...
    len -= NLMSG_ALIGN(nh->rtnh_len);
    nh = RTNH_NEXT(nh);
    close_json_object();
}

With #define RTNH_NEXT(rtnh) ((struct rtnexthop*)(((char*)(rtnh)) + RTNH_ALIGN((rtnh)->rtnh_len))).

So it will cause dead loop in iproute2 for data struct above as NLMSG_ALIGN(0) is still 0.

Hence I think [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] is not a valid RouteNextHopBuffer.

cathay4t added a commit to cathay4t/netlink-packet-route that referenced this issue Mar 22, 2025
For buffer `[0u8; 8]`, the `RouteNextHopBuffer::attributes()` will panic
on `self.length() - 8` as `0u16 -8` will trigger underflow.

Fixed by raising error when `self.length() < PAYLOAD_OFFSET`.

We consider above all zero buffer as invalid data type instead of
skipping it during pasing is because:

 1. The length property is invalid, it should always bigger or equal than 8
    because it is the length of payload plus header.
 2. The same data will cause error in iproute2 also:

The iproute code `print_rta_multipath()` has:

```c
while (len >= sizeof(*nh)) {
    ...
    len -= NLMSG_ALIGN(nh->rtnh_len);
    nh = RTNH_NEXT(nh);
    close_json_object();
}
```

The `RTNH_NEXT` is defined as

```c
    ((struct rtnexthop*)(((char*)(rtnh)) + RTNH_ALIGN((rtnh)->rtnh_len)))
```

It will cause dead loop in iproute2 for `[0u8;8]` above as
NLMSG_ALIGN(0) is still 0.

Resolves: rust-netlink#152

Signed-off-by: Gris Ge <[email protected]>
@aknobloch
Copy link
Author

FYI, it looks like most code in the Linux kernel actual processes these hops in a while loop, thus silently skipping if it encounters an invalid entry such as the one given in the repro example of this issue. Here's an example from the Linux source. This also would explain why the same code returns cleanly on Linux. It's a philosophical discussion as to whether rust-netlink should throw an error or silently continue to process the packets it was able to process. I can see reasonable positions for either behavior. And of course I'm not a maintainer, so don't have any weight in the discussion. Just figured that I'd point it out, in case it influences the thinking here!

@cathay4t
Copy link
Member

As point I made in PR #153, I think it is a invalid packet and raise error is better than tolerate. @little-dude @JohnTitor your votes?

cathay4t added a commit to cathay4t/netlink-packet-route that referenced this issue Apr 8, 2025
For buffer `[0u8; 8]`, the `RouteNextHopBuffer::attributes()` will panic
on `self.length() - 8` as `0u16 -8` will trigger underflow.

Fixed by raising error when `self.length() < PAYLOAD_OFFSET`.

We consider above all zero buffer as invalid data type instead of
skipping it during pasing is because:

 1. The length property is invalid, it should always bigger or equal than 8
    because it is the length of payload plus header.
 2. The same data will cause error in iproute2 also:

The iproute code `print_rta_multipath()` has:

```c
while (len >= sizeof(*nh)) {
    ...
    len -= NLMSG_ALIGN(nh->rtnh_len);
    nh = RTNH_NEXT(nh);
    close_json_object();
}
```

The `RTNH_NEXT` is defined as

```c
    ((struct rtnexthop*)(((char*)(rtnh)) + RTNH_ALIGN((rtnh)->rtnh_len)))
```

It will cause dead loop in iproute2 for `[0u8;8]` above as
NLMSG_ALIGN(0) is still 0.

Resolves: rust-netlink#152

Signed-off-by: Gris Ge <[email protected]>
cathay4t added a commit that referenced this issue Apr 8, 2025
For buffer `[0u8; 8]`, the `RouteNextHopBuffer::attributes()` will panic
on `self.length() - 8` as `0u16 -8` will trigger underflow.

Fixed by raising error when `self.length() < PAYLOAD_OFFSET`.

We consider above all zero buffer as invalid data type instead of
skipping it during pasing is because:

 1. The length property is invalid, it should always bigger or equal than 8
    because it is the length of payload plus header.
 2. The same data will cause error in iproute2 also:

The iproute code `print_rta_multipath()` has:

```c
while (len >= sizeof(*nh)) {
    ...
    len -= NLMSG_ALIGN(nh->rtnh_len);
    nh = RTNH_NEXT(nh);
    close_json_object();
}
```

The `RTNH_NEXT` is defined as

```c
    ((struct rtnexthop*)(((char*)(rtnh)) + RTNH_ALIGN((rtnh)->rtnh_len)))
```

It will cause dead loop in iproute2 for `[0u8;8]` above as
NLMSG_ALIGN(0) is still 0.

Resolves: #152

Signed-off-by: Gris Ge <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants