Page MenuHomeFreeBSD

D60702.1793887833.diff
No OneTemporary

Authored By
Unknown
Size
30 KB
Referenced Files
None
Subscribers
None

D60702.1793887833.diff

diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist
--- a/etc/mtree/BSD.tests.dist
+++ b/etc/mtree/BSD.tests.dist
@@ -897,6 +897,8 @@
libkqueue
..
..
+ linux
+ ..
mac
bsdextended
..
diff --git a/tests/sys/Makefile b/tests/sys/Makefile
--- a/tests/sys/Makefile
+++ b/tests/sys/Makefile
@@ -19,6 +19,7 @@
TESTS_SUBDIRS+= geom
TESTS_SUBDIRS+= kern
TESTS_SUBDIRS+= kqueue
+TESTS_SUBDIRS+= ${_linux}
TESTS_SUBDIRS+= mac
TESTS_SUBDIRS+= mqueue
TESTS_SUBDIRS+= net
@@ -47,6 +48,10 @@
_cddl= cddl
.endif
+.if ${MACHINE_ARCH} == "amd64" || ${MACHINE_ARCH} == "aarch64"
+_linux= linux
+.endif
+
.if ${MK_NETGRAPH} != "no"
_netgraph= netgraph
.endif
diff --git a/tests/sys/kern/tty_pts.c b/tests/sys/kern/tty_pts.c
--- a/tests/sys/kern/tty_pts.c
+++ b/tests/sys/kern/tty_pts.c
@@ -6,8 +6,13 @@
#include <sys/types.h>
#include <sys/ioctl.h>
+#include <sys/wait.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
#include <termios.h>
+#include <unistd.h>
#include <atf-c.h>
#include <libutil.h>
@@ -57,8 +62,80 @@
ATF_REQUIRE_EQ(0, read(master, rbuf, sizeof(rbuf)));
}
+ATF_TC(ctty_last_close);
+ATF_TC_HEAD(ctty_last_close, tc)
+{
+
+ atf_tc_set_md_var(tc, "descr", "Drop a native controlling terminal "
+ "on the last slave close");
+}
+ATF_TC_BODY(ctty_last_close, tc)
+{
+ struct {
+ int fd;
+ int error;
+ } result;
+ char byte;
+ int go[2], master, ready[2], slave, status;
+ ssize_t nread;
+ pid_t child;
+
+ ATF_REQUIRE_EQ(0, newpty(&master, &slave));
+ ATF_REQUIRE_EQ(0, pipe(ready));
+ ATF_REQUIRE_EQ(0, pipe(go));
+
+ child = fork();
+ ATF_REQUIRE(child >= 0);
+ if (child == 0) {
+ (void)close(master);
+ (void)close(ready[0]);
+ (void)close(go[1]);
+ if (setsid() < 0)
+ _exit(10);
+ if (ioctl(slave, TIOCSCTTY, 0) < 0)
+ _exit(11);
+ if (write(ready[1], "r", 1) != 1)
+ _exit(12);
+ if (read(go[0], &byte, 1) != 1)
+ _exit(13);
+ if (close(slave) < 0)
+ _exit(14);
+ errno = 0;
+ result.fd = open("/dev/tty", O_RDWR);
+ result.error = errno;
+ if (write(ready[1], &result, sizeof(result)) !=
+ (ssize_t)sizeof(result))
+ _exit(15);
+ if (result.fd >= 0)
+ (void)close(result.fd);
+ _exit(0);
+ }
+
+ (void)close(ready[1]);
+ (void)close(go[0]);
+ ATF_REQUIRE_EQ(1, read(ready[0], &byte, 1));
+ ATF_REQUIRE_EQ(0, close(slave));
+ ATF_REQUIRE_EQ(1, write(go[1], "g", 1));
+ nread = read(ready[0], &result, sizeof(result));
+ ATF_REQUIRE_MSG(waitpid(child, &status, 0) == child,
+ "waitpid failed: %s", strerror(errno));
+ (void)close(ready[0]);
+ (void)close(go[1]);
+ (void)close(master);
+
+ ATF_REQUIRE_MSG(WIFEXITED(status), "child died from signal %d",
+ WTERMSIG(status));
+ ATF_REQUIRE_MSG(WEXITSTATUS(status) == 0,
+ "child exited with status %d", WEXITSTATUS(status));
+ ATF_REQUIRE_MSG(nread == (ssize_t)sizeof(result),
+ "short result read: %zd", nread);
+ ATF_CHECK_EQ(-1, result.fd);
+ ATF_CHECK_EQ(ENXIO, result.error);
+}
+
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, fionread);
+ ATF_TP_ADD_TC(tp, ctty_last_close);
return (atf_no_error());
}
diff --git a/tests/sys/linux/Makefile b/tests/sys/linux/Makefile
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/Makefile
@@ -0,0 +1,63 @@
+PACKAGE= tests
+
+TESTSDIR= ${TESTSBASE}/sys/linux
+
+.include <src.lua.mk>
+
+.if ${MACHINE_ARCH} == "amd64"
+LINUX_TARGET= x86_64-linux-gnu
+LINUX_SYSARCH= amd64
+.elif ${MACHINE_ARCH} == "aarch64"
+LINUX_TARGET= aarch64-linux-gnu
+LINUX_SYSARCH= arm64
+.endif
+
+.if defined(LINUX_TARGET)
+ATF_TESTS_C+= devfs_pty
+ATF_TESTS_C+= epoll_fork
+ATF_TESTS_C+= ip_pktinfo
+ATF_TESTS_C+= recvmmsg
+
+LIBADD.devfs_pty= util
+
+.for _test in devfs_pty epoll_fork ip_pktinfo recvmmsg
+SRCS.${_test}= ${_test}.c linux_test_support.c
+.endfor
+
+LINUX_HELPERS= devfs_pty_linux epoll_fork_linux ip_pktinfo_linux \
+ recvmmsg_linux
+LINUX_CFLAGS= --target=${LINUX_TARGET} \
+ -I${SRCTOP}/sys -fno-stack-protector -fno-builtin -O2 \
+ -Wall -Wextra -Werror
+LINUX_LDFLAGS= -fuse-ld=lld -nostdlib -static -Wl,-e,_start \
+ -Wl,--build-id=none -Wl,--gc-sections
+LINUX_SYSCALL_MASTER= ${SRCTOP}/sys/${LINUX_SYSARCH}/linux/syscalls.master
+LINUX_SYSCALL_STUBS= linux_syscall_stubs.S
+LINUX_SYSCALL_GENDEPS= ${SRCTOP}/sys/tools/syscalls/config.lua \
+ ${SRCTOP}/sys/tools/syscalls/core/freebsd-syscall.lua \
+ ${SRCTOP}/sys/tools/syscalls/core/scarg.lua \
+ ${SRCTOP}/sys/tools/syscalls/core/scret.lua \
+ ${SRCTOP}/sys/tools/syscalls/core/syscall.lua \
+ ${SRCTOP}/sys/tools/syscalls/tools/util.lua
+LINUX_RUNTIME= linux_crt0.c linux_test.c linux_test.h linux_socket.h \
+ linux_syscalls.h ${LINUX_SYSCALL_STUBS}
+
+SCRIPTS+= ${LINUX_HELPERS}
+CLEANFILES+= ${LINUX_HELPERS} ${LINUX_SYSCALL_STUBS}
+
+${LINUX_SYSCALL_STUBS}: gen_syscall_stubs.lua ${LINUX_SYSCALL_MASTER} \
+ ${LINUX_SYSCALL_GENDEPS}
+ ${LUA} ${.CURDIR}/gen_syscall_stubs.lua ${SRCTOP} ${LINUX_SYSARCH} \
+ ${LINUX_SYSCALL_MASTER} > ${.TARGET}
+
+.for _helper in ${LINUX_HELPERS}
+SCRIPTSDIR_${_helper}= ${TESTSDIR}
+
+${_helper}: ${_helper}.c ${LINUX_RUNTIME}
+ ${CC} ${LINUX_CFLAGS} ${LINUX_LDFLAGS} -o ${.TARGET} \
+ ${.ALLSRC:M*.c} ${LINUX_SYSCALL_STUBS}
+ brandelf -t Linux ${.TARGET}
+.endfor
+.endif
+
+.include <bsd.test.mk>
diff --git a/tests/sys/linux/devfs_pty.c b/tests/sys/linux/devfs_pty.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/devfs_pty.c
@@ -0,0 +1,93 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+ */
+
+#include <sys/ioctl.h>
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <libutil.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+#include "linux_test_support.h"
+
+ATF_TC(ctty_last_close);
+ATF_TC_HEAD(ctty_last_close, tc)
+{
+
+ atf_tc_set_md_var(tc, "descr", "Keep a Linux controlling terminal "
+ "across the last slave close");
+ atf_tc_set_md_var(tc, "require.kmods", "linux64");
+}
+ATF_TC_BODY(ctty_last_close, tc)
+{
+ /*
+ * Set up the controlling terminal natively, then let a Linux process
+ * perform the last closes. Linux expects the session's reference to
+ * survive those closes, so reopening /dev/tty must succeed.
+ */
+ char buf[16], helper[PATH_MAX];
+ int master, pipefd[2], slave, status;
+ ssize_t nread;
+ pid_t child;
+
+ linux_test_helper_path(tc, "devfs_pty_linux", helper, sizeof(helper));
+ ATF_REQUIRE_MSG(openpty(&master, &slave, NULL, NULL, NULL) == 0,
+ "openpty failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pipe(pipefd) == 0, "pipe failed: %s", strerror(errno));
+
+ child = fork();
+ ATF_REQUIRE_MSG(child >= 0, "fork failed: %s", strerror(errno));
+ if (child == 0) {
+ (void)close(master);
+ (void)close(pipefd[0]);
+ if (setsid() < 0)
+ _exit(10);
+ if (ioctl(slave, TIOCSCTTY, 0) < 0)
+ _exit(11);
+ if (dup2(slave, STDIN_FILENO) < 0 ||
+ dup2(slave, STDOUT_FILENO) < 0 ||
+ dup2(slave, STDERR_FILENO) < 0)
+ _exit(12);
+ if (slave > STDERR_FILENO)
+ (void)close(slave);
+ if (pipefd[1] != 3) {
+ if (dup2(pipefd[1], 3) < 0)
+ _exit(13);
+ (void)close(pipefd[1]);
+ }
+ execl(helper, "devfs_pty_linux", (char *)NULL);
+ _exit(14);
+ }
+
+ (void)close(slave);
+ (void)close(pipefd[1]);
+ nread = read(pipefd[0], buf, sizeof(buf) - 1);
+ ATF_REQUIRE_MSG(nread >= 0, "read failed: %s", strerror(errno));
+ buf[nread] = '\0';
+ ATF_REQUIRE_MSG(waitpid(child, &status, 0) == child,
+ "waitpid failed: %s", strerror(errno));
+ (void)close(pipefd[0]);
+ (void)close(master);
+
+ ATF_REQUIRE_MSG(WIFEXITED(status), "Linux helper died from signal %d",
+ WTERMSIG(status));
+ ATF_CHECK_MSG(strcmp(buf, "ok\n") == 0,
+ "opening /dev/tty after the last slave close returned: %s", buf);
+ ATF_CHECK_MSG(WEXITSTATUS(status) == 0,
+ "Linux helper exited with status %d", WEXITSTATUS(status));
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+
+ ATF_TP_ADD_TC(tp, ctty_last_close);
+ return (atf_no_error());
+}
diff --git a/tests/sys/linux/devfs_pty_linux.c b/tests/sys/linux/devfs_pty_linux.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/devfs_pty_linux.c
@@ -0,0 +1,24 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+ */
+
+#include "linux_syscalls.h"
+
+int
+main(void)
+{
+ int fd;
+
+ (void)close_range(0, 2, 0);
+
+ fd = openat(LINUX_AT_FDCWD, "/dev/tty", LINUX_O_RDWR, 0);
+ if (fd < 0) {
+ (void)write(3, "fail\n", 5);
+ return (1);
+ }
+ (void)close(fd);
+ (void)write(3, "ok\n", 3);
+ return (0);
+}
diff --git a/tests/sys/linux/epoll_fork.c b/tests/sys/linux/epoll_fork.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/epoll_fork.c
@@ -0,0 +1,28 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+ */
+
+#include <atf-c.h>
+#include "linux_test_support.h"
+
+ATF_TC(epoll_descriptor);
+ATF_TC_HEAD(epoll_descriptor, tc)
+{
+
+ atf_tc_set_md_var(tc, "descr", "Preserve Linux epoll descriptors and "
+ "registrations across fork");
+ atf_tc_set_md_var(tc, "require.kmods", "linux64");
+}
+ATF_TC_BODY(epoll_descriptor, tc)
+{
+ linux_test_run(tc, "epoll_fork_linux");
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+
+ ATF_TP_ADD_TC(tp, epoll_descriptor);
+ return (atf_no_error());
+}
diff --git a/tests/sys/linux/epoll_fork_linux.c b/tests/sys/linux/epoll_fork_linux.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/epoll_fork_linux.c
@@ -0,0 +1,81 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+ */
+
+#include "linux_syscalls.h"
+#include "linux_test.h"
+
+static int failed;
+
+static void
+check(const char *description, long actual, long expected)
+{
+
+ if (actual == expected)
+ return;
+ putstr("FAIL: ");
+ putstr(description);
+ putstr("\n");
+ failed++;
+}
+
+static void
+test_flags(int flags)
+{
+ struct linux_epoll_event event = { 0 }, output = { 0 };
+ int epfd, extra[2], pipefd[2], status = 0;
+ linux_pid_t child, result;
+
+ event.events = LINUX_EPOLLIN;
+ event.data = 0x1234;
+ epfd = setup(epoll_create1(flags), "epoll_create1", &failed);
+ if (epfd < 0)
+ return;
+ if (setup(pipe2(pipefd, 0), "pipe2", &failed) < 0)
+ return;
+ if (setup(epoll_ctl(epfd, LINUX_EPOLL_CTL_ADD, pipefd[0],
+ &event), "epoll_ctl(ADD)", &failed) < 0)
+ return;
+ if (setup(write(pipefd[1], "x", 1), "write", &failed) < 0)
+ return;
+
+ child = clone(LINUX_SIGCHLD, 0, 0, 0, 0);
+ if (child < 0) {
+ (void)setup(child, "fork", &failed);
+ return;
+ }
+ if (child == 0) {
+ failed = 0;
+ check("epoll F_GETFD", fcntl(epfd, LINUX_F_GETFD, 0),
+ flags != 0 ? 1 : 0);
+ check("pipe F_GETFD", fcntl(pipefd[0], LINUX_F_GETFD, 0),
+ 0);
+ check("inherited readiness",
+ epoll_pwait(epfd, &output, 1, 0, 0, 0), 1);
+ check("inherited event data", output.data, event.data);
+ if (setup(pipe2(extra, 0), "child pipe2", &failed) >= 0)
+ check("epoll_ctl after fd allocation", epoll_ctl(epfd,
+ LINUX_EPOLL_CTL_MOD, pipefd[0], &event), 0);
+ exit_group(failed != 0 ? 1 : 0);
+ }
+
+ do {
+ result = wait4(child, &status, 0, 0);
+ } while (result == -1 && errno == LINUX_EINTR);
+ check("waitpid", result, child);
+ check("child status", status, 0);
+ (void)close(epfd);
+ (void)close(pipefd[0]);
+ (void)close(pipefd[1]);
+}
+
+int
+main(void)
+{
+
+ test_flags(0);
+ test_flags(LINUX_O_CLOEXEC);
+ return (failed != 0 ? 1 : 0);
+}
diff --git a/tests/sys/linux/gen_syscall_stubs.lua b/tests/sys/linux/gen_syscall_stubs.lua
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/gen_syscall_stubs.lua
@@ -0,0 +1,95 @@
+#!/usr/libexec/flua
+--
+-- SPDX-License-Identifier: BSD-2-Clause
+--
+-- Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+--
+
+if #arg ~= 3 then
+ error("usage: gen_syscall_stubs.lua srctop arch syscalls.master")
+end
+
+package.path = package.path .. ";" .. arg[1] ..
+ "/sys/tools/syscalls/?.lua"
+
+local config = require("config")
+local FreeBSDSyscall = require("core.freebsd-syscall")
+
+config.mergeCompat()
+
+local arch = arg[2]
+local tbl = FreeBSDSyscall:new({sysfile = arg[3], config = config})
+local syscalls = {}
+
+for _, syscall in pairs(tbl.syscalls) do
+ if syscall:native() then
+ local symbol = syscall.name:gsub("^linux_", "")
+ if syscalls[symbol] ~= nil then
+ error("duplicate syscall symbol: " .. symbol)
+ end
+ syscalls[symbol] = syscall.num
+ end
+end
+
+local function emit_stub(symbol, number)
+ local function_type = arch == "amd64" and "@function" or "%function"
+ local section_type = arch == "amd64" and "@progbits" or "%progbits"
+
+ io.write("\n\t.section\t.text.", symbol, ",\"ax\",", section_type,
+ "\n")
+ io.write("\t.globl\t", symbol, "\n")
+ io.write("\t.type\t", symbol, ",", function_type, "\n")
+ io.write(symbol, ":\n")
+ if arch == "amd64" then
+ io.write("\tmovq\t$", number, ",%rax\n")
+ io.write("\tmovq\t%rcx,%r10\n")
+ io.write("\tsyscall\n")
+ io.write("\tcmpq\t$-4095,%rax\n")
+ io.write("\tjae\t.Llinux_syscall_error\n")
+ elseif arch == "arm64" then
+ io.write("\tmov\tx8,#", number, "\n")
+ io.write("\tsvc\t#0\n")
+ io.write("\tcmn\tx0,#4095\n")
+ io.write("\tb.cs\t.Llinux_syscall_error\n")
+ else
+ error("unsupported architecture: " .. arch)
+ end
+ io.write("\tret\n")
+ io.write("\t.size\t", symbol, ",.-", symbol, "\n")
+end
+
+local function emit_error_handler()
+ local section_type = arch == "amd64" and "@progbits" or "%progbits"
+
+ io.write("\n\t.section\t.text.linux_syscall_error,\"ax\",",
+ section_type, "\n")
+ io.write(".Llinux_syscall_error:\n")
+ if arch == "amd64" then
+ io.write("\tnegq\t%rax\n")
+ io.write("\tmovl\t%eax,errno(%rip)\n")
+ io.write("\tmovq\t$-1,%rax\n")
+ elseif arch == "arm64" then
+ io.write("\tneg\tw1,w0\n")
+ io.write("\tadrp\tx2,errno\n")
+ io.write("\tstr\tw1,[x2,:lo12:errno]\n")
+ io.write("\tmov\tx0,#-1\n")
+ end
+ io.write("\tret\n")
+end
+
+io.write("/* Generated from ", arg[3], "; do not edit. */\n")
+local symbols = {}
+for symbol in pairs(syscalls) do
+ table.insert(symbols, symbol)
+end
+table.sort(symbols)
+for _, symbol in ipairs(symbols) do
+ emit_stub(symbol, syscalls[symbol])
+end
+emit_error_handler()
+
+if arch == "amd64" then
+ io.write("\n\t.section .note.GNU-stack,\"\",@progbits\n")
+else
+ io.write("\n\t.section .note.GNU-stack,\"\",%progbits\n")
+end
diff --git a/tests/sys/linux/ip_pktinfo.c b/tests/sys/linux/ip_pktinfo.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/ip_pktinfo.c
@@ -0,0 +1,28 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+ */
+
+#include <atf-c.h>
+#include "linux_test_support.h"
+
+ATF_TC(ip_pktinfo);
+ATF_TC_HEAD(ip_pktinfo, tc)
+{
+
+ atf_tc_set_md_var(tc, "descr",
+ "Receive Linux IP_PKTINFO ancillary data");
+ atf_tc_set_md_var(tc, "require.kmods", "linux64");
+}
+ATF_TC_BODY(ip_pktinfo, tc)
+{
+ linux_test_run(tc, "ip_pktinfo_linux");
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+
+ ATF_TP_ADD_TC(tp, ip_pktinfo);
+ return (atf_no_error());
+}
diff --git a/tests/sys/linux/ip_pktinfo_linux.c b/tests/sys/linux/ip_pktinfo_linux.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/ip_pktinfo_linux.c
@@ -0,0 +1,161 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+ */
+
+#include "linux_syscalls.h"
+#include "linux_socket.h"
+#include "linux_test.h"
+
+#define LINUX_SOL_IP 0
+#define LINUX_IP_PKTINFO 8
+
+struct linux_cmsghdr {
+ linux_size_t cmsg_len;
+ int cmsg_level;
+ int cmsg_type;
+};
+
+struct linux_in_pktinfo {
+ int ipi_ifindex;
+ unsigned int ipi_spec_dst;
+ unsigned int ipi_addr;
+};
+
+#define LINUX_CMSG_ALIGN(length) \
+ (((length) + sizeof(linux_size_t) - 1) & ~(sizeof(linux_size_t) - 1))
+#define LINUX_CMSG_DATA(cmsg) \
+ ((void *)((char *)(cmsg) + LINUX_CMSG_ALIGN(sizeof(*(cmsg)))))
+#define LINUX_CMSG_LEN(length) \
+ (LINUX_CMSG_ALIGN(sizeof(struct linux_cmsghdr)) + (length))
+#define LINUX_CMSG_SPACE(length) \
+ (LINUX_CMSG_ALIGN(sizeof(struct linux_cmsghdr)) + \
+ LINUX_CMSG_ALIGN(length))
+
+_Static_assert(sizeof(struct linux_in_pktinfo) == 12,
+ "Linux in_pktinfo ABI");
+
+static int failed;
+
+static void
+check(int condition, const char *description)
+{
+
+ if (condition)
+ return;
+ putstr("FAIL: ");
+ putstr(description);
+ putstr("\n");
+ failed++;
+}
+
+int
+main(void)
+{
+ struct linux_sockaddr_in address = { 0 };
+ struct linux_sockaddr_in source = { 0 };
+ struct linux_in_pktinfo *pktinfo;
+ struct linux_cmsghdr *cmsg;
+ struct linux_iovec iov, send_iov;
+ struct linux_msghdr msg = { 0 };
+ struct linux_msghdr send_msg = { 0 };
+ unsigned char control[64] = { 0 };
+ unsigned char send_control[LINUX_CMSG_SPACE(
+ sizeof(struct linux_in_pktinfo))] = { 0 };
+ unsigned int address_length;
+ char byte, received;
+ int fd, enabled, send_fd;
+
+ fd = setup(socket(LINUX_AF_INET, LINUX_SOCK_DGRAM, 0), "socket",
+ &failed);
+ if (fd < 0)
+ return (1);
+ enabled = 1;
+ if (setup(setsockopt(fd, LINUX_SOL_IP, LINUX_IP_PKTINFO, &enabled,
+ sizeof(enabled)), "setsockopt(IP_PKTINFO)", &failed) < 0)
+ return (1);
+
+ address.sin_family = LINUX_AF_INET;
+ address.sin_addr = __builtin_bswap32(0x7f000001U);
+ if (setup(bind(fd, &address, sizeof(address)), "bind",
+ &failed) < 0)
+ return (1);
+ address_length = sizeof(address);
+ if (setup(getsockname(fd, &address, &address_length),
+ "getsockname", &failed) < 0)
+ return (1);
+
+ byte = 'x';
+ if (setup(sendto(fd, &byte, sizeof(byte), 0, &address,
+ sizeof(address)), "sendto", &failed) < 0)
+ return (1);
+
+ iov.iov_base = &received;
+ iov.iov_len = sizeof(received);
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+ msg.msg_control = control;
+ msg.msg_controllen = sizeof(control);
+ if (setup(recvmsg(fd, &msg, 0), "recvmsg", &failed) < 0)
+ return (1);
+
+ check(received == byte, "received payload");
+ check(msg.msg_controllen >= sizeof(*cmsg), "control message present");
+ cmsg = (struct linux_cmsghdr *)(void *)control;
+ check(cmsg->cmsg_level == LINUX_SOL_IP, "IP_PKTINFO cmsg level");
+ check(cmsg->cmsg_type == LINUX_IP_PKTINFO, "IP_PKTINFO cmsg type");
+ check(cmsg->cmsg_len >= LINUX_CMSG_ALIGN(sizeof(*cmsg)) +
+ sizeof(*pktinfo), "IP_PKTINFO cmsg length");
+ pktinfo = LINUX_CMSG_DATA(cmsg);
+ check(pktinfo->ipi_ifindex > 0, "IP_PKTINFO interface index");
+ check(pktinfo->ipi_spec_dst == address.sin_addr,
+ "IP_PKTINFO local address");
+ check(pktinfo->ipi_addr == address.sin_addr,
+ "IP_PKTINFO destination address");
+
+ /*
+ * Linux applications such as wireguard-go use IP_PKTINFO with
+ * sendmsg(2) to select the source address of an outgoing datagram.
+ */
+ send_fd = setup(socket(LINUX_AF_INET, LINUX_SOCK_DGRAM, 0),
+ "send socket", &failed);
+ if (send_fd < 0)
+ return (1);
+ source.sin_family = LINUX_AF_INET;
+ if (setup(bind(send_fd, &source, sizeof(source)),
+ "bind send socket", &failed) < 0)
+ return (1);
+ byte = 'y';
+ send_iov.iov_base = &byte;
+ send_iov.iov_len = sizeof(byte);
+ send_msg.msg_name = &address;
+ send_msg.msg_namelen = sizeof(address);
+ send_msg.msg_iov = &send_iov;
+ send_msg.msg_iovlen = 1;
+ send_msg.msg_control = send_control;
+ send_msg.msg_controllen = sizeof(send_control);
+ cmsg = (struct linux_cmsghdr *)(void *)send_control;
+ cmsg->cmsg_len = LINUX_CMSG_LEN(sizeof(*pktinfo));
+ cmsg->cmsg_level = LINUX_SOL_IP;
+ cmsg->cmsg_type = LINUX_IP_PKTINFO;
+ pktinfo = LINUX_CMSG_DATA(cmsg);
+ pktinfo->ipi_spec_dst = address.sin_addr;
+ if (setup(sendmsg(send_fd, &send_msg, 0),
+ "sendmsg(IP_PKTINFO)", &failed) < 0)
+ return (1);
+ iov.iov_base = &received;
+ iov.iov_len = sizeof(received);
+ msg.msg_name = 0;
+ msg.msg_namelen = 0;
+ msg.msg_control = 0;
+ msg.msg_controllen = 0;
+ if (setup(recvmsg(fd, &msg, 0),
+ "recvmsg after sendmsg(IP_PKTINFO)", &failed) < 0)
+ return (1);
+ check(received == byte, "received sendmsg(IP_PKTINFO) payload");
+
+ (void)close(send_fd);
+ (void)close(fd);
+ return (failed != 0 ? 1 : 0);
+}
diff --git a/tests/sys/linux/linux_crt0.c b/tests/sys/linux/linux_crt0.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/linux_crt0.c
@@ -0,0 +1,35 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+ */
+
+#include "linux_syscalls.h"
+#include "linux_test.h"
+
+int errno;
+
+void
+linux_start(void)
+{
+
+ exit_group(main());
+}
+
+#if defined(__amd64__)
+__asm__(".global _start\n"
+ "_start:\n"
+ "xor %ebp,%ebp\n"
+ "andq $-16,%rsp\n"
+ "call linux_start\n"
+ "ud2\n");
+#elif defined(__aarch64__)
+__asm__(".global _start\n"
+ "_start:\n"
+ "mov x29, xzr\n"
+ "mov x30, xzr\n"
+ "bl linux_start\n"
+ "brk #0\n");
+#else
+#error "Unsupported Linux architecture"
+#endif
diff --git a/tests/sys/linux/linux_socket.h b/tests/sys/linux/linux_socket.h
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/linux_socket.h
@@ -0,0 +1,37 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+ */
+
+#ifndef _LINUX_TEST_SOCKET_H_
+#define _LINUX_TEST_SOCKET_H_
+
+#include "linux_syscalls.h"
+
+#define LINUX_AF_INET 2
+#define LINUX_SOCK_DGRAM 2
+
+struct linux_sockaddr_in {
+ unsigned short sin_family;
+ unsigned short sin_port;
+ unsigned int sin_addr;
+ unsigned char sin_zero[8];
+};
+
+struct linux_iovec {
+ void *iov_base;
+ linux_size_t iov_len;
+};
+
+struct linux_msghdr {
+ void *msg_name;
+ unsigned int msg_namelen;
+ struct linux_iovec *msg_iov;
+ linux_size_t msg_iovlen;
+ void *msg_control;
+ linux_size_t msg_controllen;
+ unsigned int msg_flags;
+};
+
+#endif
diff --git a/tests/sys/linux/linux_syscalls.h b/tests/sys/linux/linux_syscalls.h
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/linux_syscalls.h
@@ -0,0 +1,82 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+ */
+
+#ifndef _LINUX_TEST_SYSCALLS_H_
+#define _LINUX_TEST_SYSCALLS_H_
+
+#include <compat/linux/linux_errno.h>
+#include <compat/linux/linux_event.h>
+
+#define LINUX_O_RDWR 2
+#define LINUX_AT_FDCWD -100
+#define LINUX_SIGCHLD 17
+#define LINUX_F_GETFD 1
+#define LINUX_O_CLOEXEC 02000000
+
+typedef int linux_pid_t;
+typedef __SIZE_TYPE__ linux_size_t;
+typedef __PTRDIFF_TYPE__ linux_ssize_t;
+
+/* The test helpers are single-threaded, so errno does not require TLS. */
+extern int errno;
+
+struct linux_epoll_event {
+ unsigned int events;
+#if defined(__aarch64__)
+ unsigned int padding;
+#endif
+ unsigned long data;
+}
+#if defined(__amd64__)
+__attribute__((packed))
+#endif
+;
+
+#if defined(__amd64__)
+_Static_assert(sizeof(struct linux_epoll_event) == 12,
+ "Linux x86-64 epoll ABI");
+#elif defined(__aarch64__)
+_Static_assert(sizeof(struct linux_epoll_event) == 16,
+ "Linux AArch64 epoll ABI");
+#endif
+
+int bind(int fd, const void *address, unsigned int address_length);
+int close(int fd);
+int close_range(unsigned int first, unsigned int last,
+ unsigned int flags);
+int epoll_create1(int flags);
+int epoll_ctl(int epfd, int op, int fd,
+ struct linux_epoll_event *event);
+int epoll_pwait(int epfd, struct linux_epoll_event *events,
+ int maxevents, int timeout, const void *sigmask, linux_size_t sigsetsize);
+int fcntl(int fd, int command, long argument);
+int getsockname(int fd, void *address, unsigned int *address_length);
+#if defined(__amd64__)
+linux_pid_t clone(unsigned long flags, void *stack, int *parent_tid,
+ int *child_tid, unsigned long tls);
+#elif defined(__aarch64__)
+linux_pid_t clone(unsigned long flags, void *stack, int *parent_tid,
+ unsigned long tls, int *child_tid);
+#endif
+int openat(int dirfd, const char *path, int flags, int mode);
+int pipe2(int pipefd[2], int flags);
+int ppoll(void *fds, unsigned int count, void *timeout, const void *sigmask,
+ linux_size_t sigsetsize);
+int recvmmsg(int fd, void *messages, unsigned int count, int flags,
+ void *timeout);
+linux_ssize_t recvmsg(int fd, void *message, int flags);
+linux_ssize_t sendmsg(int fd, const void *message, int flags);
+linux_ssize_t sendto(int fd, const void *buffer, linux_size_t length,
+ int flags, const void *address, unsigned int address_length);
+int setsockopt(int fd, int level, int name, const void *value,
+ unsigned int value_length);
+int socket(int domain, int type, int protocol);
+linux_pid_t wait4(linux_pid_t pid, int *status, int options,
+ void *rusage);
+linux_ssize_t write(int fd, const void *buffer, linux_size_t length);
+_Noreturn void exit_group(int status);
+
+#endif /* _LINUX_TEST_SYSCALLS_H_ */
diff --git a/tests/sys/linux/linux_test.h b/tests/sys/linux/linux_test.h
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/linux_test.h
@@ -0,0 +1,14 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+ */
+
+#ifndef _LINUX_TEST_H_
+#define _LINUX_TEST_H_
+
+int main(void);
+void putstr(const char *);
+int setup(long, const char *, int *);
+
+#endif
diff --git a/tests/sys/linux/linux_test.c b/tests/sys/linux/linux_test.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/linux_test.c
@@ -0,0 +1,42 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+ */
+
+#include "linux_syscalls.h"
+#include "linux_test.h"
+
+void
+putstr(const char *string)
+{
+ const char *p;
+ linux_size_t length;
+ linux_ssize_t written;
+
+ for (p = string; *p != '\0'; p++)
+ ;
+ length = p - string;
+ while (length > 0) {
+ written = write(1, string, length);
+ if (written == -1 && errno == LINUX_EINTR)
+ continue;
+ if (written <= 0)
+ return;
+ string += written;
+ length -= written;
+ }
+}
+
+int
+setup(long result, const char *description, int *failed)
+{
+ if (result >= 0)
+ return ((int)result);
+ putstr("SETUP FAILED: ");
+ putstr(description);
+ putstr("\n");
+ if (failed != 0)
+ (*failed)++;
+ return (-1);
+}
diff --git a/tests/sys/linux/linux_test_support.h b/tests/sys/linux/linux_test_support.h
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/linux_test_support.h
@@ -0,0 +1,16 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+ */
+
+#ifndef _LINUX_TEST_SUPPORT_H_
+#define _LINUX_TEST_SUPPORT_H_
+
+#include <sys/types.h>
+#include <atf-c.h>
+
+void linux_test_helper_path(const atf_tc_t *, const char *, char *, size_t);
+void linux_test_run(const atf_tc_t *, const char *);
+
+#endif /* _LINUX_TEST_SUPPORT_H_ */
diff --git a/tests/sys/linux/linux_test_support.c b/tests/sys/linux/linux_test_support.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/linux_test_support.c
@@ -0,0 +1,55 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+ */
+
+#include <sys/sysctl.h>
+#include <sys/wait.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <atf-c.h>
+#include "linux_test_support.h"
+
+void
+linux_test_helper_path(const atf_tc_t *tc, const char *name, char *path,
+ size_t pathlen)
+{
+ const char *srcdir;
+ size_t len;
+
+ len = 0;
+ if (sysctlbyname("compat.linux.emul_path", NULL, &len, NULL, 0) != 0)
+ atf_tc_skip("Linux ABI is not available: %s", strerror(errno));
+ srcdir = atf_tc_get_config_var(tc, "srcdir");
+ ATF_REQUIRE_MSG(snprintf(path, pathlen, "%s/%s", srcdir, name) <
+ (int)pathlen, "test helper path is too long");
+ ATF_REQUIRE_MSG(access(path, X_OK) == 0,
+ "Linux test helper is not executable (%s): %s", path,
+ strerror(errno));
+}
+
+void
+linux_test_run(const atf_tc_t *tc, const char *name)
+{
+ char helper[PATH_MAX];
+ int status;
+ pid_t child;
+
+ linux_test_helper_path(tc, name, helper, sizeof(helper));
+ child = fork();
+ ATF_REQUIRE_MSG(child >= 0, "fork failed: %s", strerror(errno));
+ if (child == 0) {
+ execl(helper, name, (char *)NULL);
+ _exit(127);
+ }
+ ATF_REQUIRE_MSG(waitpid(child, &status, 0) == child,
+ "waitpid failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(WIFEXITED(status), "Linux helper died from signal %d",
+ WTERMSIG(status));
+ ATF_CHECK_MSG(WEXITSTATUS(status) == 0,
+ "Linux helper exited with status %d", WEXITSTATUS(status));
+}
diff --git a/tests/sys/linux/recvmmsg.c b/tests/sys/linux/recvmmsg.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/recvmmsg.c
@@ -0,0 +1,28 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+ */
+
+#include <atf-c.h>
+#include "linux_test_support.h"
+
+ATF_TC(partial_result);
+ATF_TC_HEAD(partial_result, tc)
+{
+
+ atf_tc_set_md_var(tc, "descr",
+ "Return datagrams received before EAGAIN");
+ atf_tc_set_md_var(tc, "require.kmods", "linux64");
+}
+ATF_TC_BODY(partial_result, tc)
+{
+ linux_test_run(tc, "recvmmsg_linux");
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+
+ ATF_TP_ADD_TC(tp, partial_result);
+ return (atf_no_error());
+}
diff --git a/tests/sys/linux/recvmmsg_linux.c b/tests/sys/linux/recvmmsg_linux.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/linux/recvmmsg_linux.c
@@ -0,0 +1,91 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Vladimir Grebenshchikov <vova@zote.me>
+ */
+
+#include "linux_syscalls.h"
+#include "linux_socket.h"
+#include "linux_test.h"
+
+#define LINUX_SOCK_NONBLOCK 000004000
+#define LINUX_POLLIN 0x0001
+
+struct linux_mmsghdr {
+ struct linux_msghdr msg_hdr;
+ unsigned int msg_len;
+};
+
+struct linux_pollfd {
+ int fd;
+ short events;
+ short revents;
+};
+
+struct linux_timespec {
+ long tv_sec;
+ long tv_nsec;
+};
+
+int
+main(void)
+{
+ struct linux_sockaddr_in address = { 0 };
+ struct linux_mmsghdr messages[2] = { 0 };
+ struct linux_pollfd pollfd;
+ struct linux_timespec timeout;
+ struct linux_iovec iov[2];
+ unsigned int address_length;
+ char payload, received[2] = { 0 };
+ int fd, result;
+
+ fd = setup(socket(LINUX_AF_INET,
+ LINUX_SOCK_DGRAM | LINUX_SOCK_NONBLOCK, 0), "socket", 0);
+ if (fd < 0)
+ return (1);
+ address.sin_family = LINUX_AF_INET;
+ address.sin_addr = __builtin_bswap32(0x7f000001U);
+ if (setup(bind(fd, &address, sizeof(address)), "bind", 0) < 0)
+ return (1);
+ address_length = sizeof(address);
+ if (setup(getsockname(fd, &address, &address_length),
+ "getsockname", 0) < 0)
+ return (1);
+
+ payload = 'x';
+ if (setup(sendto(fd, &payload, sizeof(payload), 0, &address,
+ sizeof(address)), "sendto", 0) < 0)
+ return (1);
+ pollfd.fd = fd;
+ pollfd.events = LINUX_POLLIN;
+ pollfd.revents = 0;
+ timeout.tv_sec = 1;
+ timeout.tv_nsec = 0;
+ if (setup(ppoll(&pollfd, 1, &timeout, 0, 0), "ppoll", 0) != 1) {
+ putstr("SETUP FAILED: socket did not become readable\n");
+ return (1);
+ }
+
+ for (unsigned int i = 0; i < 2; i++) {
+ iov[i].iov_base = &received[i];
+ iov[i].iov_len = sizeof(received[i]);
+ messages[i].msg_hdr.msg_iov = &iov[i];
+ messages[i].msg_hdr.msg_iovlen = 1;
+ }
+ result = recvmmsg(fd, messages, 2, 0, 0);
+ if (result != 1) {
+ putstr("FAIL: recvmmsg did not return one datagram\n");
+ return (1);
+ }
+ if (messages[0].msg_len != sizeof(payload)) {
+ putstr("FAIL: incorrect datagram length\n");
+ return (1);
+ }
+ if (received[0] != payload) {
+ putstr("FAIL: incorrect datagram payload\n");
+ return (1);
+ }
+
+ (void)close(fd);
+ return (0);
+}

File Metadata

Mime Type
text/plain
Expires
Thu, Nov 5, 2:10 PM (5 h, 30 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41449461
Default Alt Text
D60702.1793887833.diff (30 KB)

Event Timeline