Skip to content

Commit 4164dbf

Browse files
committed
Accept FIOASYNC ioctl and F_SETOWN/F_GETOWN fcntl
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 failure of either as fatal: it logs an alert, closes the channel, and returns NGX_INVALID_PID without ever forking the worker. elfuse answered FIOASYNC with ENOTTY and F_SETOWN with EINVAL, so nginx in its default master/worker mode silently ended up with zero workers: the listen socket still accepted connections at the host kernel, but no guest worker ever accept()ed them, so every request hung. (With "master_process off" nginx served fine, which masked the issue.) 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 both calls are safe to accept as no-ops: - sys_ioctl: FIOASYNC reads the int arg (for EFAULT parity) and returns success without arming host O_ASYNC. - sys_fcntl: F_SETOWN / F_SETOWN_EX accept and track no owner; F_GETOWN / F_GETOWN_EX report "no owner". glibc implements fcntl(F_GETOWN) on top of F_GETOWN_EX, so the EX form writes a struct f_owner_ex{type=F_OWNER_PID, pid=0} to stay coherent. With this, "nginx -g 'daemon off;'" forks its worker and serves HTTP (GET/HEAD/404, keep-alive, concurrency) on the default config. Add tests/test-ioctl-fioasync.c, which replays nginx's pre-fork channel arming on a socketpair and a TCP socket.
1 parent 23b8300 commit 4164dbf

5 files changed

Lines changed: 134 additions & 0 deletions

File tree

src/syscall/abi.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ typedef struct {
352352
#define LINUX_TIOCSCTTY 0x540E /* -> macOS TIOCSCTTY (same semantics) */
353353
#define LINUX_TIOCGWINSZ 0x5413 /* -> macOS TIOCGWINSZ (same struct) */
354354
#define LINUX_FIONREAD 0x541B /* -> macOS FIONREAD (same semantics) */
355+
#define LINUX_FIOASYNC 0x5452 /* set/clear O_ASYNC SIGIO delivery (arg: int *) */
355356
#define LINUX_TIOCNOTTY 0x5422 /* -> macOS TIOCNOTTY (same semantics) */
356357
#define LINUX_TIOCGSID 0x5429 /* -> macOS TIOCGSID (same semantics) */
357358
/* termios2 variant (adds c_ispeed/c_ospeed) */

src/syscall/fs.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,29 @@ int64_t sys_fcntl(guest_t *g, int fd, int cmd, uint64_t arg)
801801
host_fd_ref_close(&host_ref);
802802
return 0;
803803
}
804+
case 8: /* F_SETOWN */
805+
case 15: /* F_SETOWN_EX */
806+
/* SIGIO/SIGURG delivery owner. nginx's ngx_spawn_process pairs
807+
* ioctl(FIOASYNC) with fcntl(F_SETOWN) on the channel socket before
808+
* fork() and aborts the worker (NGX_INVALID_PID) if either fails.
809+
* elfuse does not deliver host SIGIO into the guest (see LINUX_FIOASYNC
810+
* in sys_ioctl), so no owner is tracked: accept the request as a
811+
* no-op. */
812+
return 0;
813+
case 9: /* F_GETOWN */
814+
/* No owner tracked; report none. */
815+
return 0;
816+
case 16: { /* F_GETOWN_EX */
817+
/* glibc implements fcntl(F_GETOWN) on top of F_GETOWN_EX, so this must
818+
* answer coherently with the F_SETOWN no-op above rather than EINVAL
819+
* (which would make F_GETOWN fail under glibc). Report "owned by no
820+
* specific process": struct f_owner_ex { int type; int pid; } with
821+
* type=F_OWNER_PID(1), pid=0. */
822+
int32_t owner_ex[2] = {1 /* F_OWNER_PID */, 0 /* pid */};
823+
if (guest_write_small(g, arg, owner_ex, sizeof(owner_ex)) < 0)
824+
return -LINUX_EFAULT;
825+
return 0;
826+
}
804827
case 1024: /* F_GETPIPE_SZ */
805828
/* macOS does not support pipe size queries; return default 64KiB */
806829
return 65536;

src/syscall/io.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,6 +1688,25 @@ int64_t sys_ioctl(guest_t *g, int fd, uint64_t request, uint64_t arg)
16881688
return 0;
16891689
}
16901690

1691+
case LINUX_FIOASYNC: {
1692+
/* Set/clear O_ASYNC (SIGIO-driven I/O). nginx's ngx_spawn_process arms
1693+
* this on the master's channel socket right before fork() and treats a
1694+
* failure as fatal (ngx_close_channel + return NGX_INVALID_PID), so
1695+
* answering ENOTTY here aborts worker spawning entirely -- the master is
1696+
* left with no workers and accepted connections are never serviced.
1697+
* elfuse does not forward host SIGIO into the guest, and nginx workers
1698+
* receive both client I/O and channel commands via epoll rather than
1699+
* SIGIO, so accept the request as a no-op: read the int arg for EFAULT
1700+
* parity and report success without arming host async delivery. */
1701+
int32_t on = 0;
1702+
if (guest_read_small(g, arg, &on, sizeof(on)) < 0) {
1703+
host_fd_ref_close(&host_ref);
1704+
return -LINUX_EFAULT;
1705+
}
1706+
host_fd_ref_close(&host_ref);
1707+
return 0;
1708+
}
1709+
16911710
default:
16921711
host_fd_ref_close(&host_ref);
16931712
return -LINUX_ENOTTY;

tests/manifest.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ test-epoll
6464
test-epoll-edge
6565
test-timerfd
6666
test-large-io-boundary
67+
test-ioctl-fioasync
6768

6869
[section] /proc and /dev emulation tests
6970
test-proc

tests/test-ioctl-fioasync.c

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/* FIOASYNC ioctl + F_SETOWN/F_GETOWN fcntl regression test
2+
*
3+
* Copyright 2026 elfuse contributors
4+
* SPDX-License-Identifier: Apache-2.0
5+
*
6+
* nginx's ngx_spawn_process arms the master->worker channel socket with
7+
* ioctl(FIOASYNC) immediately followed by fcntl(F_SETOWN), right before fork(),
8+
* and treats a failure of EITHER as fatal: it logs an alert, ngx_close_channel,
9+
* and returns NGX_INVALID_PID -- so it never forks the worker. elfuse used to
10+
* answer FIOASYNC with ENOTTY and F_SETOWN with EINVAL, which silently left the
11+
* master with zero workers: the listen socket still accepted connections at the
12+
* host kernel, but nothing in the guest ever accept()ed them, so every request
13+
* hung. This test pins the fix by replaying that pre-fork channel arming.
14+
*
15+
* elfuse does not forward host SIGIO into the guest, and nginx workers receive
16+
* client I/O and channel commands via epoll rather than SIGIO, so both calls
17+
* are accepted as no-ops that report success (F_GETOWN reports "no owner", 0).
18+
*
19+
* Syscalls exercised: socketpair(199), socket(198), ioctl(29) FIOASYNC,
20+
* fcntl(25) F_SETOWN/F_GETOWN, getpid(172), close(57)
21+
*/
22+
23+
#include <fcntl.h>
24+
#include <sys/ioctl.h>
25+
#include <sys/socket.h>
26+
#include <unistd.h>
27+
28+
#include "test-harness.h"
29+
30+
#ifndef FIOASYNC
31+
#define FIOASYNC 0x5452
32+
#endif
33+
34+
int passes = 0, fails = 0;
35+
36+
/* Replay nginx's ngx_spawn_process async/owner arming on a single fd. */
37+
static void check_async_owner(int fd, const char *what)
38+
{
39+
char label[80];
40+
int on = 1;
41+
42+
snprintf(label, sizeof(label), "%s: ioctl(FIOASYNC) enable -> 0", what);
43+
TEST(label);
44+
EXPECT_EQ(ioctl(fd, FIOASYNC, &on), 0, "FIOASYNC enable rejected");
45+
46+
snprintf(label, sizeof(label), "%s: fcntl(F_SETOWN) -> 0", what);
47+
TEST(label);
48+
EXPECT_EQ(fcntl(fd, F_SETOWN, getpid()), 0, "F_SETOWN rejected");
49+
50+
/* F_GETOWN reports the owner; elfuse tracks none, so 0 (no error). glibc
51+
* may probe F_GETOWN_EX first and fall back to plain F_GETOWN on EINVAL --
52+
* either way the visible result must not be a failure. */
53+
snprintf(label, sizeof(label), "%s: fcntl(F_GETOWN) -> not an error", what);
54+
TEST(label);
55+
EXPECT_TRUE(fcntl(fd, F_GETOWN) >= 0, "F_GETOWN returned an error");
56+
57+
on = 0;
58+
snprintf(label, sizeof(label), "%s: ioctl(FIOASYNC) disable -> 0", what);
59+
TEST(label);
60+
EXPECT_EQ(ioctl(fd, FIOASYNC, &on), 0, "FIOASYNC disable rejected");
61+
}
62+
63+
int main(void)
64+
{
65+
printf("test-ioctl-fioasync: FIOASYNC ioctl + F_SETOWN/F_GETOWN fcntl\n");
66+
67+
/* nginx's channel is an AF_UNIX SOCK_STREAM socketpair; FIOASYNC/F_SETOWN
68+
* are applied to channel[0] (nginx also marks it non-blocking via FIONBIO
69+
* first, which this test omits to stay focused on the calls added here). */
70+
int sp[2];
71+
TEST("socketpair(AF_UNIX, SOCK_STREAM)");
72+
EXPECT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sp), 0, "socketpair failed");
73+
74+
check_async_owner(sp[0], "unix socketpair");
75+
76+
close(sp[0]);
77+
close(sp[1]);
78+
79+
/* A plain TCP socket too -- the same family as nginx's listen sockets. */
80+
int s = socket(AF_INET, SOCK_STREAM, 0);
81+
TEST("socket(AF_INET, SOCK_STREAM)");
82+
EXPECT_TRUE(s >= 0, "socket failed");
83+
if (s >= 0) {
84+
check_async_owner(s, "tcp socket");
85+
close(s);
86+
}
87+
88+
SUMMARY("test-ioctl-fioasync");
89+
return fails > 0 ? 1 : 0;
90+
}

0 commit comments

Comments
 (0)