Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F177152562
D60702.1793887833.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
30 KB
Referenced Files
None
Subscribers
None
D60702.1793887833.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D60702: tests: Add Linuxulator regression test infrastructure
Attached
Detach File
Event Timeline