mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
* fix(preload): answer NETLINK_ROUTE queries inside a session ip(8) could not run in a session: the app's SELinux domain has no netlink_route_socket bind permission, so iproute2 failed at bind and never saw the link that getifaddrs() and /proc/net already expose. Serve NETLINK_ROUTE from the same etc/droiddeck-net snapshot: an AF_UNIX socketpair backs the netlink fd, link/addr/route dumps and route get are synthesised, writes return EPERM, and multicast subscriptions keep failing with EACCES so consumers that wait for link events keep their fallback. Replies carry the port id reported by getsockname() and recvmsg() rewrites the source address to AF_NETLINK/pid 0. iproute2's dump loop requires both before it accepts a message or NLMSG_DONE; when either is missing every message is skipped silently and the process blocks in recvmsg(). * fix(preload): keep socket() and send() semantics for emulated netlink * fix(preload): deliver netlink replies with the caller's blocking mode * fix(preload): preserve NETLINK_ROUTE protocol and fd semantics - Send ACKs for successful queries and honor NETLINK_CAP_ACK - Reject unsupported subscriptions and apply socket timeouts - Track socket identity and shared references across dup and close - Support kernel connect and rewrite recvfrom source addresses - Resolve local and connected routes before default gateways - Add netlink and iproute2 regression tests * fix(preload): handle netlink request limits and socket lifecycle - Collect large sendmsg requests without falling back to UNIX sockets - Reject oversized replies explicitly instead of returning partial results - Allocate unique port IDs and reject duplicate bindings - Honor socket flags and restore inherited sockets across exec - Validate socket types and report netlink socket identity - Replace the fixed socket table with a dynamic registry - Add regression tests for batching, port conflicts and exec inheritance * fix(preload): optimize netlink tracking and support link name queries - Add lock-free fd tracking to bypass netlink locks on ordinary sockets - Move descriptor restoration to initialization and explicit discovery paths - Support RTM_GETLINK lookups via IFLA_IFNAME with correct error codes - Add regression coverage for hot-path overhead and name-based queries * fix(preload): accept unterminated netlink interface names - Copy IFLA_IFNAME into a bounded, NUL-terminated buffer before lookup - Match kernel string length validation and ERANGE errors - Add regression coverage for unterminated names and boundary cases * fix(preload): pin peers for netlink replies * fix(preload): make autobind idempotent * fix(preload): isolate forked netlink cleanup * fix(preload): avoid interposed fd scans * perf(preload): skip PID checks for untracked fds * fix(preload): verify peer before replay --------- Co-authored-by: uubooster-team <uu.booster@service.netease.com>
1239 lines
54 KiB
Python
1239 lines
54 KiB
Python
import errno
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload"
|
|
|
|
SHIM = r"""
|
|
#define _GNU_SOURCE
|
|
#include <dlfcn.h>
|
|
#include <pthread.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/syscall.h>
|
|
#include <unistd.h>
|
|
|
|
#ifndef SYS_close_range
|
|
#define SYS_close_range 436
|
|
#endif
|
|
|
|
static pthread_mutex_t *netlink_mutex;
|
|
static __thread int probe_mode;
|
|
static __thread unsigned probe_counts[5];
|
|
|
|
void test_probe_begin(int capture) {
|
|
memset(probe_counts, 0, sizeof(probe_counts));
|
|
probe_mode = capture ? 1 : 2;
|
|
}
|
|
|
|
void test_probe_end(unsigned *counts) {
|
|
probe_mode = 0;
|
|
memcpy(counts, probe_counts, sizeof(probe_counts));
|
|
}
|
|
|
|
int pthread_mutex_lock(pthread_mutex_t *mutex) {
|
|
static int (*real)(pthread_mutex_t *);
|
|
if (!real) real = dlsym(RTLD_NEXT, "pthread_mutex_lock");
|
|
if (probe_mode == 1) netlink_mutex = mutex;
|
|
if (probe_mode == 2 && mutex == netlink_mutex) probe_counts[0]++;
|
|
return real(mutex);
|
|
}
|
|
|
|
int getsockname(int fd, struct sockaddr *addr, socklen_t *len) {
|
|
static int (*real)(int, struct sockaddr *, socklen_t *);
|
|
if (!real) real = dlsym(RTLD_NEXT, "getsockname");
|
|
if (probe_mode == 2) probe_counts[1]++;
|
|
return real(fd, addr, len);
|
|
}
|
|
|
|
int getpeername(int fd, struct sockaddr *addr, socklen_t *len) {
|
|
static int (*real)(int, struct sockaddr *, socklen_t *);
|
|
if (!real) real = dlsym(RTLD_NEXT, "getpeername");
|
|
if (probe_mode == 2) probe_counts[2]++;
|
|
return real(fd, addr, len);
|
|
}
|
|
|
|
int fstat(int fd, struct stat *status) {
|
|
static int (*real)(int, struct stat *);
|
|
if (!real) real = dlsym(RTLD_NEXT, "fstat");
|
|
if (probe_mode == 2) probe_counts[3]++;
|
|
return real(fd, status);
|
|
}
|
|
|
|
#ifdef __GLIBC__
|
|
int __fxstat(int version, int fd, struct stat *status) {
|
|
static int (*real)(int, int, struct stat *);
|
|
if (!real) real = dlsym(RTLD_NEXT, "__fxstat");
|
|
if (probe_mode == 2) probe_counts[3]++;
|
|
return real(version, fd, status);
|
|
}
|
|
#endif
|
|
|
|
int getsockopt(int fd, int level, int option, void *value, socklen_t *len) {
|
|
static int (*real)(int, int, int, void *, socklen_t *);
|
|
if (!real) real = dlsym(RTLD_NEXT, "getsockopt");
|
|
if (probe_mode == 2) probe_counts[4]++;
|
|
return real(fd, level, option, value, len);
|
|
}
|
|
|
|
FILE *fopen(const char *path, const char *mode) {
|
|
static FILE *(*real)(const char *, const char *);
|
|
if (!real) real = dlsym(RTLD_NEXT, "fopen");
|
|
const char *snapshot = getenv("NETLINK_TEST_SNAPSHOT");
|
|
if (snapshot && !strcmp(path, "/etc/droiddeck-net")) path = snapshot;
|
|
return real(path, mode);
|
|
}
|
|
|
|
int close_range(unsigned int first, unsigned int last, int flags) {
|
|
return syscall(SYS_close_range, (long)first, (long)last, (long)flags, 0L, 0L, 0L);
|
|
}
|
|
|
|
void closefrom(int first) {
|
|
syscall(SYS_close_range, (long)first, (long)~0u, 0L, 0L, 0L, 0L);
|
|
}
|
|
"""
|
|
|
|
PROGRAM = r"""
|
|
#define _GNU_SOURCE
|
|
#include <arpa/inet.h>
|
|
#include <dlfcn.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <linux/netlink.h>
|
|
#include <linux/rtnetlink.h>
|
|
#include <net/if.h>
|
|
#include <pthread.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/resource.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/syscall.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
static void require(int condition, const char *label) {
|
|
if (!condition) {
|
|
fprintf(stderr, "%s: errno=%d\n", label, errno);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
static int route_socket(void) {
|
|
int descriptor = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
|
|
struct sockaddr_nl address = {.nl_family = AF_NETLINK};
|
|
require(descriptor >= 0, "socket");
|
|
require(bind(descriptor, (struct sockaddr *)&address, sizeof(address)) == 0, "bind");
|
|
return descriptor;
|
|
}
|
|
|
|
static unsigned port_of(int descriptor) {
|
|
struct sockaddr_nl address;
|
|
socklen_t length = sizeof(address);
|
|
require(getsockname(descriptor, (struct sockaddr *)&address, &length) == 0, "getsockname");
|
|
require(length == sizeof(address) && address.nl_family == AF_NETLINK, "socket identity");
|
|
return address.nl_pid;
|
|
}
|
|
|
|
static void send_link(int descriptor, int index, unsigned flags) {
|
|
struct {
|
|
struct nlmsghdr header;
|
|
struct ifinfomsg info;
|
|
} query = {
|
|
.header = {.nlmsg_len = sizeof(query), .nlmsg_type = RTM_GETLINK,
|
|
.nlmsg_flags = NLM_F_REQUEST | flags, .nlmsg_seq = 42},
|
|
.info = {.ifi_index = index}
|
|
};
|
|
struct sockaddr_nl destination = {.nl_family = AF_NETLINK};
|
|
require(sendto(descriptor, &query, sizeof(query), 0,
|
|
(struct sockaddr *)&destination, sizeof(destination)) == sizeof(query), "send link");
|
|
}
|
|
|
|
struct autobind_sender {
|
|
int descriptor;
|
|
pthread_barrier_t *barrier;
|
|
};
|
|
|
|
static ssize_t receive(int descriptor, unsigned char *buffer, size_t capacity);
|
|
|
|
static void *send_after_barrier(void *argument) {
|
|
struct autobind_sender *sender = argument;
|
|
int result = pthread_barrier_wait(sender->barrier);
|
|
require(result == 0 || result == PTHREAD_BARRIER_SERIAL_THREAD, "autobind barrier");
|
|
send_link(sender->descriptor, 1, NLM_F_ACK);
|
|
return NULL;
|
|
}
|
|
|
|
static void autobind_race_test(void) {
|
|
for (int round = 0; round < 50; round++) {
|
|
int descriptor = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
|
|
require(descriptor >= 0 && port_of(descriptor) == 0, "unbound netlink socket");
|
|
pthread_barrier_t barrier;
|
|
require(pthread_barrier_init(&barrier, NULL, 2) == 0, "autobind barrier init");
|
|
struct autobind_sender sender = {.descriptor = descriptor, .barrier = &barrier};
|
|
pthread_t first, second;
|
|
require(pthread_create(&first, NULL, send_after_barrier, &sender) == 0, "autobind first thread");
|
|
require(pthread_create(&second, NULL, send_after_barrier, &sender) == 0, "autobind second thread");
|
|
require(pthread_join(first, NULL) == 0 && pthread_join(second, NULL) == 0, "autobind threads");
|
|
require(pthread_barrier_destroy(&barrier) == 0, "autobind barrier destroy");
|
|
require(port_of(descriptor) != 0, "concurrent autobind assigned one port");
|
|
close(descriptor);
|
|
}
|
|
}
|
|
|
|
static void vfork_close_range_test(void) {
|
|
int descriptor = route_socket();
|
|
pid_t child = vfork();
|
|
require(child >= 0, "vfork");
|
|
if (child == 0) {
|
|
_exit(close_range(3, ~0u, 0) == 0 ? 0 : 1);
|
|
}
|
|
int status;
|
|
require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
|
|
"vfork close range child");
|
|
send_link(descriptor, 1, NLM_F_ACK);
|
|
unsigned char buffer[4096];
|
|
ssize_t received = receive(descriptor, buffer, sizeof(buffer));
|
|
require(received > 0 && ((struct nlmsghdr *) buffer)->nlmsg_type == RTM_NEWLINK,
|
|
"netlink bookkeeping survives vfork close range");
|
|
close(descriptor);
|
|
}
|
|
|
|
static void named_link_test(const char *name, const char *mode) {
|
|
int descriptor = route_socket();
|
|
struct {
|
|
struct nlmsghdr header;
|
|
struct ifinfomsg info;
|
|
struct rtattr attr;
|
|
char name[IFNAMSIZ + 1];
|
|
} query = {
|
|
.header = {.nlmsg_type = RTM_GETLINK, .nlmsg_flags = NLM_F_REQUEST, .nlmsg_seq = 42},
|
|
.attr = {.rta_type = IFLA_IFNAME}
|
|
};
|
|
size_t length = strlen(name) + 1;
|
|
require(length <= sizeof(query.name), "name length");
|
|
memcpy(query.name, name, length);
|
|
if (!strcmp(mode, "unterminated")) length--;
|
|
if (!strcmp(mode, "oversized")) {
|
|
memset(query.name, 'x', sizeof(query.name));
|
|
query.name[sizeof(query.name) - 1] = '\0';
|
|
length = sizeof(query.name);
|
|
}
|
|
query.attr.rta_len = RTA_LENGTH(length);
|
|
query.header.nlmsg_len = NLMSG_LENGTH(sizeof(query.info)) + RTA_SPACE(length);
|
|
if (!strcmp(mode, "missing")) query.header.nlmsg_len = NLMSG_LENGTH(sizeof(query.info));
|
|
if (!strcmp(mode, "index")) query.info.ifi_index = 1;
|
|
struct sockaddr_nl destination = {.nl_family = AF_NETLINK};
|
|
require(sendto(descriptor, &query, query.header.nlmsg_len, 0,
|
|
(struct sockaddr *)&destination, sizeof(destination)) == query.header.nlmsg_len,
|
|
"send named link");
|
|
unsigned char buffer[4096];
|
|
require(recv(descriptor, buffer, sizeof(buffer), MSG_DONTWAIT) > 0, "receive named link");
|
|
struct nlmsghdr *header = (struct nlmsghdr *)buffer;
|
|
require(header->nlmsg_seq == 42, "named link sequence");
|
|
if (header->nlmsg_type == NLMSG_ERROR) {
|
|
printf("error %d\n", ((struct nlmsgerr *)NLMSG_DATA(header))->error);
|
|
} else {
|
|
require(header->nlmsg_type == RTM_NEWLINK, "named link reply");
|
|
struct ifinfomsg *info = NLMSG_DATA(header);
|
|
int remaining = IFLA_PAYLOAD(header);
|
|
const char *reply_name = NULL;
|
|
for (struct rtattr *attr = IFLA_RTA(info); RTA_OK(attr, remaining);
|
|
attr = RTA_NEXT(attr, remaining)) {
|
|
if (attr->rta_type == IFLA_IFNAME) reply_name = RTA_DATA(attr);
|
|
}
|
|
require(reply_name != NULL, "reply interface name");
|
|
printf("link %d %s\n", info->ifi_index, reply_name);
|
|
}
|
|
close(descriptor);
|
|
}
|
|
|
|
static ssize_t receive(int descriptor, unsigned char *buffer, size_t capacity) {
|
|
struct sockaddr_nl address;
|
|
memset(&address, 0xff, sizeof(address));
|
|
socklen_t length = sizeof(address);
|
|
ssize_t received = recvfrom(descriptor, buffer, capacity, MSG_DONTWAIT,
|
|
(struct sockaddr *)&address, &length);
|
|
require(received > 0, "recvfrom");
|
|
require(length == sizeof(address) && address.nl_family == AF_NETLINK && address.nl_pid == 0,
|
|
"kernel source");
|
|
return received;
|
|
}
|
|
|
|
static void usable(int descriptor) {
|
|
send_link(descriptor, 1, 0);
|
|
unsigned port = port_of(descriptor);
|
|
unsigned char buffer[4096];
|
|
receive(descriptor, buffer, sizeof(buffer));
|
|
struct nlmsghdr *header = (struct nlmsghdr *)buffer;
|
|
require(header->nlmsg_type == RTM_NEWLINK && header->nlmsg_pid == port, "usable socket");
|
|
}
|
|
|
|
static void socket_info(int descriptor, int kind) {
|
|
int value;
|
|
socklen_t length = sizeof(value);
|
|
require(getsockopt(descriptor, SOL_SOCKET, SO_TYPE, &value, &length) == 0
|
|
&& length == sizeof(value) && value == kind, "reported socket type");
|
|
length = sizeof(value);
|
|
require(getsockopt(descriptor, SOL_SOCKET, SO_DOMAIN, &value, &length) == 0
|
|
&& value == AF_NETLINK, "reported socket domain");
|
|
length = sizeof(value);
|
|
require(getsockopt(descriptor, SOL_SOCKET, SO_PROTOCOL, &value, &length) == 0
|
|
&& value == NETLINK_ROUTE, "reported socket protocol");
|
|
length = 1;
|
|
require(getsockopt(descriptor, SOL_SOCKET, SO_TYPE, &value, &length) == 0
|
|
&& length == 1, "short socket option");
|
|
require(getsockopt(descriptor, SOL_SOCKET, SO_TYPE, NULL, &length) == -1
|
|
&& errno == EFAULT, "invalid socket option buffer");
|
|
}
|
|
|
|
static void (*probe_begin)(int);
|
|
static void (*probe_end)(unsigned *);
|
|
|
|
static void *ordinary_traffic(void *argument) {
|
|
int kind = (int)(uintptr_t)argument;
|
|
unsigned counts[5];
|
|
probe_begin(0);
|
|
int listener = socket(AF_INET, kind, 0);
|
|
int client = socket(AF_INET, kind, 0);
|
|
require(listener >= 0 && client >= 0, "ordinary sockets");
|
|
struct sockaddr_in address = {.sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK)};
|
|
require(bind(listener, (struct sockaddr *)&address, sizeof(address)) == 0, "ordinary bind");
|
|
socklen_t length = sizeof(address);
|
|
require(getsockname(listener, (struct sockaddr *)&address, &length) == 0, "ordinary address");
|
|
if (kind == SOCK_STREAM) require(listen(listener, 1) == 0, "ordinary listen");
|
|
require(connect(client, (struct sockaddr *)&address, sizeof(address)) == 0, "ordinary connect");
|
|
int server = kind == SOCK_STREAM ? accept(listener, NULL, NULL) : listener;
|
|
require(server >= 0, "ordinary server");
|
|
probe_end(counts);
|
|
require(counts[0] == 0, "ordinary setup avoids netlink lock");
|
|
|
|
probe_begin(0);
|
|
const int rounds = 32;
|
|
for (int round = 0; round < rounds; round++) {
|
|
char buffer;
|
|
require(send(client, "x", 1, 0) == 1, "ordinary send");
|
|
require(recv(server, &buffer, 1, 0) == 1 && buffer == 'x', "ordinary recv");
|
|
struct sockaddr *destination = kind == SOCK_DGRAM ? (struct sockaddr *)&address : NULL;
|
|
socklen_t destination_length = destination ? sizeof(address) : 0;
|
|
require(sendto(client, "y", 1, 0, destination, destination_length) == 1, "ordinary sendto");
|
|
struct sockaddr_storage source;
|
|
length = sizeof(source);
|
|
require(recvfrom(server, &buffer, 1, 0, (struct sockaddr *)&source, &length) == 1
|
|
&& buffer == 'y', "ordinary recvfrom");
|
|
struct iovec part = {.iov_base = "z", .iov_len = 1};
|
|
struct msghdr message = {.msg_name = destination, .msg_namelen = destination_length,
|
|
.msg_iov = &part, .msg_iovlen = 1};
|
|
require(sendmsg(client, &message, 0) == 1, "ordinary sendmsg");
|
|
part.iov_base = &buffer;
|
|
message.msg_name = &source;
|
|
message.msg_namelen = sizeof(source);
|
|
require(recvmsg(server, &message, 0) == 1 && buffer == 'z', "ordinary recvmsg");
|
|
length = sizeof(source);
|
|
require(getpeername(client, (struct sockaddr *)&source, &length) == 0, "ordinary peer");
|
|
const int options[] = {SO_TYPE, SO_DOMAIN, SO_PROTOCOL};
|
|
const int expected[] = {kind, AF_INET, kind == SOCK_DGRAM ? IPPROTO_UDP : IPPROTO_TCP};
|
|
for (size_t index = 0; index < sizeof(options) / sizeof(options[0]); index++) {
|
|
int value;
|
|
length = sizeof(value);
|
|
require(getsockopt(client, SOL_SOCKET, options[index], &value, &length) == 0
|
|
&& value == expected[index], "ordinary socket option");
|
|
}
|
|
int enabled = 1;
|
|
require(setsockopt(client, SOL_SOCKET, SO_KEEPALIVE, &enabled, sizeof(enabled)) == 0,
|
|
"ordinary setsockopt");
|
|
int alias = dup(client);
|
|
require(alias >= 0 && dup2(client, alias) == alias
|
|
&& dup3(client, alias, O_CLOEXEC) == alias, "ordinary duplicates");
|
|
close(alias);
|
|
alias = fcntl(client, F_DUPFD, 0);
|
|
require(alias >= 0, "ordinary fcntl duplicate");
|
|
close(alias);
|
|
alias = fcntl(client, F_DUPFD_CLOEXEC, 0);
|
|
require(alias >= 0, "ordinary cloexec duplicate");
|
|
close(alias);
|
|
}
|
|
probe_end(counts);
|
|
require(counts[0] == 0, "ordinary traffic avoids netlink lock");
|
|
require(counts[1] == 0, "ordinary traffic avoids restoration getsockname");
|
|
require(counts[2] == (unsigned)rounds, "ordinary traffic has only requested getpeername");
|
|
require(counts[3] == 0, "ordinary traffic avoids restoration fstat");
|
|
require(counts[4] == (unsigned)rounds * 3, "ordinary socket options use one syscall each");
|
|
close(client);
|
|
close(server);
|
|
if (server != listener) close(listener);
|
|
return NULL;
|
|
}
|
|
|
|
static void hotpath_test(int active) {
|
|
probe_begin = dlsym(RTLD_DEFAULT, "test_probe_begin");
|
|
probe_end = dlsym(RTLD_DEFAULT, "test_probe_end");
|
|
require(probe_begin && probe_end, "probe symbols");
|
|
unsigned counts[5];
|
|
probe_begin(1);
|
|
int descriptor = route_socket();
|
|
probe_end(counts);
|
|
if (!active) close(descriptor);
|
|
pthread_t threads[4];
|
|
for (size_t index = 0; index < sizeof(threads) / sizeof(threads[0]); index++) {
|
|
int kind = index % 2 ? SOCK_STREAM : SOCK_DGRAM;
|
|
require(pthread_create(&threads[index], NULL, ordinary_traffic, (void *)(uintptr_t)kind) == 0,
|
|
"traffic thread");
|
|
}
|
|
for (size_t index = 0; index < sizeof(threads) / sizeof(threads[0]); index++)
|
|
require(pthread_join(threads[index], NULL) == 0, "join traffic thread");
|
|
if (active) {
|
|
usable(descriptor);
|
|
close(descriptor);
|
|
}
|
|
}
|
|
|
|
static void transferred_test(const char *mode) {
|
|
int transport[2];
|
|
require(socketpair(AF_UNIX, SOCK_DGRAM, 0, transport) == 0, "rights transport");
|
|
pid_t child = fork();
|
|
require(child >= 0, "rights fork");
|
|
if (child == 0) {
|
|
close(transport[0]);
|
|
unsigned port;
|
|
struct iovec part = {.iov_base = &port, .iov_len = sizeof(port)};
|
|
union {
|
|
struct cmsghdr align;
|
|
unsigned char bytes[CMSG_SPACE(sizeof(int))];
|
|
} control;
|
|
struct msghdr message = {.msg_iov = &part, .msg_iovlen = 1,
|
|
.msg_control = control.bytes, .msg_controllen = sizeof(control.bytes)};
|
|
ssize_t received = !strcmp(mode, "received") ? recvmsg(transport[1], &message, 0) :
|
|
syscall(SYS_recvmsg, (long)transport[1], &message, 0L, 0L, 0L, 0L);
|
|
require(received == sizeof(port), "rights receive");
|
|
struct cmsghdr *header = CMSG_FIRSTHDR(&message);
|
|
require(header && header->cmsg_level == SOL_SOCKET && header->cmsg_type == SCM_RIGHTS
|
|
&& header->cmsg_len == CMSG_LEN(sizeof(int)), "rights header");
|
|
int descriptor;
|
|
memcpy(&descriptor, CMSG_DATA(header), sizeof(descriptor));
|
|
struct sockaddr_nl address = {.nl_family = AF_NETLINK};
|
|
if (!strcmp(mode, "domain")) {
|
|
int domain;
|
|
socklen_t length = sizeof(domain);
|
|
require(getsockopt(descriptor, SOL_SOCKET, SO_DOMAIN, &domain, &length) == 0
|
|
&& domain == AF_NETLINK, "rights domain discovery");
|
|
} else if (!strcmp(mode, "bind")) {
|
|
address.nl_pid = port;
|
|
require(bind(descriptor, (struct sockaddr *)&address, sizeof(address)) == 0,
|
|
"rights bind discovery");
|
|
} else if (!strcmp(mode, "connect")) {
|
|
require(connect(descriptor, (struct sockaddr *)&address, sizeof(address)) == 0,
|
|
"rights connect discovery");
|
|
} else if (!strcmp(mode, "option")) {
|
|
int enabled = 1;
|
|
require(setsockopt(descriptor, SOL_NETLINK, NETLINK_EXT_ACK, &enabled, sizeof(enabled)) == 0,
|
|
"rights option discovery");
|
|
}
|
|
usable(descriptor);
|
|
require(port_of(descriptor) == port, "rights port retained");
|
|
socket_info(descriptor, SOCK_RAW);
|
|
close(descriptor);
|
|
close(transport[1]);
|
|
_exit(0);
|
|
}
|
|
close(transport[1]);
|
|
int descriptor = route_socket();
|
|
unsigned port = port_of(descriptor);
|
|
struct iovec part = {.iov_base = &port, .iov_len = sizeof(port)};
|
|
union {
|
|
struct cmsghdr align;
|
|
unsigned char bytes[CMSG_SPACE(sizeof(int))];
|
|
} control = {0};
|
|
struct msghdr message = {.msg_iov = &part, .msg_iovlen = 1,
|
|
.msg_control = control.bytes, .msg_controllen = sizeof(control.bytes)};
|
|
struct cmsghdr *header = CMSG_FIRSTHDR(&message);
|
|
header->cmsg_level = SOL_SOCKET;
|
|
header->cmsg_type = SCM_RIGHTS;
|
|
header->cmsg_len = CMSG_LEN(sizeof(descriptor));
|
|
memcpy(CMSG_DATA(header), &descriptor, sizeof(descriptor));
|
|
require(sendmsg(transport[0], &message, 0) == sizeof(port), "rights send");
|
|
int status;
|
|
require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
|
|
"rights child");
|
|
usable(descriptor);
|
|
close(descriptor);
|
|
close(transport[0]);
|
|
}
|
|
|
|
static void socket_types_test(void) {
|
|
const int kinds[] = {SOCK_RAW, SOCK_DGRAM};
|
|
for (size_t index = 0; index < sizeof(kinds) / sizeof(kinds[0]); index++) {
|
|
int descriptor = socket(AF_NETLINK, kinds[index], NETLINK_ROUTE);
|
|
require(descriptor >= 0, "supported socket type");
|
|
require(port_of(descriptor) == 0, "unbound port");
|
|
require((fcntl(descriptor, F_GETFD) & FD_CLOEXEC) == 0, "caller cloexec flag");
|
|
socket_info(descriptor, kinds[index]);
|
|
usable(descriptor);
|
|
close(descriptor);
|
|
descriptor = socket(AF_NETLINK, kinds[index] | SOCK_CLOEXEC | SOCK_NONBLOCK, NETLINK_ROUTE);
|
|
require(descriptor >= 0 && (fcntl(descriptor, F_GETFD) & FD_CLOEXEC)
|
|
&& (fcntl(descriptor, F_GETFL) & O_NONBLOCK), "requested socket flags");
|
|
socket_info(descriptor, kinds[index]);
|
|
close(descriptor);
|
|
}
|
|
require(socket(AF_NETLINK, SOCK_STREAM, NETLINK_ROUTE) == -1
|
|
&& errno == ESOCKTNOSUPPORT, "stream rejected");
|
|
require(socket(AF_NETLINK, SOCK_SEQPACKET, NETLINK_ROUTE) == -1
|
|
&& errno == ESOCKTNOSUPPORT, "seqpacket rejected");
|
|
require(socket(AF_NETLINK, SOCK_RAW | 0x40000000, NETLINK_ROUTE) == -1
|
|
&& errno == EINVAL, "unknown socket flag rejected");
|
|
require(socket(AF_NETLINK, 15, NETLINK_ROUTE) == -1 && errno == EINVAL, "invalid socket type");
|
|
}
|
|
|
|
static void ports_test(int forked) {
|
|
int descriptors[32];
|
|
unsigned ports[32];
|
|
for (int index = 0; index < 32; index++) {
|
|
descriptors[index] = route_socket();
|
|
ports[index] = port_of(descriptors[index]);
|
|
require(ports[index] != 0, "bound port");
|
|
for (int previous = 0; previous < index; previous++)
|
|
require(ports[index] != ports[previous], "unique automatic port");
|
|
}
|
|
int duplicate = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);
|
|
require(duplicate >= 0, "explicit bind socket");
|
|
struct sockaddr_nl address = {.nl_family = AF_NETLINK, .nl_pid = ports[0]};
|
|
require(bind(duplicate, (struct sockaddr *)&address, sizeof(address)) == -1
|
|
&& errno == EADDRINUSE && port_of(duplicate) == 0, "duplicate explicit port");
|
|
if (forked) {
|
|
pid_t child = fork();
|
|
require(child >= 0, "port fork");
|
|
if (child == 0) {
|
|
for (int index = 0; index < 32; index++) close(descriptors[index]);
|
|
require(bind(duplicate, (struct sockaddr *)&address, sizeof(address)) == -1
|
|
&& errno == EADDRINUSE, "cross process duplicate port");
|
|
close(duplicate);
|
|
_exit(0);
|
|
}
|
|
int status;
|
|
require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
|
|
"port child");
|
|
}
|
|
int alias = dup(descriptors[0]);
|
|
close(descriptors[0]);
|
|
require(bind(duplicate, (struct sockaddr *)&address, sizeof(address)) == -1
|
|
&& errno == EADDRINUSE, "alias keeps port reserved");
|
|
close(alias);
|
|
require(bind(duplicate, (struct sockaddr *)&address, sizeof(address)) == 0
|
|
&& port_of(duplicate) == ports[0], "closed port reused");
|
|
require(bind(duplicate, (struct sockaddr *)&address, sizeof(address)) == 0, "same port rebind");
|
|
address.nl_pid = 0;
|
|
require(bind(duplicate, (struct sockaddr *)&address, sizeof(address)) == -1 && errno == EINVAL,
|
|
"explicit zero port rebind denied");
|
|
address.nl_pid = ports[0] + 1;
|
|
require(bind(duplicate, (struct sockaddr *)&address, sizeof(address)) == -1
|
|
&& errno == EINVAL && port_of(duplicate) == ports[0], "different port rebind denied");
|
|
usable(duplicate);
|
|
close(duplicate);
|
|
for (int index = 1; index < 32; index++) {
|
|
usable(descriptors[index]);
|
|
close(descriptors[index]);
|
|
}
|
|
}
|
|
|
|
static void batch_test(int oversized) {
|
|
int descriptor = route_socket();
|
|
struct query {
|
|
struct nlmsghdr header;
|
|
struct ifinfomsg info;
|
|
} queries[128];
|
|
int count = oversized ? 128 : 40;
|
|
for (int index = 0; index < count; index++) {
|
|
memset(&queries[index], 0, sizeof(queries[index]));
|
|
queries[index].header.nlmsg_len = sizeof(queries[index]);
|
|
queries[index].header.nlmsg_type = RTM_GETLINK;
|
|
queries[index].header.nlmsg_flags = NLM_F_REQUEST;
|
|
queries[index].header.nlmsg_seq = index + 1;
|
|
queries[index].info.ifi_index = 1;
|
|
}
|
|
struct iovec parts[2] = {
|
|
{.iov_base = queries, .iov_len = 256},
|
|
{.iov_base = (unsigned char *)queries + 256, .iov_len = count * sizeof(queries[0]) - 256}
|
|
};
|
|
struct sockaddr_nl address = {.nl_family = AF_NETLINK};
|
|
struct msghdr message = {.msg_name = &address, .msg_namelen = sizeof(address),
|
|
.msg_iov = parts, .msg_iovlen = 2};
|
|
ssize_t sent = sendmsg(descriptor, &message, 0);
|
|
unsigned char buffer[8192];
|
|
if (oversized) {
|
|
require(sent == -1 && errno == EMSGSIZE, "oversized reply rejected");
|
|
require(recv(descriptor, buffer, sizeof(buffer), MSG_DONTWAIT) == -1 && errno == EAGAIN,
|
|
"no partial batch reply");
|
|
} else {
|
|
require(sent == count * sizeof(queries[0]), "large scatter gather request");
|
|
int remaining = (int)receive(descriptor, buffer, sizeof(buffer));
|
|
int replies = 0;
|
|
for (struct nlmsghdr *header = (struct nlmsghdr *)buffer;
|
|
NLMSG_OK(header, remaining); header = NLMSG_NEXT(header, remaining)) {
|
|
require(header->nlmsg_type == RTM_NEWLINK && header->nlmsg_seq == (unsigned)replies + 1,
|
|
"batch reply sequence");
|
|
replies++;
|
|
}
|
|
require(replies == count, "complete batch");
|
|
}
|
|
parts[0].iov_base = NULL;
|
|
require(sendmsg(descriptor, &message, 0) == -1 && errno == EFAULT, "invalid iov base");
|
|
parts[0].iov_base = queries;
|
|
parts[0].iov_len = SIZE_MAX;
|
|
require(sendmsg(descriptor, &message, 0) == -1 && errno == EMSGSIZE, "iov length overflow");
|
|
require(sendmsg(descriptor, NULL, 0) == -1 && errno == EFAULT, "invalid message");
|
|
usable(descriptor);
|
|
close(descriptor);
|
|
}
|
|
|
|
static void inherited_test(int argc, char **argv) {
|
|
require(argc == 6, "inherited arguments");
|
|
int descriptor = atoi(argv[2]), closed = atoi(argv[4]), datagram = atoi(argv[5]);
|
|
unsigned port = (unsigned)strtoul(argv[3], NULL, 10);
|
|
require(fcntl(closed, F_GETFD) == -1 && errno == EBADF, "cloexec descriptor closed");
|
|
require(port_of(descriptor) == port && !(fcntl(descriptor, F_GETFD) & FD_CLOEXEC),
|
|
"inherited bound port");
|
|
socket_info(descriptor, SOCK_RAW);
|
|
socket_info(datagram, SOCK_DGRAM);
|
|
require(fcntl(datagram, F_GETFL) & O_NONBLOCK, "inherited nonblocking flag");
|
|
unsigned char buffer[4096];
|
|
receive(descriptor, buffer, sizeof(buffer));
|
|
require(((struct nlmsghdr *)buffer)->nlmsg_type == RTM_NEWLINK, "queued reply survives exec");
|
|
send_link(descriptor, 9999, NLM_F_ACK);
|
|
receive(descriptor, buffer, sizeof(buffer));
|
|
require(((struct nlmsghdr *)buffer)->nlmsg_type == NLMSG_ERROR
|
|
&& (((struct nlmsghdr *)buffer)->nlmsg_flags & NLM_F_CAPPED), "cap ack survives exec");
|
|
usable(descriptor);
|
|
usable(datagram);
|
|
int fresh = route_socket();
|
|
require(port_of(fresh) != port && port_of(fresh) != port_of(datagram), "ports after exec");
|
|
close(fresh);
|
|
close(descriptor);
|
|
close(datagram);
|
|
}
|
|
|
|
static void exec_test(const char *program, int forked) {
|
|
int descriptor = route_socket();
|
|
int enabled = 1;
|
|
require(setsockopt(descriptor, SOL_NETLINK, NETLINK_CAP_ACK, &enabled, sizeof(enabled)) == 0,
|
|
"inherited cap ack");
|
|
unsigned port = port_of(descriptor);
|
|
int alias = fcntl(descriptor, F_DUPFD, 100);
|
|
int closed = fcntl(descriptor, F_DUPFD_CLOEXEC, 200);
|
|
require(alias >= 100 && closed >= 200, "exec aliases");
|
|
close(descriptor);
|
|
int datagram = socket(AF_NETLINK, SOCK_DGRAM | SOCK_NONBLOCK, NETLINK_ROUTE);
|
|
require(datagram >= 0, "exec datagram");
|
|
send_link(alias, 1, 0);
|
|
char descriptor_arg[32], port_arg[32], closed_arg[32], datagram_arg[32];
|
|
snprintf(descriptor_arg, sizeof(descriptor_arg), "%d", alias);
|
|
snprintf(port_arg, sizeof(port_arg), "%u", port);
|
|
snprintf(closed_arg, sizeof(closed_arg), "%d", closed);
|
|
snprintf(datagram_arg, sizeof(datagram_arg), "%d", datagram);
|
|
char *arguments[] = {(char *)program, "inherited", descriptor_arg, port_arg, closed_arg, datagram_arg, NULL};
|
|
if (forked) {
|
|
pid_t child = fork();
|
|
require(child >= 0, "exec fork");
|
|
if (child > 0) {
|
|
int status;
|
|
require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
|
|
"exec child");
|
|
usable(alias);
|
|
usable(datagram);
|
|
close(alias);
|
|
close(closed);
|
|
close(datagram);
|
|
return;
|
|
}
|
|
}
|
|
execv(program, arguments);
|
|
require(0, "exec");
|
|
}
|
|
|
|
static void cloexec_test(const char *program) {
|
|
int descriptor = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
|
|
require(descriptor >= 0, "cloexec socket");
|
|
struct sockaddr_nl address = {.nl_family = AF_NETLINK, .nl_pid = (unsigned)getpid()};
|
|
require(bind(descriptor, (struct sockaddr *)&address, sizeof(address)) == 0, "cloexec bind");
|
|
char port_arg[32];
|
|
snprintf(port_arg, sizeof(port_arg), "%u", address.nl_pid);
|
|
char *arguments[] = {(char *)program, "rebind_after_exec", port_arg, NULL};
|
|
execv(program, arguments);
|
|
require(0, "cloexec exec");
|
|
}
|
|
|
|
static void ack_test(const char *mode) {
|
|
int descriptor = route_socket();
|
|
int capped = !strcmp(mode, "capped");
|
|
if (capped) {
|
|
int enabled = 1;
|
|
require(setsockopt(descriptor, SOL_NETLINK, NETLINK_CAP_ACK, &enabled, sizeof(enabled)) == 0,
|
|
"cap ack");
|
|
}
|
|
int rejected = !strcmp(mode, "error") || capped;
|
|
send_link(descriptor, rejected ? 9999 : 1, NLM_F_ACK);
|
|
unsigned char buffer[4096];
|
|
int remaining = (int)receive(descriptor, buffer, sizeof(buffer));
|
|
int acknowledgements = 0, links = 0;
|
|
for (struct nlmsghdr *header = (struct nlmsghdr *)buffer;
|
|
NLMSG_OK(header, remaining); header = NLMSG_NEXT(header, remaining)) {
|
|
require(header->nlmsg_seq == 42 && header->nlmsg_pid == port_of(descriptor), "ack identity");
|
|
if (header->nlmsg_type == RTM_NEWLINK) links++;
|
|
if (header->nlmsg_type != NLMSG_ERROR) continue;
|
|
struct nlmsgerr *reply = NLMSG_DATA(header);
|
|
require(reply->error == (rejected ? -ENODEV : 0), "ack error");
|
|
require(reply->msg.nlmsg_type == RTM_GETLINK && reply->msg.nlmsg_seq == 42, "ack request");
|
|
require(!!(header->nlmsg_flags & NLM_F_CAPPED) == (!rejected || capped), "ack capped flag");
|
|
size_t extra = rejected && !capped ? sizeof(struct ifinfomsg) : 0;
|
|
require(header->nlmsg_len == NLMSG_LENGTH(sizeof(*reply) + extra), "ack payload length");
|
|
acknowledgements++;
|
|
}
|
|
require(acknowledgements == 1 && links == !rejected, "one acknowledgement");
|
|
close(descriptor);
|
|
}
|
|
|
|
static void options_test(void) {
|
|
int descriptor = route_socket();
|
|
int group = RTNLGRP_LINK;
|
|
require(setsockopt(descriptor, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP, &group, sizeof(group)) == -1
|
|
&& errno == EACCES, "membership denied");
|
|
require(setsockopt(descriptor, SOL_NETLINK, NETLINK_PKTINFO, &group, sizeof(group)) == -1
|
|
&& errno == ENOPROTOOPT, "unsupported option");
|
|
require(setsockopt(descriptor, SOL_NETLINK, NETLINK_EXT_ACK, &group, sizeof(group)) == 0, "ext ack");
|
|
struct timeval timeout = {.tv_usec = 30000};
|
|
require(setsockopt(descriptor, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) == 0, "receive timeout");
|
|
memset(&timeout, 0, sizeof(timeout));
|
|
socklen_t length = sizeof(timeout);
|
|
require(getsockopt(descriptor, SOL_SOCKET, SO_RCVTIMEO, &timeout, &length) == 0
|
|
&& timeout.tv_usec > 0, "timeout retained");
|
|
unsigned char buffer[4096];
|
|
require(recv(descriptor, buffer, sizeof(buffer), 0) == -1
|
|
&& (errno == EAGAIN || errno == EWOULDBLOCK), "receive times out");
|
|
close(descriptor);
|
|
}
|
|
|
|
static void dump_test(void) {
|
|
int descriptor = route_socket();
|
|
send_link(descriptor, 0, NLM_F_DUMP | NLM_F_ACK);
|
|
unsigned char buffer[4096];
|
|
int remaining = (int)receive(descriptor, buffer, sizeof(buffer));
|
|
int links = 0, done = 0;
|
|
for (struct nlmsghdr *header = (struct nlmsghdr *)buffer;
|
|
NLMSG_OK(header, remaining); header = NLMSG_NEXT(header, remaining)) {
|
|
require(header->nlmsg_seq == 42 && (header->nlmsg_flags & NLM_F_MULTI), "dump header");
|
|
if (header->nlmsg_type == RTM_NEWLINK) links++;
|
|
else {
|
|
require(header->nlmsg_type == NLMSG_DONE && *(int *)NLMSG_DATA(header) == 0,
|
|
"dump completion");
|
|
done++;
|
|
}
|
|
}
|
|
require(links == 2 && done == 1, "dump results");
|
|
close(descriptor);
|
|
}
|
|
|
|
static void send_timeout_test(int nonblocking) {
|
|
int descriptor = route_socket();
|
|
if (nonblocking) {
|
|
require(fcntl(descriptor, F_SETFL, O_NONBLOCK) == 0, "nonblocking");
|
|
} else {
|
|
struct timeval timeout = {.tv_usec = 30000};
|
|
require(setsockopt(descriptor, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) == 0,
|
|
"send timeout");
|
|
}
|
|
struct {
|
|
struct nlmsghdr header;
|
|
struct ifinfomsg info;
|
|
} query = {
|
|
.header = {.nlmsg_len = sizeof(query), .nlmsg_type = RTM_GETLINK,
|
|
.nlmsg_flags = NLM_F_REQUEST, .nlmsg_seq = 42},
|
|
.info = {.ifi_index = 1}
|
|
};
|
|
for (int count = 0; count < 10000; count++) {
|
|
ssize_t result = send(descriptor, &query, sizeof(query), 0);
|
|
if (result == -1) {
|
|
require(errno == EAGAIN || errno == EWOULDBLOCK, "full queue would block");
|
|
close(descriptor);
|
|
return;
|
|
}
|
|
require(result == sizeof(query), "queued send");
|
|
}
|
|
require(0, "queue filled");
|
|
}
|
|
|
|
static void connect_test(void) {
|
|
int descriptor = route_socket();
|
|
struct sockaddr_nl address = {.nl_family = AF_NETLINK};
|
|
socklen_t length = sizeof(address);
|
|
require(getpeername(descriptor, (struct sockaddr *)&address, &length) == 0
|
|
&& address.nl_family == AF_NETLINK && address.nl_pid == 0, "default kernel peer");
|
|
require(connect(descriptor, (struct sockaddr *)&address, sizeof(address)) == 0, "connect kernel");
|
|
memset(&address, 0xff, sizeof(address));
|
|
length = sizeof(address);
|
|
require(getpeername(descriptor, (struct sockaddr *)&address, &length) == 0
|
|
&& length == sizeof(address) && address.nl_family == AF_NETLINK
|
|
&& address.nl_pid == 0 && address.nl_groups == 0, "kernel peer");
|
|
usable(descriptor);
|
|
address.nl_pid = 1234;
|
|
require(connect(descriptor, (struct sockaddr *)&address, sizeof(address)) == -1
|
|
&& errno == ECONNREFUSED, "userspace peer refused");
|
|
address.nl_pid = 0;
|
|
address.nl_groups = 1;
|
|
require(connect(descriptor, (struct sockaddr *)&address, sizeof(address)) == -1
|
|
&& errno == EACCES, "multicast peer denied");
|
|
struct sockaddr disconnect = {.sa_family = AF_UNSPEC};
|
|
require(connect(descriptor, &disconnect, sizeof(disconnect)) == 0, "disconnect");
|
|
length = sizeof(address);
|
|
require(getpeername(descriptor, (struct sockaddr *)&address, &length) == 0
|
|
&& address.nl_family == AF_NETLINK && address.nl_pid == 0, "disconnected default peer");
|
|
close(descriptor);
|
|
}
|
|
|
|
static void receive_test(void) {
|
|
int descriptor = route_socket();
|
|
send_link(descriptor, 1, 0);
|
|
unsigned char buffer[4096];
|
|
struct sockaddr_nl address;
|
|
memset(&address, 0xff, sizeof(address));
|
|
socklen_t length = sizeof(sa_family_t);
|
|
require(recvfrom(descriptor, buffer, sizeof(buffer), MSG_PEEK,
|
|
(struct sockaddr *)&address, &length) > 0
|
|
&& length == sizeof(address) && address.nl_family == AF_NETLINK
|
|
&& address.nl_pid == (unsigned)-1, "truncated source");
|
|
struct iovec part = {.iov_base = buffer, .iov_len = sizeof(buffer)};
|
|
struct msghdr message = {.msg_name = &address, .msg_namelen = sizeof(address),
|
|
.msg_iov = &part, .msg_iovlen = 1};
|
|
require(recvmsg(descriptor, &message, 0) > 0 && message.msg_namelen == sizeof(address)
|
|
&& address.nl_family == AF_NETLINK && address.nl_pid == 0, "recvmsg source");
|
|
memset(&address, 0xff, sizeof(address));
|
|
length = sizeof(address);
|
|
require(recvfrom(descriptor, buffer, sizeof(buffer), MSG_DONTWAIT,
|
|
(struct sockaddr *)&address, &length) == -1 && errno == EAGAIN
|
|
&& address.nl_family == (unsigned short)-1, "failed receive preserves source");
|
|
close(descriptor);
|
|
}
|
|
|
|
static void duplicate_test(const char *mode) {
|
|
int descriptor = route_socket();
|
|
unsigned port = port_of(descriptor);
|
|
int duplicate;
|
|
if (!strcmp(mode, "dup")) duplicate = dup(descriptor);
|
|
else if (!strcmp(mode, "high_dup")) {
|
|
struct rlimit limit;
|
|
require(getrlimit(RLIMIT_NOFILE, &limit) == 0, "descriptor limit");
|
|
int minimum = limit.rlim_cur > 262144 ? 262144 : (int)(limit.rlim_cur / 2);
|
|
duplicate = fcntl(descriptor, F_DUPFD, minimum);
|
|
require(duplicate >= minimum, "high descriptor duplicate");
|
|
}
|
|
else if (!strcmp(mode, "fcntl")) duplicate = fcntl(descriptor, F_DUPFD, 20);
|
|
else if (!strcmp(mode, "fcntl_cloexec")) duplicate = fcntl(descriptor, F_DUPFD_CLOEXEC, 20);
|
|
else {
|
|
int target = open("/dev/null", O_RDONLY);
|
|
require(target >= 0, "dup target");
|
|
duplicate = !strcmp(mode, "dup2") ? dup2(descriptor, target) :
|
|
dup3(descriptor, target, O_CLOEXEC);
|
|
}
|
|
require(duplicate >= 0 && port_of(duplicate) == port, "duplicate identity");
|
|
require(dup2(descriptor, descriptor) == descriptor, "dup2 same fd");
|
|
require(dup2(-1, descriptor) == -1 && errno == EBADF, "failed replacement");
|
|
usable(descriptor);
|
|
close(descriptor);
|
|
usable(duplicate);
|
|
close(duplicate);
|
|
for (int pass = 0; pass < 16; pass++) {
|
|
descriptor = route_socket();
|
|
usable(descriptor);
|
|
close(descriptor);
|
|
}
|
|
}
|
|
|
|
static void replacement_test(int raw) {
|
|
int descriptor = route_socket();
|
|
int pair[2];
|
|
require(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair) == 0, "unix pair");
|
|
int result = raw ? (int)syscall(SYS_dup3, (long)pair[0], (long)descriptor, 0L, 0L, 0L, 0L) :
|
|
dup2(pair[0], descriptor);
|
|
require(result == descriptor, "replace descriptor");
|
|
struct sockaddr_storage address;
|
|
socklen_t length = sizeof(address);
|
|
require(getsockname(descriptor, (struct sockaddr *)&address, &length) == 0
|
|
&& address.ss_family == AF_UNIX, "replacement identity");
|
|
const char payload[] = "unix payload";
|
|
require(send(descriptor, payload, sizeof(payload), MSG_DONTWAIT) == sizeof(payload), "unix send");
|
|
char buffer[64];
|
|
require(recv(pair[1], buffer, sizeof(buffer), MSG_DONTWAIT) == sizeof(payload)
|
|
&& !memcmp(buffer, payload, sizeof(payload)), "unix receive");
|
|
close(descriptor);
|
|
close(pair[0]);
|
|
close(pair[1]);
|
|
}
|
|
|
|
static void shared_test(int forked) {
|
|
int descriptor = route_socket();
|
|
if (forked) {
|
|
pid_t child = fork();
|
|
require(child >= 0, "fork");
|
|
if (child == 0) {
|
|
usable(descriptor);
|
|
close(descriptor);
|
|
_exit(0);
|
|
}
|
|
int status;
|
|
require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
|
|
"child socket");
|
|
usable(descriptor);
|
|
} else {
|
|
int alias = dup(descriptor);
|
|
require(alias >= 0 && dup2(descriptor, alias) == alias, "replace same socket alias");
|
|
int other = route_socket();
|
|
require(dup2(descriptor, other) == other, "replace other socket");
|
|
close(descriptor);
|
|
usable(alias);
|
|
close(alias);
|
|
usable(other);
|
|
close(other);
|
|
return;
|
|
}
|
|
close(descriptor);
|
|
}
|
|
|
|
static void close_range_test(const char *mode) {
|
|
int descriptor = route_socket();
|
|
if (!strcmp(mode, "closefrom")) {
|
|
void (*close_from)(int) = dlsym(RTLD_DEFAULT, "closefrom");
|
|
require(close_from != NULL, "closefrom symbol");
|
|
close_from(descriptor);
|
|
} else {
|
|
int (*close_range_fn)(unsigned int, unsigned int, int) = dlsym(RTLD_DEFAULT, "close_range");
|
|
require(close_range_fn != NULL, "close_range symbol");
|
|
if (!strcmp(mode, "cloexec")) {
|
|
require(close_range_fn(descriptor, descriptor, 4) == 0, "close_range cloexec");
|
|
usable(descriptor);
|
|
close(descriptor);
|
|
} else {
|
|
require(close_range_fn(descriptor, descriptor, 1) == -1 && errno == EINVAL,
|
|
"invalid close_range");
|
|
usable(descriptor);
|
|
require(close_range_fn(descriptor, descriptor, 0) == 0, "close_range");
|
|
}
|
|
}
|
|
for (int pass = 0; pass < 16; pass++) {
|
|
descriptor = route_socket();
|
|
usable(descriptor);
|
|
close(descriptor);
|
|
}
|
|
}
|
|
|
|
static void route_test(const char *destination) {
|
|
int descriptor = route_socket();
|
|
int family = strchr(destination, ':') ? AF_INET6 : AF_INET;
|
|
int size = family == AF_INET ? 4 : 16;
|
|
struct {
|
|
struct nlmsghdr header;
|
|
struct rtmsg info;
|
|
struct rtattr attr;
|
|
unsigned char destination[16];
|
|
} query = {
|
|
.header = {.nlmsg_type = RTM_GETROUTE, .nlmsg_flags = NLM_F_REQUEST, .nlmsg_seq = 42},
|
|
.info = {.rtm_family = family, .rtm_dst_len = size * 8},
|
|
.attr = {.rta_type = RTA_DST, .rta_len = RTA_LENGTH(size)}
|
|
};
|
|
query.header.nlmsg_len = NLMSG_LENGTH(sizeof(query.info)) + RTA_SPACE(size);
|
|
require(inet_pton(family, destination, query.destination) == 1, "destination");
|
|
struct sockaddr_nl address = {.nl_family = AF_NETLINK};
|
|
require(sendto(descriptor, &query, query.header.nlmsg_len, 0,
|
|
(struct sockaddr *)&address, sizeof(address)) == query.header.nlmsg_len, "route send");
|
|
unsigned char buffer[4096];
|
|
receive(descriptor, buffer, sizeof(buffer));
|
|
struct nlmsghdr *header = (struct nlmsghdr *)buffer;
|
|
if (header->nlmsg_type == NLMSG_ERROR) {
|
|
printf("error %d\n", ((struct nlmsgerr *)NLMSG_DATA(header))->error);
|
|
} else {
|
|
require(header->nlmsg_type == RTM_NEWROUTE, "route reply");
|
|
struct rtmsg *info = NLMSG_DATA(header);
|
|
int remaining = RTM_PAYLOAD(header);
|
|
unsigned interface = 0;
|
|
char gateway[INET6_ADDRSTRLEN] = "-", source[INET6_ADDRSTRLEN] = "-";
|
|
for (struct rtattr *attr = RTM_RTA(info); RTA_OK(attr, remaining);
|
|
attr = RTA_NEXT(attr, remaining)) {
|
|
if (attr->rta_type == RTA_OIF) memcpy(&interface, RTA_DATA(attr), sizeof(interface));
|
|
if (attr->rta_type == RTA_GATEWAY) inet_ntop(family, RTA_DATA(attr), gateway, sizeof(gateway));
|
|
if (attr->rta_type == RTA_PREFSRC) inet_ntop(family, RTA_DATA(attr), source, sizeof(source));
|
|
}
|
|
printf("route %u %u %u %u %s %s\n", info->rtm_type, info->rtm_scope,
|
|
info->rtm_table, interface, gateway, source);
|
|
}
|
|
close(descriptor);
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
require(argc >= 2, "mode");
|
|
if (!strcmp(argv[1], "ack") || !strcmp(argv[1], "error") || !strcmp(argv[1], "capped"))
|
|
ack_test(argv[1]);
|
|
else if (!strcmp(argv[1], "options")) options_test();
|
|
else if (!strcmp(argv[1], "dump")) dump_test();
|
|
else if (!strcmp(argv[1], "send_timeout")) send_timeout_test(0);
|
|
else if (!strcmp(argv[1], "nonblocking")) send_timeout_test(1);
|
|
else if (!strcmp(argv[1], "connect")) connect_test();
|
|
else if (!strcmp(argv[1], "receive")) receive_test();
|
|
else if (!strcmp(argv[1], "replace")) replacement_test(0);
|
|
else if (!strcmp(argv[1], "raw_replace")) replacement_test(1);
|
|
else if (!strcmp(argv[1], "shared")) shared_test(0);
|
|
else if (!strcmp(argv[1], "fork")) shared_test(1);
|
|
else if (!strcmp(argv[1], "socket_types")) socket_types_test();
|
|
else if (!strcmp(argv[1], "hotpath")) {
|
|
require(argc == 3, "hotpath arguments");
|
|
hotpath_test(atoi(argv[2]));
|
|
}
|
|
else if (!strcmp(argv[1], "transferred")) {
|
|
require(argc == 3, "transfer arguments");
|
|
transferred_test(argv[2]);
|
|
}
|
|
else if (!strcmp(argv[1], "named_link")) {
|
|
require(argc == 4, "named link arguments");
|
|
named_link_test(argv[2], argv[3]);
|
|
}
|
|
else if (!strcmp(argv[1], "autobind_race")) autobind_race_test();
|
|
else if (!strcmp(argv[1], "vfork_close_range")) vfork_close_range_test();
|
|
else if (!strcmp(argv[1], "ports")) ports_test(0);
|
|
else if (!strcmp(argv[1], "cross_process_ports")) ports_test(1);
|
|
else if (!strcmp(argv[1], "batch")) batch_test(0);
|
|
else if (!strcmp(argv[1], "oversized_batch")) batch_test(1);
|
|
else if (!strcmp(argv[1], "exec")) exec_test(argv[0], 0);
|
|
else if (!strcmp(argv[1], "fork_exec")) exec_test(argv[0], 1);
|
|
else if (!strcmp(argv[1], "inherited")) inherited_test(argc, argv);
|
|
else if (!strcmp(argv[1], "all_cloexec")) cloexec_test(argv[0]);
|
|
else if (!strcmp(argv[1], "rebind_after_exec")) {
|
|
require(argc == 3, "rebind arguments");
|
|
int descriptor = route_socket();
|
|
require(port_of(descriptor) == (unsigned)strtoul(argv[2], NULL, 10), "exec releases closed ports");
|
|
usable(descriptor);
|
|
close(descriptor);
|
|
}
|
|
else if (!strcmp(argv[1], "range") || !strcmp(argv[1], "cloexec") || !strcmp(argv[1], "closefrom"))
|
|
close_range_test(argv[1]);
|
|
else if (!strcmp(argv[1], "route")) {
|
|
require(argc >= 3, "route destination");
|
|
route_test(argv[2]);
|
|
} else duplicate_test(argv[1]);
|
|
return 0;
|
|
}
|
|
"""
|
|
|
|
SNAPSHOT = """if wlan0 2 1500
|
|
mac 02:11:22:33:44:55
|
|
addr 192.168.1.10 24
|
|
addr 192.168.1.130 25
|
|
addr fe80::10 64
|
|
addr 2001:db8::10 64
|
|
gw 192.168.1.1
|
|
gw 2001:db8::1
|
|
"""
|
|
|
|
|
|
@unittest.skipUnless(sys.platform.startswith("linux") and shutil.which("cc"), "needs Linux and a C compiler")
|
|
class NetlinkPreloadTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.temp = tempfile.TemporaryDirectory()
|
|
cls.directory = Path(cls.temp.name)
|
|
cls.preload = cls.directory / "libblsession.so"
|
|
cls.shim = cls.directory / "libshim.so"
|
|
cls.program = cls.directory / "program"
|
|
(cls.directory / "shim.c").write_text(SHIM)
|
|
(cls.directory / "program.c").write_text(PROGRAM)
|
|
subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-Wall", "-pthread",
|
|
"-o", str(cls.preload), *map(str, sorted(PRELOAD.glob("*.c"))), "-ldl"], check=True)
|
|
subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-Wall", "-pthread", "-o", str(cls.shim),
|
|
str(cls.directory / "shim.c"), "-ldl"], check=True)
|
|
subprocess.run(["cc", "-O2", "-Wall", "-pthread", "-o", str(cls.program),
|
|
str(cls.directory / "program.c"), "-ldl"], check=True)
|
|
cls.snapshot = cls.directory / "snapshot"
|
|
cls.snapshot.write_text(SNAPSHOT)
|
|
cls.direct = cls.directory / "direct"
|
|
cls.direct.write_text("\n".join(line for line in SNAPSHOT.splitlines() if not line.startswith("gw ")) + "\n")
|
|
cls.offline = cls.directory / "offline"
|
|
cls.offline.write_text("")
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.temp.cleanup()
|
|
|
|
def run_program(self, mode, *arguments, snapshot=None):
|
|
env = dict(os.environ, LD_PRELOAD=f"{self.preload}:{self.shim}",
|
|
NETLINK_TEST_SNAPSHOT=str(snapshot or self.snapshot),
|
|
BL_SYSV_DIR=str(self.directory / "sysv"))
|
|
result = subprocess.run([str(self.program), mode, *arguments], env=env,
|
|
capture_output=True, text=True, timeout=5)
|
|
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
|
return result.stdout.strip().split()
|
|
|
|
def test_successful_queries_acknowledge_requests(self):
|
|
self.run_program("ack")
|
|
|
|
def test_failed_queries_return_only_one_error(self):
|
|
self.run_program("error")
|
|
|
|
def test_cap_ack_omits_the_request_payload(self):
|
|
self.run_program("capped")
|
|
|
|
def test_socket_types_flags_and_identity_match_netlink(self):
|
|
self.run_program("socket_types")
|
|
|
|
def test_ordinary_socket_paths_do_not_lock_or_probe_for_netlink(self):
|
|
for active in ("0", "1"):
|
|
with self.subTest(netlink_active=active):
|
|
self.run_program("hotpath", active)
|
|
|
|
def test_transferred_sockets_restore_on_receipt_or_netlink_specific_operations(self):
|
|
for mode in ("received", "domain", "bind", "connect", "option"):
|
|
with self.subTest(discovery=mode):
|
|
self.run_program("transferred", mode)
|
|
|
|
def test_getlink_resolves_names_and_prefers_a_nonzero_index(self):
|
|
for name, mode, expected in (("wlan0", "name", ["link", "2", "wlan0"]),
|
|
("lo", "name", ["link", "1", "lo"]),
|
|
("unknown", "index", ["link", "1", "lo"]),
|
|
("unknown", "name", ["error", str(-errno.ENODEV)]),
|
|
("wlan0", "missing", ["error", str(-errno.EINVAL)]),
|
|
("wlan0", "unterminated", ["link", "2", "wlan0"]),
|
|
("lo", "unterminated", ["link", "1", "lo"]),
|
|
("unknown", "unterminated", ["error", str(-errno.ENODEV)]),
|
|
("x" * 15, "name", ["error", str(-errno.ENODEV)]),
|
|
("x" * 15, "unterminated", ["error", str(-errno.ENODEV)]),
|
|
("x" * 16, "unterminated", ["error", str(-errno.ERANGE)]),
|
|
("", "name", ["error", str(-errno.ENODEV)]),
|
|
("", "unterminated", ["error", str(-errno.ERANGE)]),
|
|
("wlan0", "oversized", ["error", str(-errno.ERANGE)])):
|
|
with self.subTest(name=name, mode=mode):
|
|
self.assertEqual(expected, self.run_program("named_link", name, mode))
|
|
self.assertEqual(["error", str(-errno.ENODEV)],
|
|
self.run_program("named_link", "wlan0", "name", snapshot=self.offline))
|
|
|
|
def test_more_than_eight_sockets_get_unique_ports_and_release_them(self):
|
|
self.run_program("ports")
|
|
|
|
def test_concurrent_implicit_binds_are_idempotent(self):
|
|
self.run_program("autobind_race")
|
|
|
|
def test_vfork_close_range_does_not_mutate_parent_bookkeeping(self):
|
|
self.run_program("vfork_close_range")
|
|
|
|
def test_explicit_ports_cannot_collide_across_processes(self):
|
|
self.run_program("cross_process_ports")
|
|
|
|
def test_large_scatter_gather_requests_receive_all_their_replies(self):
|
|
self.run_program("batch")
|
|
|
|
def test_reply_overflow_and_invalid_iovecs_fail_without_unix_fallback(self):
|
|
self.run_program("oversized_batch")
|
|
|
|
def test_non_cloexec_sockets_and_queued_replies_survive_exec(self):
|
|
self.run_program("exec")
|
|
|
|
def test_fork_exec_does_not_break_the_parents_socket(self):
|
|
self.run_program("fork_exec")
|
|
|
|
def test_cloexec_sockets_release_their_ports_after_exec(self):
|
|
self.run_program("all_cloexec")
|
|
|
|
def test_subscription_is_denied_and_receive_timeout_is_applied(self):
|
|
self.run_program("options")
|
|
|
|
def test_dump_keeps_multipart_completion(self):
|
|
self.run_program("dump")
|
|
|
|
def test_full_reply_queues_respect_send_timeout_and_nonblocking_mode(self):
|
|
for mode in ("send_timeout", "nonblocking"):
|
|
with self.subTest(mode=mode):
|
|
self.run_program(mode)
|
|
|
|
def test_kernel_connect_and_disconnect(self):
|
|
self.run_program("connect")
|
|
|
|
def test_receive_sources_and_truncation(self):
|
|
self.run_program("receive")
|
|
|
|
def test_duplicates_share_the_socket_until_the_last_close(self):
|
|
for mode in ("dup", "dup2", "dup3", "fcntl", "fcntl_cloexec", "high_dup"):
|
|
with self.subTest(mode=mode):
|
|
self.run_program(mode)
|
|
|
|
def test_replacing_a_route_fd_does_not_intercept_unix_traffic(self):
|
|
self.run_program("replace")
|
|
|
|
def test_untracked_replacement_is_detected_by_socket_identity(self):
|
|
self.run_program("raw_replace")
|
|
|
|
def test_replacing_other_netlink_descriptors_preserves_shared_state(self):
|
|
self.run_program("shared")
|
|
|
|
def test_forked_sockets_remain_usable_in_both_processes(self):
|
|
self.run_program("fork")
|
|
|
|
def test_range_closes_release_slots_but_failures_and_cloexec_do_not(self):
|
|
for mode in ("range", "cloexec", "closefrom"):
|
|
with self.subTest(mode=mode):
|
|
self.run_program(mode)
|
|
|
|
def test_loopback_and_own_addresses_are_local_routes(self):
|
|
for destination, source in (("127.0.0.2", "127.0.0.1"), ("::1", "::1"),
|
|
("192.168.1.10", "192.168.1.10"), ("2001:db8::10", "2001:db8::10")):
|
|
with self.subTest(destination=destination):
|
|
self.assertEqual(["route", "2", "254", "255", "1", "-", source],
|
|
self.run_program("route", destination))
|
|
|
|
def test_connected_routes_choose_the_longest_prefix_without_a_gateway(self):
|
|
for destination, source in (("192.168.1.11", "192.168.1.10"),
|
|
("192.168.1.131", "192.168.1.130"),
|
|
("fe80::11", "fe80::10"), ("2001:db8::11", "2001:db8::10")):
|
|
with self.subTest(destination=destination):
|
|
self.assertEqual(["route", "1", "253", "254", "2", "-", source],
|
|
self.run_program("route", destination, snapshot=self.direct))
|
|
|
|
def test_other_destinations_use_a_default_gateway(self):
|
|
self.assertEqual(["route", "1", "0", "254", "2", "192.168.1.1", "192.168.1.10"],
|
|
self.run_program("route", "8.8.8.8"))
|
|
self.assertEqual(["route", "1", "0", "254", "2", "2001:db8::1", "2001:db8::10"],
|
|
self.run_program("route", "2001:db9::1"))
|
|
|
|
def test_without_a_gateway_only_off_link_destinations_are_unreachable(self):
|
|
self.assertEqual(["error", str(-errno.ENETUNREACH)],
|
|
self.run_program("route", "8.8.8.8", snapshot=self.direct))
|
|
self.assertEqual(["route", "2", "254", "255", "1", "-", "127.0.0.1"],
|
|
self.run_program("route", "127.0.0.1", snapshot=self.offline))
|
|
|
|
@unittest.skipUnless(shutil.which("ip"), "needs iproute2")
|
|
def test_iproute2_dumps_and_lookups_accept_the_replies(self):
|
|
env = dict(os.environ, LD_PRELOAD=f"{self.preload}:{self.shim}",
|
|
NETLINK_TEST_SNAPSHOT=str(self.snapshot),
|
|
BL_SYSV_DIR=str(self.directory / "sysv"))
|
|
queries = ((["link", "show"], "wlan0"),
|
|
(["link", "show", "wlan0"], "wlan0"),
|
|
(["-d", "link", "show", "dev", "wlan0"], "wlan0"),
|
|
(["link", "show", "lo"], "lo"),
|
|
(["addr", "show"], "192.168.1.10/24"),
|
|
(["route", "show"], "default via 192.168.1.1 dev wlan0"),
|
|
(["route", "get", "127.0.0.1"], "local 127.0.0.1 dev lo"),
|
|
(["route", "get", "192.168.1.11"], "192.168.1.11 dev wlan0"),
|
|
(["-6", "route", "get", "fe80::11"], "fe80::11 dev wlan0"))
|
|
for arguments, expected in queries:
|
|
with self.subTest(arguments=arguments):
|
|
result = subprocess.run([shutil.which("ip"), *arguments], env=env,
|
|
capture_output=True, text=True, timeout=5)
|
|
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
|
self.assertIn(expected, result.stdout)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|