Skip to content

Commit fafdb43

Browse files
committed
Add netlink getsockname/sendto/recvfrom regression test
make check did not exercise the getsockname/sendto/recvfrom paths that the netlink emulation now handles, so a regression reverting them to the host socket fd (ENOTSOCK) would pass unnoticed. Add tests/test-netlink.c and register it in the manifest. The test drives each dispatched syscall directly against a NETLINK_ROUTE socket: getsockname must report an AF_NETLINK address and a non-zero port id, a flat RTM_GETLINK dump request must be accepted by sendto, and recvfrom must drain at least one RTM_NEWLINK message with an AF_NETLINK source. It then runs glibc getifaddrs(), the end-to-end call from issue #53 that originally failed with ENOTSOCK. Only implementation-independent netlink semantics are asserted, so the same static binary passes under a real kernel as well as the emulation. Validated with make check on Apple Silicon (9/9 checks pass); the same binary run against an elfuse built from main reproduces the bug with 7 ENOTSOCK failures, confirming the test catches the regression.
1 parent 47fc927 commit fafdb43

2 files changed

Lines changed: 136 additions & 0 deletions

File tree

tests/manifest.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ test-sysfs-cpu
7171
[section] Network tests
7272
test-net
7373
test-netstat
74+
test-netlink
7475

7576
[section] Threading tests
7677
test-thread # diff=skip

tests/test-netlink.c

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/* Exercise the AF_NETLINK getsockname/sendto/recvfrom dispatch paths.
2+
*
3+
* Copyright 2026 elfuse contributors
4+
* Copyright 2025 Moritz Angermann, zw3rk pte. ltd.
5+
* SPDX-License-Identifier: Apache-2.0
6+
*
7+
* Regression guard for the netlink socket emulation. Before getsockname,
8+
* sendto, and recvfrom were routed to the netlink handlers, these calls fell
9+
* through to the host socket syscalls on the underlying pipe fd and failed
10+
* with ENOTSOCK (errno 88), which in turn broke glibc getifaddrs(). The test
11+
* drives each of the three syscalls directly against a NETLINK_ROUTE socket
12+
* and then validates the end-to-end getifaddrs() path that originally
13+
* regressed.
14+
*
15+
* The assertions hold for both the elfuse emulation and a real Linux kernel
16+
* (the test matrix runs the same binary under qemu-aarch64), so only
17+
* implementation-independent netlink semantics are checked.
18+
*/
19+
20+
#include <errno.h>
21+
#include <ifaddrs.h>
22+
#include <stdio.h>
23+
#include <string.h>
24+
#include <unistd.h>
25+
26+
#include <sys/socket.h>
27+
#include <linux/netlink.h>
28+
#include <linux/rtnetlink.h>
29+
30+
static int pass, fail;
31+
32+
#define CHECK(cond, msg) \
33+
do { \
34+
if (cond) { \
35+
printf("PASS: %s\n", (msg)); \
36+
pass++; \
37+
} else { \
38+
printf("FAIL: %s (errno=%d %s)\n", (msg), errno, strerror(errno)); \
39+
fail++; \
40+
} \
41+
} while (0)
42+
43+
/* RTM_GETLINK dump request: nlmsghdr immediately followed by ifinfomsg. */
44+
struct getlink_req {
45+
struct nlmsghdr nlh;
46+
struct ifinfomsg ifi;
47+
};
48+
49+
int main(void)
50+
{
51+
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
52+
if (fd < 0) {
53+
printf("FAIL: socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE): %s\n",
54+
strerror(errno));
55+
/* Without a socket none of the dispatch paths can be reached. */
56+
printf("\n%d passed, %d failed\n", pass, fail + 1);
57+
return 1;
58+
}
59+
printf("PASS: socket(AF_NETLINK, NETLINK_ROUTE) = %d\n", fd);
60+
pass++;
61+
62+
/* bind() with nl_pid=0 lets the kernel/emulation assign a port id. */
63+
struct sockaddr_nl local = {.nl_family = AF_NETLINK};
64+
CHECK(bind(fd, (struct sockaddr *) &local, sizeof(local)) == 0,
65+
"bind(AF_NETLINK)");
66+
67+
/* 1. getsockname(): previously ENOTSOCK on the pipe fd. */
68+
struct sockaddr_nl got = {0};
69+
socklen_t gotlen = sizeof(got);
70+
int rc = getsockname(fd, (struct sockaddr *) &got, &gotlen);
71+
CHECK(rc == 0 && gotlen >= sizeof(struct sockaddr_nl) &&
72+
got.nl_family == AF_NETLINK,
73+
"getsockname() returns an AF_NETLINK address");
74+
CHECK(rc == 0 && got.nl_pid != 0,
75+
"getsockname() reports a non-zero port id");
76+
77+
/* 2. sendto(): flat request buffer, no msghdr. */
78+
struct getlink_req req = {0};
79+
req.nlh.nlmsg_len = sizeof(req);
80+
req.nlh.nlmsg_type = RTM_GETLINK;
81+
req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
82+
req.nlh.nlmsg_seq = 1;
83+
req.ifi.ifi_family = AF_UNSPEC;
84+
85+
struct sockaddr_nl kernel = {.nl_family = AF_NETLINK};
86+
ssize_t sent = sendto(fd, &req, req.nlh.nlmsg_len, 0,
87+
(struct sockaddr *) &kernel, sizeof(kernel));
88+
CHECK(sent == (ssize_t) req.nlh.nlmsg_len,
89+
"sendto(RTM_GETLINK) accepts the request");
90+
91+
/* 3. recvfrom(): drain the dump, expecting RTM_NEWLINK then NLMSG_DONE. */
92+
int saw_newlink = 0, saw_done = 0, src_ok = 0;
93+
for (int iter = 0; iter < 64 && !saw_done; iter++) {
94+
char buf[8192];
95+
struct sockaddr_nl src = {0};
96+
socklen_t srclen = sizeof(src);
97+
ssize_t n = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *) &src,
98+
&srclen);
99+
if (n <= 0)
100+
break;
101+
if (srclen >= sizeof(struct sockaddr_nl) && src.nl_family == AF_NETLINK)
102+
src_ok = 1;
103+
for (struct nlmsghdr *nlh = (struct nlmsghdr *) buf;
104+
NLMSG_OK(nlh, (unsigned) n); nlh = NLMSG_NEXT(nlh, n)) {
105+
if (nlh->nlmsg_type == RTM_NEWLINK)
106+
saw_newlink = 1;
107+
else if (nlh->nlmsg_type == NLMSG_DONE)
108+
saw_done = 1;
109+
else if (nlh->nlmsg_type == NLMSG_ERROR)
110+
saw_done = 1; /* stop draining on error terminator */
111+
}
112+
}
113+
CHECK(saw_newlink, "recvfrom() returns at least one RTM_NEWLINK");
114+
CHECK(src_ok, "recvfrom() fills an AF_NETLINK source address");
115+
116+
close(fd);
117+
118+
/* 4. End-to-end: glibc getifaddrs() drives getsockname + sendto + recv
119+
* internally. This is the exact call that regressed with ENOTSOCK.
120+
*/
121+
struct ifaddrs *ifa = NULL;
122+
rc = getifaddrs(&ifa);
123+
CHECK(rc == 0, "getifaddrs() succeeds");
124+
int n_ifaces = 0;
125+
for (struct ifaddrs *p = ifa; p; p = p->ifa_next)
126+
if (p->ifa_name && p->ifa_name[0])
127+
n_ifaces++;
128+
CHECK(rc == 0 && n_ifaces > 0,
129+
"getifaddrs() enumerates at least one interface");
130+
if (ifa)
131+
freeifaddrs(ifa);
132+
133+
printf("\n%d passed, %d failed\n", pass, fail);
134+
return fail ? 1 : 0;
135+
}

0 commit comments

Comments
 (0)