|
| 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