mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 19:11:08 +00:00
1303 lines
58 KiB
Python
1303 lines
58 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/uio.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");
|
|
|
|
int pipes[2];
|
|
require(pipe(pipes) == 0, "ordinary pipe setup");
|
|
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");
|
|
require(write(client, "w", 1) == 1, "ordinary write");
|
|
require(read(server, &buffer, 1) == 1 && buffer == 'w', "ordinary read");
|
|
char pair[2];
|
|
struct iovec pieces[2] = {{.iov_base = "v", .iov_len = 1}, {.iov_base = "u", .iov_len = 1}};
|
|
require(writev(client, pieces, 2) == 2, "ordinary writev");
|
|
require(recv(server, pair, 2, MSG_WAITALL) == 2 && pair[0] == 'v' && pair[1] == 'u',
|
|
"ordinary writev data");
|
|
require(write(pipes[1], "p", 1) == 1 && read(pipes[0], &buffer, 1) == 1 && buffer == 'p',
|
|
"ordinary pipe");
|
|
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(pipes[0]);
|
|
close(pipes[1]);
|
|
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 ssize_t link_reply(int descriptor, const char *path, unsigned char *buffer, size_t capacity) {
|
|
struct {
|
|
struct nlmsghdr header;
|
|
struct ifinfomsg info;
|
|
} query = {
|
|
.header = {.nlmsg_len = sizeof(query), .nlmsg_type = RTM_GETLINK,
|
|
.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK, .nlmsg_seq = 42},
|
|
.info = {.ifi_index = 1}
|
|
};
|
|
struct iovec parts[2] = {{.iov_base = &query.header, .iov_len = sizeof(query.header)},
|
|
{.iov_base = &query.info, .iov_len = sizeof(query.info)}};
|
|
struct msghdr message = {.msg_iov = parts, .msg_iovlen = 2};
|
|
ssize_t sent;
|
|
if (!strcmp(path, "send")) sent = send(descriptor, &query, sizeof(query), 0);
|
|
else if (!strcmp(path, "sendmsg")) sent = sendmsg(descriptor, &message, 0);
|
|
else if (!strcmp(path, "write")) sent = write(descriptor, &query, sizeof(query));
|
|
else sent = writev(descriptor, parts, 2);
|
|
require(sent == sizeof(query), path);
|
|
ssize_t received = read(descriptor, buffer, capacity);
|
|
require(received > 0, "read reply");
|
|
return received;
|
|
}
|
|
|
|
static void write_test(void) {
|
|
const char *paths[] = {"send", "sendmsg", "write", "writev"};
|
|
unsigned char expected[4096];
|
|
int descriptor = route_socket();
|
|
ssize_t expected_length = link_reply(descriptor, paths[0], expected, sizeof(expected));
|
|
struct nlmsghdr *header = (struct nlmsghdr *)expected;
|
|
require(header->nlmsg_type == RTM_NEWLINK && header->nlmsg_seq == 42
|
|
&& header->nlmsg_pid == port_of(descriptor), "send reply");
|
|
require(expected_length > (ssize_t)NLMSG_ALIGN(header->nlmsg_len), "acknowledged reply");
|
|
struct nlmsghdr *acknowledgement = (struct nlmsghdr *)(expected + NLMSG_ALIGN(header->nlmsg_len));
|
|
require(acknowledgement->nlmsg_type == NLMSG_ERROR
|
|
&& ((struct nlmsgerr *)NLMSG_DATA(acknowledgement))->error == 0, "send acknowledgement");
|
|
for (size_t index = 1; index < sizeof(paths) / sizeof(paths[0]); index++) {
|
|
unsigned char buffer[4096];
|
|
ssize_t length = link_reply(descriptor, paths[index], buffer, sizeof(buffer));
|
|
require(length == expected_length, "reply length");
|
|
require(!memcmp(buffer, expected, length), "reply bytes");
|
|
}
|
|
require(recv(descriptor, expected, sizeof(expected), MSG_DONTWAIT) == -1 && errno == EAGAIN,
|
|
"no extra replies");
|
|
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], "write")) write_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_write_and_writev_requests_get_the_same_replies_as_send(self):
|
|
self.run_program("write")
|
|
|
|
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()
|