Summary
PR #204 raises the host process's RLIMIT_NOFILE so the guest's 1024-entry
FD table is actually reachable, and adds a fixed HOST_FD_RESERVE for
elfuse's own runtime overhead. Three pre-existing host-fd amplification paths
scale with guest behavior instead of being fixed overhead — no constant added
to FD_TABLE_SIZE can bound them. All three reproduce on main too (checked
with a manually-set ulimit -Sn 1088), so they predate #204; #204 just makes
a full 1024-entry guest table reliably reachable for the first time, which is
what makes these paths reachable in practice.
1. Fork duplicates the entire live fd table before sending
fork_ipc_send_fd_table() dup() every live fd_table entry before sending
it over SCM_RIGHTS, closing each dup only after the whole batch is sent.
A fork momentarily needs ~2× the guest's live fd count in host fds.
2. Guest directory fds cost 2 host fds each
Opening a directory allocates one host fd, then clone_dir_stream()
dup()s it again for fdopendir() (same pattern in the fork-restore path). The guest
table advertises 1024 capacity uniformly, but a directory-fd-heavy workload
(recursive walkers, build systems, find) hits the host wall at roughly
half the entries a pipe/file-heavy workload would reach.
3. ppoll/pselect6 hold one host dup per polled fd, for the whole wait — stacks with MAX_THREADS
Every blocking read/write already dups its one fd for the wait's duration
(io_block_wait() → host_fd_ref_open(),. ppoll
and pselect6 do the same once per polled fd, all held simultaneously for the entire
call. ppoll caps nfds at 256; pselect6 covers up to FD_TABLE_SIZE
(1024) bits. With up to MAX_THREADS = 64 (src/runtime/thread.h:30)
concurrent guest threads each potentially in their own poll/select call, this
path's host-fd cost is O(threads × poll-set size)
Aggravating factor common to all three
When the dup underlying host_fd_ref_open()/host_fd_ref_open_io() fails,
the guest-facing syscall returns -EBADF, since the fd itself is
fine — and nothing is logged. This makes every amplifier above present as an
unattributable failure indistinguishable from an app use-after-close bug,
instead of a clear "elfuse ran out of host fd headroom" signal.
Summary
PR #204 raises the host process's
RLIMIT_NOFILEso the guest's 1024-entryFD table is actually reachable, and adds a fixed
HOST_FD_RESERVEforelfuse's own runtime overhead. Three pre-existing host-fd amplification paths
scale with guest behavior instead of being fixed overhead — no constant added
to
FD_TABLE_SIZEcan bound them. All three reproduce onmaintoo (checkedwith a manually-set
ulimit -Sn 1088), so they predate #204; #204 just makesa full 1024-entry guest table reliably reachable for the first time, which is
what makes these paths reachable in practice.
1. Fork duplicates the entire live fd table before sending
fork_ipc_send_fd_table()dup()every livefd_tableentry before sendingit over
SCM_RIGHTS, closing each dup only after the whole batch is sent.A fork momentarily needs ~2× the guest's live fd count in host fds.
2. Guest directory fds cost 2 host fds each
Opening a directory allocates one host fd, then
clone_dir_stream()dup()s it again forfdopendir()(same pattern in the fork-restore path). The guesttable advertises 1024 capacity uniformly, but a directory-fd-heavy workload
(recursive walkers, build systems,
find) hits the host wall at roughlyhalf the entries a pipe/file-heavy workload would reach.
3.
ppoll/pselect6hold one host dup per polled fd, for the whole wait — stacks withMAX_THREADSEvery blocking read/write already dups its one fd for the wait's duration
(
io_block_wait()→host_fd_ref_open(),.ppolland
pselect6do the same once per polled fd, all held simultaneously for the entirecall.
ppollcapsnfdsat 256;pselect6covers up toFD_TABLE_SIZE(1024) bits. With up to
MAX_THREADS = 64(src/runtime/thread.h:30)concurrent guest threads each potentially in their own poll/select call, this
path's host-fd cost is O(threads × poll-set size)
Aggravating factor common to all three
When the dup underlying
host_fd_ref_open()/host_fd_ref_open_io()fails,the guest-facing syscall returns
-EBADF, since the fd itself isfine — and nothing is logged. This makes every amplifier above present as an
unattributable failure indistinguishable from an app use-after-close bug,
instead of a clear "elfuse ran out of host fd headroom" signal.