Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/syscall/abi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand Down
23 changes: 23 additions & 0 deletions src/syscall/fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: New F_SETOWN/F_GETOWN handlers can return success for closed FDs because they skip closed-descriptor validation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/syscall/fs.c, line 804:

<comment>New F_SETOWN/F_GETOWN handlers can return success for closed FDs because they skip closed-descriptor validation.</comment>

<file context>
@@ -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
</file context>

case 15: /* F_SETOWN_EX */
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: F_SETOWN_EX no-op path skips user-pointer read/validation, so bad guest pointers incorrectly succeed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/syscall/fs.c, line 805:

<comment>F_SETOWN_EX no-op path skips user-pointer read/validation, so bad guest pointers incorrectly succeed.</comment>

<file context>
@@ -801,6 +801,29 @@ int64_t sys_fcntl(guest_t *g, int fd, int cmd, uint64_t arg)
         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
</file context>

/* 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;
Expand Down
19 changes: 19 additions & 0 deletions src/syscall/io.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions tests/manifest.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 90 additions & 0 deletions tests/test-ioctl-fioasync.c
Original file line number Diff line number Diff line change
@@ -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 <fcntl.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>

#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;
}
Loading