diff --git a/src/syscall/abi.h b/src/syscall/abi.h index 1cea435..fa7d22d 100644 --- a/src/syscall/abi.h +++ b/src/syscall/abi.h @@ -352,6 +352,7 @@ typedef struct { #define LINUX_TIOCSCTTY 0x540E /* -> macOS TIOCSCTTY (same semantics) */ #define LINUX_TIOCGWINSZ 0x5413 /* -> macOS TIOCGWINSZ (same struct) */ #define LINUX_FIONREAD 0x541B /* -> macOS FIONREAD (same semantics) */ +#define LINUX_FIOASYNC 0x5452 /* set/clear O_ASYNC (arg: int *) */ #define LINUX_TIOCNOTTY 0x5422 /* -> macOS TIOCNOTTY (same semantics) */ #define LINUX_TIOCGSID 0x5429 /* -> macOS TIOCGSID (same semantics) */ /* termios2 variant (adds c_ispeed/c_ospeed) */ diff --git a/src/syscall/fs.c b/src/syscall/fs.c index 669c8f8..c603739 100644 --- a/src/syscall/fs.c +++ b/src/syscall/fs.c @@ -801,6 +801,29 @@ int64_t sys_fcntl(guest_t *g, int fd, int cmd, uint64_t arg) host_fd_ref_close(&host_ref); return 0; } + case 8: /* F_SETOWN */ + case 15: /* F_SETOWN_EX */ + /* SIGIO/SIGURG delivery owner. nginx's ngx_spawn_process pairs + * ioctl(FIOASYNC) with fcntl(F_SETOWN) on the channel socket before + * fork() and aborts the worker (NGX_INVALID_PID) if either fails. + * elfuse does not deliver host SIGIO into the guest (see LINUX_FIOASYNC + * in sys_ioctl), so no owner is tracked: accept the request as a + * no-op. */ + return 0; + case 9: /* F_GETOWN */ + /* No owner tracked; report none. */ + return 0; + case 16: { /* F_GETOWN_EX */ + /* glibc implements fcntl(F_GETOWN) on top of F_GETOWN_EX, so this must + * answer coherently with the F_SETOWN no-op above rather than EINVAL + * (which would make F_GETOWN fail under glibc). Report "owned by no + * specific process": struct f_owner_ex { int type; int pid; } with + * type=F_OWNER_PID(1), pid=0. */ + int32_t owner_ex[2] = {1 /* F_OWNER_PID */, 0 /* pid */}; + if (guest_write_small(g, arg, owner_ex, sizeof(owner_ex)) < 0) + return -LINUX_EFAULT; + return 0; + } case 1024: /* F_GETPIPE_SZ */ /* macOS does not support pipe size queries; return default 64KiB */ return 65536; diff --git a/src/syscall/io.c b/src/syscall/io.c index c3d26d2..6290c45 100644 --- a/src/syscall/io.c +++ b/src/syscall/io.c @@ -1688,6 +1688,25 @@ int64_t sys_ioctl(guest_t *g, int fd, uint64_t request, uint64_t arg) return 0; } + case LINUX_FIOASYNC: { + /* Set/clear O_ASYNC (SIGIO-driven I/O). nginx's ngx_spawn_process arms + * this on the master's channel socket right before fork() and treats a + * failure as fatal (ngx_close_channel + return NGX_INVALID_PID), so + * answering ENOTTY here aborts worker spawning entirely -- the master + * is left with no workers and accepted connections are never serviced. + * elfuse does not forward host SIGIO into the guest, and nginx workers + * receive both client I/O and channel commands via epoll rather than + * SIGIO, so accept the request as a no-op: read the int arg for EFAULT + * parity and report success without arming host async delivery. */ + int32_t on = 0; + if (guest_read_small(g, arg, &on, sizeof(on)) < 0) { + host_fd_ref_close(&host_ref); + return -LINUX_EFAULT; + } + host_fd_ref_close(&host_ref); + return 0; + } + default: host_fd_ref_close(&host_ref); return -LINUX_ENOTTY; diff --git a/tests/manifest.txt b/tests/manifest.txt index 7273505..34b8cdf 100644 --- a/tests/manifest.txt +++ b/tests/manifest.txt @@ -64,6 +64,7 @@ test-epoll test-epoll-edge test-timerfd test-large-io-boundary +test-ioctl-fioasync [section] /proc and /dev emulation tests test-proc diff --git a/tests/test-ioctl-fioasync.c b/tests/test-ioctl-fioasync.c new file mode 100644 index 0000000..c456af3 --- /dev/null +++ b/tests/test-ioctl-fioasync.c @@ -0,0 +1,90 @@ +/* FIOASYNC ioctl + F_SETOWN/F_GETOWN fcntl regression test + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * nginx's ngx_spawn_process arms the master->worker channel socket with + * ioctl(FIOASYNC) immediately followed by fcntl(F_SETOWN), right before fork(), + * and treats a failure of EITHER as fatal: it logs an alert, ngx_close_channel, + * and returns NGX_INVALID_PID -- so it never forks the worker. elfuse used to + * answer FIOASYNC with ENOTTY and F_SETOWN with EINVAL, which silently left the + * master with zero workers: the listen socket still accepted connections at the + * host kernel, but nothing in the guest ever accept()ed them, so every request + * hung. This test pins the fix by replaying that pre-fork channel arming. + * + * elfuse does not forward host SIGIO into the guest, and nginx workers receive + * client I/O and channel commands via epoll rather than SIGIO, so both calls + * are accepted as no-ops that report success (F_GETOWN reports "no owner", 0). + * + * Syscalls exercised: socketpair(199), socket(198), ioctl(29) FIOASYNC, + * fcntl(25) F_SETOWN/F_GETOWN, getpid(172), close(57) + */ + +#include +#include +#include +#include + +#include "test-harness.h" + +#ifndef FIOASYNC +#define FIOASYNC 0x5452 +#endif + +int passes = 0, fails = 0; + +/* Replay nginx's ngx_spawn_process async/owner arming on a single fd. */ +static void check_async_owner(int fd, const char *what) +{ + char label[80]; + int on = 1; + + snprintf(label, sizeof(label), "%s: ioctl(FIOASYNC) enable -> 0", what); + TEST(label); + EXPECT_EQ(ioctl(fd, FIOASYNC, &on), 0, "FIOASYNC enable rejected"); + + snprintf(label, sizeof(label), "%s: fcntl(F_SETOWN) -> 0", what); + TEST(label); + EXPECT_EQ(fcntl(fd, F_SETOWN, getpid()), 0, "F_SETOWN rejected"); + + /* F_GETOWN reports the owner; elfuse tracks none, so 0 (no error). glibc + * may probe F_GETOWN_EX first and fall back to plain F_GETOWN on EINVAL -- + * either way the visible result must not be a failure. */ + snprintf(label, sizeof(label), "%s: fcntl(F_GETOWN) -> not an error", what); + TEST(label); + EXPECT_TRUE(fcntl(fd, F_GETOWN) >= 0, "F_GETOWN returned an error"); + + on = 0; + snprintf(label, sizeof(label), "%s: ioctl(FIOASYNC) disable -> 0", what); + TEST(label); + EXPECT_EQ(ioctl(fd, FIOASYNC, &on), 0, "FIOASYNC disable rejected"); +} + +int main(void) +{ + printf("test-ioctl-fioasync: FIOASYNC ioctl + F_SETOWN/F_GETOWN fcntl\n"); + + /* nginx's channel is an AF_UNIX SOCK_STREAM socketpair; FIOASYNC/F_SETOWN + * are applied to channel[0] (nginx also marks it non-blocking via FIONBIO + * first, which this test omits to stay focused on the calls added here). */ + int sp[2]; + TEST("socketpair(AF_UNIX, SOCK_STREAM)"); + EXPECT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sp), 0, "socketpair failed"); + + check_async_owner(sp[0], "unix socketpair"); + + close(sp[0]); + close(sp[1]); + + /* A plain TCP socket too -- the same family as nginx's listen sockets. */ + int s = socket(AF_INET, SOCK_STREAM, 0); + TEST("socket(AF_INET, SOCK_STREAM)"); + EXPECT_TRUE(s >= 0, "socket failed"); + if (s >= 0) { + check_async_owner(s, "tcp socket"); + close(s); + } + + SUMMARY("test-ioctl-fioasync"); + return fails > 0 ? 1 : 0; +}