Skip to content

fix(local_opendir): clear stale errno before the EINTR check - #5156

Open
KuzinAndrey wants to merge 1 commit into
MidnightCommander:masterfrom
KuzinAndrey:fix-local-opendir-stale-errno
Open

KuzinAndrey wants to merge 1 commit into
MidnightCommander:masterfrom
KuzinAndrey:fix-local-opendir-stale-errno

Conversation

@KuzinAndrey

Copy link
Copy Markdown
Contributor

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

  • I have referenced the issue(s) resolved by this PR (if any)
  • I have signed-off my contribution with git commit --amend -s
  • Lint and unit tests pass locally with my changes (make indent && make check)
  • I have added tests that prove my fix is effective or that my feature works
  • I have added the necessary documentation (if appropriate)

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>
@github-actions github-actions Bot added needs triage Needs triage by maintainers prio: medium Has the potential to affect progress labels Sep 16, 2026
@github-actions github-actions Bot added this to the Future Releases milestone Sep 16, 2026
Comment thread src/vfs/local/local.c
return NULL;

errno = 0;
if (readdir (dir) == NULL && errno == EINTR)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw, the NULL && !EINTR case should be handled in a separate path, as the rewinddir() obviously makes no sense then.

@ossilator

Copy link
Copy Markdown
Contributor

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.

@KuzinAndrey

Copy link
Copy Markdown
Contributor Author

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 readdir reveals a common recommendation: reset errno to zero before making the call. The existing code failed to do this.

@ossilator

Copy link
Copy Markdown
Contributor

A quick Google search on using readdir reveals a common recommendation: reset errno to zero before making the call. The existing code failed to do this.

yes, exactly. that's all that is to say about this. the long analysis how it actually happens is entirely irrelevant.

@zyv zyv added area: vfs Virtual File System support and removed needs triage Needs triage by maintainers labels Sep 18, 2026
@zyv zyv modified the milestones: Future Releases, 4.9.0 Sep 18, 2026

@zyv zyv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/vfs/local/local.c
if (dir == NULL)
return NULL;

errno = 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
errno = 0;
// Reset errno before readdir to avoid reading a stale EINTR (#5156)
errno = 0;

@KuzinAndrey

Copy link
Copy Markdown
Contributor Author

I don't have time right now to make immediate changes; the @ossilator is also flagging issues in the loop involving the rewinddir call, so the logic for this entire section might need rethinking. You can amend the commit if these fixes are important, or I can try to rework the algorithm entirely later on.

@zyv

zyv commented Sep 18, 2026

Copy link
Copy Markdown
Member

I can try to rework the algorithm entirely later on.

I'm happy to wait unless Andrew wants to take care of this immediately. Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: vfs Virtual File System support prio: medium Has the potential to affect progress

Development

Successfully merging this pull request may close these issues.

3 participants