You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
xtool dev build dies with a fatal SwiftNIO error when stdout is /dev/null #268
xtool dev build with stdout redirected to /dev/null dies immediately, before doing any work, with a fatal error rather than a message:
NIOPosix/BaseSocketChannel.swift:1018: Fatal error: 'try!' expression unexpectedly raised an error: epoll_ctl(epfd:op:fd:event:): Operation not permitted) (errno: 1)
Current stack trace:
0 ... <unavailable> + 11213777
...
Reproduction
xtool dev build >/dev/null 2>&1 # crashes, every time
xtool dev build 2>&1 | cat # fine — builds normally
xtool dev build > build.log 2>&1 # fine
xtool dev build # fine in a terminal
script -qec 'xtool dev build' /dev/null # fine (a pty)
So it is specifically /dev/null, not redirection in general: pipes, regular files and ptys all work.
dev duplicates stdio and registers both descriptors with NIO's epoll. /dev/null's file_operations has no poll method, so epoll_ctl answers EPERM for it — and the try! in BaseSocketChannel.swift:1018 converts that into a fatal error. The duplicate of fd 0 registers fine; the duplicate of fd 1 is the one that fails.
Environment
xtool 1.20.0, xtool-x86_64.AppImage (also reproduces on 1.19.2)
Ubuntu 24.04.4 LTS, x86_64, kernel 6.8
Swift 6.4 release toolchain via swiftly
No seccomp filter, AppArmor unconfined — verified with /proc/self/status
Impact
>/dev/null is ordinary in scripts and CI ("run it, I only want the exit code"), and the failure is a crash with a stack trace rather than a diagnosis. Handling the registration failure, or not registering a non-pollable descriptor, would be enough — I'm happy to test a patch.
Seems like this happens when xtool tries to ask for input with Console.prompt. It's the "Path to Xcode.xip:" prompt when the SDK hasn't yet been installed. Still worth fixing but it seems like it's only in specific cases.
Does this happen for you even after you complete SDK setup?
Console.prompt always duplicated standard input and output into NIOPipeBootstrap, which registers those descriptors with epoll. Linux epoll_ctl returns EPERM for /dev/null, and SwiftNIO turns that failure into a fatal error. isatty and poll still leave /dev/null on the NIO path: other non-ttys already succeed there, and poll reports /dev/null as ready.
prompt takes explicit input and output descriptors and uses the NIO bootstrap only when isEpollRegisterable returns true for both.
Additional trigger: xtool new with no project name
Hit the same crash from a different entry point while setting up xtool, which suggests this is not specific to the .xip path prompt — any Console.prompt call can reach it.
$ xtool new # no project name
NIOPosix/BaseSocketChannel.swift:1018: Fatal error: 'try!' expression unexpectedly raised an error: epoll_ctl(epfd:op:fd:event:): Operation not permitted) (errno: 1)
→ SIGILL (exit 132), core dumped. Same failure mode as reported here: the trap is on the NIO event-loop thread, in a try! around epoll_ctl returning EPERM.
Isolated by varying only stdio, always with no project name:
stdin
stdout
result
/dev/null
pipe
SIGILL, exit 132 (11/11 runs)
/dev/null
regular file
Error: Operation unsupported, exit 1
empty regular file
pipe
Error: Operation unsupported, exit 1
tty (pty)
tty
Error: I/O on closed channel, exit 1
For contrast, supplying a name under the exact crashing condition succeeds:
So supplying a name makes the crash unreachable, and with no name the crash needs /dev/null on stdin specifically — a regular file or a pty on stdin is handled cleanly.
That is consistent with the mechanism described above: the descriptor duplicated into the NIO bootstrap fails EPOLL_CTL_ADD with EPERM. Worth noting that EPERM here is not a /dev/null speciality — regular files and non-tty character devices fail the same way when registered directly; /dev/null is simply the descriptor that reaches this path in practice.
The Darwin SDK was already installed when this happened, so the failure is not tied to the "SDK not yet installed" state — it is about the stdio descriptors.
Ruled out on my side: OOM (6 GB free, 26 GB swap free), seccomp (Seccomp: 0, Seccomp_filters: 0), container (systemd-detect-virt: none), and cwd/filesystem.
The change in #304 goes through the shared Console.prompt, so it should cover this path as well — noting it here in case it is useful for the fix's scope.
Thanks, this makes the scope clear: this is a general Console.prompt/stdio issue, not an SDK-installation edge case. The /dev/null stdin plus pipe stdout case points to the NIO epoll-registration path, while supplying a project name simply avoids prompting. Since #304 goes through shared Console.prompt, it should cover xtool new too.
Activity
kabiroberai commentedon Sep 22, 2026
Seems like this happens when xtool tries to ask for input with
Console.prompt. It's the "Path to Xcode.xip:" prompt when the SDK hasn't yet been installed. Still worth fixing but it seems like it's only in specific cases.Does this happen for you even after you complete SDK setup?
mvanhorn commentedon Oct 7, 2026
Console.prompt always duplicated standard input and output into NIOPipeBootstrap, which registers those descriptors with epoll. Linux epoll_ctl returns EPERM for /dev/null, and SwiftNIO turns that failure into a fatal error. isatty and poll still leave /dev/null on the NIO path: other non-ttys already succeed there, and poll reports /dev/null as ready.
prompt takes explicit input and output descriptors and uses the NIO bootstrap only when isEpollRegisterable returns true for both.
HadesX23 commentedon Oct 15, 2026
Additional trigger:
xtool newwith no project nameHit the same crash from a different entry point while setting up xtool, which suggests this is not specific to the
.xippath prompt — anyConsole.promptcall can reach it.→
SIGILL(exit 132), core dumped. Same failure mode as reported here: the trap is on the NIO event-loop thread, in atry!aroundepoll_ctlreturningEPERM.Isolated by varying only stdio, always with no project name:
/dev/null/dev/nullError: Operation unsupported, exit 1Error: Operation unsupported, exit 1Error: I/O on closed channel, exit 1For contrast, supplying a name under the exact crashing condition succeeds:
So supplying a name makes the crash unreachable, and with no name the crash needs
/dev/nullon stdin specifically — a regular file or a pty on stdin is handled cleanly.That is consistent with the mechanism described above: the descriptor duplicated into the NIO bootstrap fails
EPOLL_CTL_ADDwithEPERM. Worth noting thatEPERMhere is not a/dev/nullspeciality — regular files and non-tty character devices fail the same way when registered directly;/dev/nullis simply the descriptor that reaches this path in practice.The Darwin SDK was already installed when this happened, so the failure is not tied to the "SDK not yet installed" state — it is about the stdio descriptors.
Ruled out on my side: OOM (6 GB free, 26 GB swap free), seccomp (
Seccomp: 0,Seccomp_filters: 0), container (systemd-detect-virt: none), and cwd/filesystem.Environment: xtool 1.22.2 (AppImage), Omarchy 4.0.4 (Arch-based), x86_64, kernel 7.2.5.
The change in #304 goes through the shared
Console.prompt, so it should cover this path as well — noting it here in case it is useful for the fix's scope.mvanhorn commentedon Oct 17, 2026
Thanks, this makes the scope clear: this is a general
Console.prompt/stdio issue, not an SDK-installation edge case. The/dev/nullstdin plus pipe stdout case points to the NIO epoll-registration path, while supplying a project name simply avoids prompting. Since #304 goes through sharedConsole.prompt, it should coverxtool newtoo.