fix(local_opendir): clear stale errno before the EINTR check - #5156
KuzinAndrey wants to merge 1 commit into
Conversation
The Ticket MidnightCommander#3987 workaround (27de037) reopens the directory when the first readdir() returns NULL and errno == EINTR, to recover from an interrupted readdir() on CIFS shares with Linux >= 5.1. The check reads a stale errno: readdir() does not set errno when the directory stream is exhausted (man 3 readdir: "errno is not changed"), and errno is a per-thread value that any previously interrupted syscall leaves behind. mc's event loop routinely survives EINTR from select() and read() when the subshell's SIGCHLD or the terminal's SIGWINCH arrives, and success paths such as open, fstat and opendir do not clear it either. With an empty directory and a stale errno == EINTR the condition is true on every iteration -- no syscall in the loop body writes errno on the success path -- so local_opendir() spins forever: opendir, fstat, getdents, closedir, repeat. Observed on a FUSE mount: four FUSE round trips per iteration (~80k/s), ~27% stime in mc and ~34% in the FUSE daemon, until the directory becomes non-empty or the process exits. On a local filesystem the same loop saturates a core with plain syscalls. Set errno = 0 before the readdir() call so that only an EINTR reported by that readdir() itself triggers the reopen. Signed-off-by: Kuzin Andrey <kuzinandrey@yandex.ru>
| return NULL; | ||
|
|
||
| errno = 0; | ||
| if (readdir (dir) == NULL && errno == EINTR) |
There was a problem hiding this comment.
fwiw, the NULL && !EINTR case should be handled in a separate path, as the rewinddir() obviously makes no sense then.
|
looks reasonable. is this code covered by autotests by any chance? then an extension would be in order. i find the commit message somewhat excessive - it doesn't really matter how errno can become non-null outside the strictly controlled local scope; it's an obvious programming error. |
|
I am currently developing a FUSE filesystem for personal use, and I ran into an issue where Midnight Commander would hang when accessing an empty directory. An AI agent identified the problematic spot through static analysis and by examining the state of the hung processes. I want to apply a quick fix without altering the existing codebase unnecessarily. I’m not sure how to write unit tests for this, as it involves a complex combination of system calls and the launching of external processes. The commit message provides a detailed explanation of the situation to ensure there is no ambiguity. A quick Google search on using |
yes, exactly. that's all that is to say about this. the long analysis how it actually happens is entirely irrelevant. |
zyv
left a comment
There was a problem hiding this comment.
Could you please adjust the commit message to start like all other ones in our project:
https://github.com/MidnightCommander/mc/commits/master/
"Ticket #5156: vfs - clear stale errno before checking for EINTR"
I also think that the rest of the commit message can be removed - it carries no additional information. Instead, I'd add a comment before reset to reference this ticket - if needed one can always check it for background information.
Otherwise LGTM, thank you. Unfortunately, we don't have VFS test for local VFS / local_opendir. I would also very much like to see one, but I wouldn't make it a prerequisite for merging this fix.
| if (dir == NULL) | ||
| return NULL; | ||
|
|
||
| errno = 0; |
There was a problem hiding this comment.
| errno = 0; | |
| // Reset errno before readdir to avoid reading a stale EINTR (#5156) | |
| errno = 0; |
|
I don't have time right now to make immediate changes; the @ossilator is also flagging issues in the loop involving the |
I'm happy to wait unless Andrew wants to take care of this immediately. Thank you. |
Proposed changes
The Ticket #3987 workaround (27de037) reopens the directory when the first readdir() returns NULL and errno == EINTR, to recover from an interrupted readdir() on CIFS shares with Linux >= 5.1.
The check reads a stale errno: readdir() does not set errno when the directory stream is exhausted (man 3 readdir: "errno is not changed"), and errno is a per-thread value that any previously interrupted syscall leaves behind. mc's event loop routinely survives EINTR from select() and read() when the subshell's SIGCHLD or the terminal's SIGWINCH arrives, and success paths such as open, fstat and opendir do not clear it either.
With an empty directory and a stale errno == EINTR the condition is true on every iteration -- no syscall in the loop body writes errno on the success path -- so local_opendir() spins forever: opendir, fstat, getdents, closedir, repeat. Observed on a FUSE mount: four FUSE round trips per iteration (~80k/s), ~27% stime in mc and ~34% in the FUSE daemon, until the directory becomes non-empty or the process exits. On a local filesystem the same loop saturates a core with plain syscalls.
Set errno = 0 before the readdir() call so that only an EINTR reported by that readdir() itself triggers the reopen.
Checklist
git commit --amend -smake indent && make check)