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
panic: unexpected error during closedir "Bad file descriptor" for unix::fs::Dir #124105
At Zed we see 2-5 panics per week across our userbase from dropping a Dir. It is possible that this issue is macOS specific (as all of our telemetry is from macOS today), but we are unfortunately not able to reproduce this panic internally.
The problem for us is that this assertion sometimes fails with unexpected error during closedir: Os { code: 9, kind: Uncategorized, message: "Bad file descriptor" }.
I expect that the standard library should not panic on situations that do happen in reality. In particular if you are trying to close a bad file descriptor, that seems "fine". For comparison a failure to close a file during the drop of a file descriptor is silently ignored as I would expect.
Not panicking is obviously not ideal; but in the context of a long-running interactive app like a text editor, we want to be able to take the trade off of higher uptime over lower visibility.
For more discussion of possible solutions to fallible drops, #98338 has some good ideas, and I think if any are implemented we should do an equivalent thing for directories too (I particularly like the idea of a manual close that returns a result, and which causes the Drop to become a no-op). In the meantime, I think we should make rust programs crash less by ignoring this failure.
I expect that the standard library should not panic on situations that do happen in reality.
I know you meant something slightly aside from the reading of this, but I do expect the standard library to panic on indexing a slice with its length, or greater than. And people write code like that every day. So clearly the criteria has to be something more like
whether it is reasonable to proceed anyways (arguable?)
whether it is fixable (arguably not, if we get an error as joyously "descriptive" as EBADF)
Fair: I do expect panics on programmer error or in a situation where it's unsafe to continue. I am relatively (but it's obviously hard to be 100%) sure this is not programmer error, and I'm convinced that it'd be just fine if the program kept running (c.f. how this error is ignored when dropping a file descriptor).
The full back-trace is below. All line numbers in Zed code are relative to v0.131.6 and in rust 1.77.1. I'm not sure why the top line number is missed by symbolicate, but the panic message narrows it down to this assertion
I believe the reason this panics at on line 4018 of worktree.rs in Zed's code is that child_paths goes out of scope, which was retaining a ReadDir returned from std::fs::read_dir() (via smol) here. I can't see any shenanigans on this codepath that would lead us to to invalidate the file descriptor, which leads me to believe that "this just happens sometimes" when interacting with the filesystem. (Though we are holding this across async/await, so it seems possible that something else is affecting the system's state, but it's very unclear as to what would cause this kind of a problem).
I am not necessarily opposed to this change, I just felt it would be useful to know a bit more context.
Hmm. I wonder if, in another thread that is not bound by the executor, but does wait until after the worktree code has started up, destroying one of the directories in question would do it? That would make sense, and would explain why we get such a lovely "error" from the OS. That would be a classic case of "force majeure" that we typically do want to ignore, yes.
For comparison a failure to close a file during the drop of a file descriptor is silently ignored as I would expect.
This is different because closing regular files can emit IO errors for which there is no good recovery strategy because it is unclear after calling close whether the FD is already closed successfully or not. The recommended strategy for error handling in those cases is to call sync_all on files first before closing them.
EBADF is very different. It indicates that something closed a file descriptor it did not own, which is a violation of IO safety. Unless you have some evidence that this is just macos weirdness I would expect this to indicate a very serious bug in userspace which can lead to data corruption and UB due to file descriptor confusion.
This error should not be suppressed.
Arguably we should also not silently ignore EBADF for OwnedFd and only suppress other errors such as EIO.
And maybe we should add a fn close(self) -> Result<()> API to allow non-panicking error handling, though even then EBADF shouldn't be silently ignored and the application should be aborted to prevent data from being accidentally clobbered.
Caveat: closedir operates on a DIR*, not on a fd directly. So it's possible that the C library returns EBADF for something that's not an invalid file descriptor but some other invalid state in that wrapping struct.
Is the macos C library open source? Then we can check there. If not it'd require reproducing the issue and tracing syscalls to tell the difference.
It is, though as with everything Apple, it's a mess.
Errors can only occur from the close call, the rest of closedir just does some reclaiming. Most of the kernel is open-source as well, so one could try to figure out if anything except the fd lookup could cause the EBADF, but I'm not motivated enough to do that.
The only place in zed itself which calls as_raw_fd is this code for getting the PID of the child process running in the integrated terminal. While it's definitely very fishy (it's not tied to the lifetime of the PTY), it doesn't close anything so I doubt that this issue is caused there.
As for dependencies, it might help to get a complete backtrace to see which ReadDir::drop specifically is causing this.
I have an idea how to harden std types against these kinds of things, but it would be fairly expensive and imo only make sense for debug_assert builds. Randomization for file descriptor numbers. But I'm not sure if people would be on board with it.
If this replicates on macos and users have some janky fuse drivers that'd be a likely suspect. Though those drivers should have a bug filed against them.
A quick (though inconclusive) update: Although I'm not certain it doesn't reproduce, I was unable to reproduce with the example program (it fails before it gets to the end), and my testing with Zed itself, I could only reproduce libgit2/libgit2#6796, not this error with sshfs.
Still to try: updating the example program to run to completion, and setting up a Samba server to test that one.
I just tried compiling Zed on Linux with the rustc_codegen_cranelift backend. It crashes reliably with:
fatal runtime error: IO Safety violation: owned file descriptor already closed
Thread 1 "Zed" received signal SIGABRT, Aborted.
__pthread_kill_implementation (threadid=<optimized out>, signo=signo@entry=6, no_tid=no_tid@entry=0) at ./nptl/pthread_kill.c:44
44 ./nptl/pthread_kill.c: Bestand of map bestaat niet.
(gdb) bt
#0 __pthread_kill_implementation (threadid=<optimized out>, signo=signo@entry=6, no_tid=no_tid@entry=0) at ./nptl/pthread_kill.c:44
#1 0x00007ffff786fe8f in __pthread_kill_internal (signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:78
#2 0x00007ffff7820fb2 in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#3 0x00007ffff780b472 in __GI_abort () at ./stdlib/abort.c:79
#4 0x0000555562b7e5d1 in std::sys::pal::unix::abort_internal () at library/std/src/sys/pal/unix/mod.rs:366
#5 0x0000555560e37664 in std::sys::pal::unix::fs::debug_assert_fd_is_open () at /home/bjorn/Projects/cg_clif2/build/stdlib/library/std/src/rt.rs:43
#6 0x0000555560dffad6 in std::os::fd::owned::{impl#7}::drop () at /home/bjorn/Projects/cg_clif2/build/stdlib/library/std/src/os/fd/owned.rs:181
#7 0x0000555560e38069 in core::ptr::drop_in_place<std::os::fd::owned::OwnedFd> () at /home/bjorn/Projects/cg_clif2/build/stdlib/library/core/src/ptr/mod.rs:542
#8 0x000055555b0f8dde in core::ptr::drop_in_place<std::sys::pal::unix::fd::FileDesc> () at /home/bjorn/Projects/cg_clif2/build/stdlib/library/core/src/ptr/mod.rs:542
#9 0x000055555b0f928c in core::ptr::drop_in_place<std::sys::pal::unix::process::process_common::Stdio> () at /home/bjorn/Projects/cg_clif2/build/stdlib/library/core/src/ptr/mod.rs:542
#10 0x000055555b0f8856 in core::ptr::drop_in_place<core::option::Option<std::sys::pal::unix::process::process_common::Stdio>> ()
at /home/bjorn/Projects/cg_clif2/build/stdlib/library/core/src/ptr/mod.rs:542
#11 0x000055555b0f930f in core::ptr::drop_in_place<std::sys::pal::unix::process::process_common::Command> () at /home/bjorn/Projects/cg_clif2/build/stdlib/library/core/src/ptr/mod.rs:542
#12 0x000055555b0f8d67 in core::ptr::drop_in_place<std::process::Command> () at /home/bjorn/Projects/cg_clif2/build/stdlib/library/core/src/ptr/mod.rs:542
#13 0x000055555b10be12 in alacritty_terminal::tty::unix::new () at /home/bjorn/.cargo/registry/src/index.crates.io-6f17d22bba15001f/alacritty_terminal-0.23.0/src/tty/unix.rs:285
#14 0x000055555afdc160 in terminal::{impl#7}::new () at crates/terminal/src/terminal.rs:377
#15 0x000055555a49c2fb in project::terminals::{impl#0}::create_terminal () at crates/project/src/terminals.rs:167
Upon further investigation, I found alacritty/alacritty#7996, which fixes the crash I found here. I think it is very likely that the bug this fixes is responsible for the panic described in this issue too.
Reacted by the8472, Ben Kimock, Yerkebulan Tulibergenov and GuoHao
If you can reproduce this reliably, I’d love to debug it together - do you have time to talk the first week of July? If so please reach out to conrad@zed.dev
In my mind there’s quite a chance this is a bug in zed or its dependencies, but it’s also possible that this is a failure introduced by the filesystem.
things I’d like to try:
adding logging to open/close fds to see if we can find a double close
I can confirm that using the current master branch of alacritty-terminal fixes the IO safety crash with rustc_codegen_cranelift. It does still crash, but that is because of some unimplemented things in rustc_codegen_cranelift (rust-lang/rustc_codegen_cranelift#1492), not a bug in Zed or any of it's dependencies.
We've been running the fixed alacritty in preview for a week, and on stable for a few days, and have seen 0 instances of this - this either means we've been unusually lucky, or this is now solved.
Thank you very much to @the8472 for adding the debug asserts, and to @jakobhellermann for figuring out the problem.
Reacted by Jonas Böttiger, bjorn3, Yerkebulan Tulibergenov and GuoHao
I'm fairly sure it's the same underlying problem. debug_assert_fd_is_open in library/std/src/sys/fs/unix.rs does an fcntl(fd, F_GETFD) and aborts on EBADF. It has two callers, OwnedFd::drop and DirStream::drop, and in DirStream::drop it runs immediately before the closedir this issue is about:
let fd = unsafe{ libc::dirfd(self.0)};debug_assert_fd_is_open(fd);let r = unsafe{ libc::closedir(self.0)};assert!(
r == 0 || crate::io::Error::last_os_error().is_interrupted(),"unexpected error during closedir: {:?}",
...
);
deletes that whole tree with remove_dir_all while the stream is still running
The abort is rarer than the panic. I found one occurrence across the CI runs I looked at, whereas the panic recurred often enough that someone added the workaround.
Two things we ruled out on our side:
notify doesn't close any raw file descriptors on this platform. The inotify and kqueue backends are the only ones touching RawFd/OwnedFd and neither is compiled in the macOS FSEvents configuration. The FSEvents backend only holds CoreFoundation objects. The descriptors notify owns here all come from plain std::fs::File.
remove_dir_all's own ownership looks right to me. fdreaddir passes the fd to fdopendir and then calls into_raw_fd on the OwnedFd, so closedir becomes the only closer, and remove_dir_all_recursive just borrows that raw fd for openat and unlinkat. The chmod_harder code that does hold an OwnedFd is cfg'd to Linux and Android.
That leaves something else in the process closing the descriptor. Both reports I know of, this one and the Zed report that opened the issue, are macOS applications using FSEvents.
So, two questions:
Is a stray close() coming from a system framework a known failure mode here? Is there a supported way to instrument it, something like interposing on close or a descriptor guard equivalent to Guard Malloc?
Would a standalone reproducer with no notify dependency be useful, just an FSEventStream over a large tree plus a concurrent remove_dir_all? I can write that if it would help, since I gather this hasn't been reproducible internally so far.
`
Is a stray close() coming from a system framework a known failure mode here?
We have had no such reports.
If there are things that close FDs that they don't own it does not necessarily trigger the abort anywhere near the code or on the file descriptor that's responsible because when the numbers start getting reused it can become hopelessly confused which object owns which FD, especially in concurrent programs.
If you get into this situation you may already be facing data corruption from stray writes into the wrong file descriptors. It is irrecoverable global state corruption, which is why we abort the process.
Is there a supported way to instrument it, something like interposing on close or a descriptor guard equivalent to Guard Malloc?
A macos equivalent of strace is what I'd reach for to find double-closes.
Would a standalone reproducer with no notify dependency be useful, just an FSEventStream over a large tree plus a concurrent remove_dir_all? I can write that if it would help, since I gather this hasn't been reproducible internally so far.
Wrong issue? This is the first time that FSEvents has been brought up here. If you think there's an issue in Rust or want input from us please open a new one.
But sure, a minimal reproducer will help anyone looking into it.
I tracked down the bug and posted a reproducer: https://github.com/daandemeyer/fsevents-fd-repro, nothing to do with rust, but posting it here in case someone else finds this thread while looking into similar issues.
Activity
Ignore errors in Drop for unix::fs::Dir
workingjubilee commentedon Apr 18, 2024
@ConradIrwin Can you discuss the tracebacks for this, or does your telemetry lack sufficiently sophisticated crash reporting to make that possible?
workingjubilee commentedon Apr 18, 2024
I know you meant something slightly aside from the reading of this, but I do expect the standard library to panic on indexing a slice with its length, or greater than. And people write code like that every day. So clearly the criteria has to be something more like
ConradIrwin commentedon Apr 18, 2024
Fair: I do expect panics on programmer error or in a situation where it's unsafe to continue. I am relatively (but it's obviously hard to be 100%) sure this is not programmer error, and I'm convinced that it'd be just fine if the program kept running (c.f. how this error is ignored when dropping a file descriptor).
The full back-trace is below. All line numbers in Zed code are relative to v0.131.6 and in rust 1.77.1. I'm not sure why the top line number is missed by symbolicate, but the panic message narrows it down to this assertion
I believe the reason this panics at on line 4018 of worktree.rs in Zed's code is that
child_pathsgoes out of scope, which was retaining aReadDirreturned fromstd::fs::read_dir()(via smol) here. I can't see any shenanigans on this codepath that would lead us to to invalidate the file descriptor, which leads me to believe that "this just happens sometimes" when interacting with the filesystem. (Though we are holding this across async/await, so it seems possible that something else is affecting the system's state, but it's very unclear as to what would cause this kind of a problem).workingjubilee commentedon Apr 18, 2024
Thank you for sharing!
I am not necessarily opposed to this change, I just felt it would be useful to know a bit more context.
Hmm. I wonder if, in another thread that is not bound by the executor, but does wait until after the worktree code has started up, destroying one of the directories in question would do it? That would make sense, and would explain why we get such a lovely "error" from the OS. That would be a classic case of "force majeure" that we typically do want to ignore, yes.
the8472 commentedon Apr 18, 2024
This is different because closing regular files can emit IO errors for which there is no good recovery strategy because it is unclear after calling close whether the FD is already closed successfully or not. The recommended strategy for error handling in those cases is to call
sync_allon files first before closing them.EBADF is very different. It indicates that something closed a file descriptor it did not own, which is a violation of IO safety. Unless you have some evidence that this is just macos weirdness I would expect this to indicate a very serious bug in userspace which can lead to data corruption and UB due to file descriptor confusion.
This error should not be suppressed.
Arguably we should also not silently ignore EBADF for OwnedFd and only suppress other errors such as EIO.
And maybe we should add a
fn close(self) -> Result<()>API to allow non-panicking error handling, though even thenEBADFshouldn't be silently ignored and the application should be aborted to prevent data from being accidentally clobbered.the8472 commentedon Apr 18, 2024
Caveat:
closediroperates on aDIR*, not on a fd directly. So it's possible that the C library returns EBADF for something that's not an invalid file descriptor but some other invalid state in that wrapping struct.Is the macos C library open source? Then we can check there. If not it'd require reproducing the issue and tracing syscalls to tell the difference.
joboet commentedon Apr 18, 2024
It is, though as with everything Apple, it's a mess.
Errors can only occur from the
closecall, the rest ofclosedirjust does some reclaiming. Most of the kernel is open-source as well, so one could try to figure out if anything except the fd lookup could cause theEBADF, but I'm not motivated enough to do that.the8472 commentedon Apr 18, 2024
Thanks for looking it up. I think that is enough evidence that there's probably some library-UB going on in zed and we shouldn't try to silence it.
joboet commentedon Apr 18, 2024
The only place in
zeditself which callsas_raw_fdis this code for getting the PID of the child process running in the integrated terminal. While it's definitely very fishy (it's not tied to the lifetime of the PTY), it doesn't close anything so I doubt that this issue is caused there.As for dependencies, it might help to get a complete backtrace to see which
ReadDir::dropspecifically is causing this.the8472 commentedon Apr 18, 2024
It's not necessarily readdir that's causing this, it could be the victim of something else going rogue and closing the wrong fd numbers.
joboet commentedon Apr 18, 2024
It must be... Though I'm hoping that there is enough locality between the
closeand theread_dirto point us in the direction.the8472 commentedon Apr 18, 2024
I have an idea how to harden std types against these kinds of things, but it would be fairly expensive and imo only make sense for debug_assert builds. Randomization for file descriptor numbers. But I'm not sure if people would be on board with it.
15 remaining items
the8472 commentedon Apr 18, 2024
If this replicates on macos and users have some janky fuse drivers that'd be a likely suspect. Though those drivers should have a bug filed against them.
ConradIrwin commentedon Apr 26, 2024
A quick (though inconclusive) update: Although I'm not certain it doesn't reproduce, I was unable to reproduce with the example program (it fails before it gets to the end), and my testing with Zed itself, I could only reproduce libgit2/libgit2#6796, not this error with
sshfs.Still to try: updating the example program to run to completion, and setting up a Samba server to test that one.
bjorn3 commentedon May 25, 2024
I just tried compiling Zed on Linux with the rustc_codegen_cranelift backend. It crashes reliably with:
Upon further investigation, I found alacritty/alacritty#7996, which fixes the crash I found here. I think it is very likely that the bug this fixes is responsible for the panic described in this issue too.
ConradIrwin commentedon May 26, 2024
If you can reproduce this reliably, I’d love to debug it together - do you have time to talk the first week of July? If so please reach out to conrad@zed.dev
In my mind there’s quite a chance this is a bug in zed or its dependencies, but it’s also possible that this is a failure introduced by the filesystem.
things I’d like to try:
ConradIrwin commentedon May 26, 2024
Nvm, I just read the attached issue. I’ll pull that into zed and see if it fixes out problems
bjorn3 commentedon May 26, 2024
I can confirm that using the current master branch of alacritty-terminal fixes the IO safety crash with rustc_codegen_cranelift. It does still crash, but that is because of some unimplemented things in rustc_codegen_cranelift (rust-lang/rustc_codegen_cranelift#1492), not a bug in Zed or any of it's dependencies.
alacritty/alacritty#7996 hasn't been published to crates.io yet, so I had to use a git dependency.
ConradIrwin commentedon Jun 14, 2024
We've been running the fixed alacritty in preview for a week, and on stable for a few days, and have seen 0 instances of this - this either means we've been unusually lucky, or this is now solved.
Thank you very much to @the8472 for adding the debug asserts, and to @jakobhellermann for figuring out the problem.
daandemeyer commentedon Jul 25, 2026
I ran into this on notify's CI, except it showed up as an abort rather than the closedir panic:
Full job: https://github.com/notify-rs/notify/actions/runs/30158197016/job/89679165708
I'm fairly sure it's the same underlying problem.
debug_assert_fd_is_openinlibrary/std/src/sys/fs/unix.rsdoes anfcntl(fd, F_GETFD)and aborts on EBADF. It has two callers,OwnedFd::dropandDirStream::drop, and inDirStream::dropit runs immediately before theclosedirthis issue is about:Environment:
The reproducer isn't deterministic, but it is narrow. The test that provokes it does three things:
FSEventStreamCreateplusFSEventStreamScheduleWithRunLoopon a dedicated run loop thread)dir_N/subdir, so 8194 directories, and asks FSEvents to watch all 4097 at once, which fails because a stream seems to cap out around 4096 paths (Filewatching fails silently with >4096 paths fsnotify/fsevents#48)remove_dir_allwhile the stream is still runningThe abort is rarer than the panic. I found one occurrence across the CI runs I looked at, whereas the panic recurred often enough that someone added the workaround.
Two things we ruled out on our side:
notify doesn't close any raw file descriptors on this platform. The inotify and kqueue backends are the only ones touching
RawFd/OwnedFdand neither is compiled in the macOS FSEvents configuration. The FSEvents backend only holds CoreFoundation objects. The descriptors notify owns here all come from plainstd::fs::File.remove_dir_all's own ownership looks right to me.fdreaddirpasses the fd tofdopendirand then callsinto_raw_fdon theOwnedFd, soclosedirbecomes the only closer, andremove_dir_all_recursivejust borrows that raw fd foropenatandunlinkat. Thechmod_hardercode that does hold anOwnedFdis cfg'd to Linux and Android.That leaves something else in the process closing the descriptor. Both reports I know of, this one and the Zed report that opened the issue, are macOS applications using FSEvents.
So, two questions:
close()coming from a system framework a known failure mode here? Is there a supported way to instrument it, something like interposing oncloseor a descriptor guard equivalent to Guard Malloc?remove_dir_all? I can write that if it would help, since I gather this hasn't been reproducible internally so far.`
the8472 commentedon Jul 25, 2026
We have had no such reports.
If there are things that close FDs that they don't own it does not necessarily trigger the abort anywhere near the code or on the file descriptor that's responsible because when the numbers start getting reused it can become hopelessly confused which object owns which FD, especially in concurrent programs.
If you get into this situation you may already be facing data corruption from stray writes into the wrong file descriptors. It is irrecoverable global state corruption, which is why we abort the process.
A macos equivalent of
straceis what I'd reach for to find double-closes.Wrong issue? This is the first time that FSEvents has been brought up here. If you think there's an issue in Rust or want input from us please open a new one.
But sure, a minimal reproducer will help anyone looking into it.
Add a standalone reproducer attempt for rust-lang#124105
daandemeyer commentedon Jul 25, 2026
This is a bug in fsevents, has nothing to do with rust, apologies for the noise
daandemeyer commentedon Jul 25, 2026
I tracked down the bug and posted a reproducer: https://github.com/daandemeyer/fsevents-fd-repro, nothing to do with rust, but posting it here in case someone else finds this thread while looking into similar issues.
RalfJung commentedon Jul 25, 2026
That's at least the 2nd time that this assertion found an actual bug :)
the8472 commentedon Jul 25, 2026
Incomplete list of trophies from searching public github issues:
OwnedFdmultiple times alacritty/alacritty#7996 (this issue that started it)