Bug 292282 - NFS Shows Empty Filenames Under Linux Emulation
Summary: NFS Shows Empty Filenames Under Linux Emulation
Status: Closed FIXED
Alias: None
Product: Base System
Classification: Unclassified
Component: kern (show other bugs)
Version: 15.0-RELEASE
Hardware: amd64 Any
: --- Affects Many People
Assignee: Rick Macklem
URL:
Keywords: regression
Depends on:
Blocks: 247219
  Show dependency treegraph
 
Reported: 2026-01-08 18:14 UTC by Jerry Williams
Modified: 2026-02-21 21:57 UTC (History)
4 users (show)

See Also:
rmacklem: mfc-stable15+


Attachments
Modify linux_getdents() to skip invalid dir entries (8.09 KB, patch)
2026-01-11 22:42 UTC, Rick Macklem
no flags Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Jerry Williams 2026-01-08 18:14:59 UTC
Listing NFS directories from Linux-emulated binaries causes directory entries with zero-length filenames to show up. These entries do not show when listing from native binaries.

Ex:
~$ /compat/linux/bin/ls -aQ /share
""  ""  ""  ""  ""  ""  ""  "."   "other files"
""  ""  ""  ""  ""  ""  ""  ".."  "etc"

This is new behaviour in FreeBSD 15.0-RELEASE not present in 14.3-RELEASE.
This is present on NFSv3 and NFSv4.
NFS hosts tried: FreeBSD 14.3 and 15.0.
Comment 1 Martin Cracauer 2026-01-08 23:42:53 UTC
I think the root cause is that FreeBSD's getdirentries(2) can fill the d_off field with 0 when operating in the NFS client, and the linux_getdents64(2) on top doesn't understand that.
Comment 2 Martin Cracauer 2026-01-08 23:45:37 UTC
Problem seems to be that getdents64(2) returns 26 instead of 12 entries.

openat(AT_FDCWD, "/mnt/tmp", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
getdents64(3, 0x104a550 /* 26 entries */, 32768) = 768

The correct number of entries for my specific directory is 12.
Comment 3 Martin Cracauer 2026-01-08 23:50:21 UTC
For more fun try:

/compat/linux/bin/find /mnt/tmp


Side notes:
- bug present in 16-current
- bug also applies when the server is Linux
Comment 4 Martin Cracauer 2026-01-09 01:50:14 UTC
(In reply to Jerry Williams from comment #0)
>
> not present in 14.3-RELEASE.

For me the problem appears in 14.2-STABLE.
Comment 5 Martin Cracauer 2026-01-10 03:45:27 UTC
Hmmmm...

openat(AT_FDCWD, "/mnt/tmp", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
getdents64(3, 0x104a550 /* 26 entries */, 32768) = 768

So that is exactly 32768 for the return value (the total number of bytes filled in with the structures of variable length).

That is probably not a coincidence.
Comment 6 Rick Macklem freebsd_committer freebsd_triage 2026-01-10 03:55:13 UTC
(In reply to Martin Cracauer from comment #5)
The NFS client always returns a full block as
requested by the syscall.

The block is filled out with entries of 512bytes
with d_fileno == 0.  On FreeBSD (and 4BSD for that
matter), entries with d_fileno/d_ino == 0 are ignored.

I suspect that Linux does not ignore these entries,
but it is just a hunch.

Note that getdents(2) is not a POSIX standard, so
it is not surprising that it is not Linux compatible.

If there is some other way Linux marks directory
entries to be ignored, then the syscall emulator
could translate them.
Otherwise, I do not see a way to fix this.
I do not know what the Linux syscall is expected
to return. (You cannot just throw away ignored
entries. Doing that will break the offset of the
next block.)

I won't bore you with the details, but on FreeBSD
the NFS blocks are cached in the buffer cache and
creating a short one as the last block results in
breakage of the caching. (I patched it that way
once, but someone spotted that the cache hit rate
dropped significantly.)
Comment 7 Martin Cracauer 2026-01-10 06:03:45 UTC
(In reply to Martin Cracauer from comment #5)

Disregard that comment, didn't look in the right place.
Comment 8 Martin Cracauer 2026-01-10 17:56:22 UTC
(In reply to Rick Macklem from comment #6)

Why can't I throw out to-be-ignored entries? Isn't it that they can only appear at the end and hence the stream position doesn't matter anymore?
Comment 9 Rick Macklem freebsd_committer freebsd_triage 2026-01-10 21:10:44 UTC
(In reply to Martin Cracauer from comment #8)
They can exist in the middle. The tradition (from
UFS literally forever) is to expand the d_reclen
field to assume free'd entries.
However, this does not work for the first entry
in a block. For the first entry in a block, mark
it "invalid" by setting d_fileno (used to be called d_ino)
to 0 (0 was never a valid inode#).

NFS, the second type of file system for 4BSD, followed
the same mechanism.

As I mentioned, I did turn off expanding the last block
to a full blocksize (8K at the moment) and committed the
patch. bde@ noted that this broke directory block caching,
with the cache miss rate increasing dramatically.
I cannot remember why, but I reverted the patch that took
it out.

There is no reason why the Linux glue cannot take them out,
since it appears to be broken now, anyhow.

I can also tell you that the NFS readdir client code can
easily be broken. (Disabling readahead exposes a race,
to give you one example. Maintaining the correct directory
offset cookies is not easy. As such, I have no urge to
try and change it, to make it somehow "Linux compatible".)

I also wonder what happens when Linux apps use the POSIX
opendir(), readdir() function calls instead of the non-standard
syscall.
--> Someone could test this. Just write a little program on
    Linux that uses opendir(), readdir() and then try the
    binary on FreeBSD and see if it breaks?

Note that I do not know if these entries with d_fileno == 0
are causing the breakage. It is just a hunch.
I am not familiar with what the semantics of their getdents()
syscall is, nor do I use the Linuxolator.
Comment 10 Jerry Williams 2026-01-11 01:33:44 UTC
(In reply to Rick Macklem from comment #9)

Using opendir() still exhibits the behaviour.
I didn't know if uploading a binary was appropriate here, so I posted it here https://codeberg.org/jwillia3/292282

test.c:

    #include <dirent.h>
    #include <stdio.h>

    int main(int argc, char **argv)
    {
        DIR     *dirp;
        struct dirent *ent;
        int     n;

        n = 0;
        dirp = opendir(argv[1]);
        while ((ent = readdir(dirp))) {
            printf("%-8d  %s\n", (int) ent->d_fileno, ent->d_name);
            n++;
        }
        closedir(dirp);

        printf("%d entries\n", n);
    }

transcript:

        ~$ doas -s
        # pkg install linux-rl9-devtools
        Updating FreeBSD-ports repository catalogue...
        FreeBSD-ports repository is up to date.
        Updating FreeBSD-ports-kmods repository catalogue...
        FreeBSD-ports-kmods repository is up to date.
        All repositories are up to date.
        The following 1 package(s) will be affected (of 0 checked):

        New packages to be INSTALLED:
                linux-rl9-devtools: 9.6_1 [FreeBSD-ports]

        Number of packages to be installed: 1

        The process will require 280 MiB more space.
        86 MiB to be downloaded.

        Proceed with this action? [Y/n]:
        [1/1] Fetching linux-rl9-devtools-9.6_1~5a97eb46b1.pkg: 100%   86 MiB  45.0MB/s    00:02
        Checking integrity... done (0 conflicting)
        [1/1] Installing linux-rl9-devtools-9.6_1...
        [1/1] Extracting linux-rl9-devtools-9.6_1: 100%
        # ^D
        ~$ /compat/linux/bin/bash
        bash-5.1$ cc --version
        cc (GCC) 11.5.0 20240719 (Red Hat 11.5.0-5)
        Copyright (C) 2021 Free Software Foundation, Inc.
        This is free software; see the source for copying conditions.  There is NO
        warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

        bash-5.1$ cc $HOME/test.c -o linuxtest
        bash-5.1$ file linuxtest
        linuxtest: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=94f5823b0f59ead9e040aa3282d54e2e76c7bb6f, for GNU/Linux 3.2.0, not stripped

        bash-5.1$ ./linuxtest /mnt/tmp
        2964769   .
        2         ..
        2964770   emptyfile
        2964771   test.c
        2964772   test
        2964773   a.out
        0
        0
        0
        0
        0
        0
        0
        0
        0
        0
        0
        0
        0
        0
        0
        21 entries
Comment 11 Rick Macklem freebsd_committer freebsd_triage 2026-01-11 22:42:32 UTC
Created attachment 267048 [details]
Modify linux_getdents() to skip invalid dir entries

You can try this patch.  Note that it is
COMPLETELY UNTESTED!!

I do not have any Linux system to create
a Linux binary with handy.

I also do not maintain the Linuxolater,
so I do not know if fix is appropriate,
even if it works?
Comment 12 Jerry Williams 2026-01-12 02:52:02 UTC
(In reply to Rick Macklem from comment #11)

That does seem to work. Thanks!
Comment 13 commit-hook freebsd_committer freebsd_triage 2026-02-07 22:15:28 UTC
A commit in branch main references this bug:

URL: https://cgit.FreeBSD.org/src/commit/?id=110f2567cb51f1eeddbd5d9937000ad64f6dc746

commit 110f2567cb51f1eeddbd5d9937000ad64f6dc746
Author:     Rick Macklem <rmacklem@FreeBSD.org>
AuthorDate: 2026-02-07 22:12:55 +0000
Commit:     Rick Macklem <rmacklem@FreeBSD.org>
CommitDate: 2026-02-07 22:12:55 +0000

    linux_file.c: Fix handling of NFS getdents() emulation

    Bugzilla PR#292282 reports a problem, where a Linux
    binary running in the Linuxulator gets bogus entries
    in a readdir()/getdents() reply when the directory is
    an NFS mount.

    This appears to be caused by the NFS client including
    entries with d_fileno == 0, which are always ignored by
    BSD, but are not ignored by Linux.

    This patch filters out the "d_fileno == 0" entries and
    the reporter of the bugzilla PR notes that it fixes the
    problem for him.

    It could be argued that the NFS client should filter out
    the "d_fileno == 0" entries, but the NFS client readdir
    code is "fragile" and any change to it runs a significant
    risk of causing regression type problems.

    As such, since the LInuxulator is already broken for this
    case, it seems safer to filter them out there.

    PR:     292282
    Tested by:      Jerry Williams <jwillia3@proton.me>
    Reviewed by:    markj
    MFC after:      2 weeks
    Differential Revision:  https://reviews.freebsd.org/D54679

 sys/compat/linux/linux_file.c | 178 ++++++++++++++++++++++++++----------------
 1 file changed, 110 insertions(+), 68 deletions(-)
Comment 14 Rick Macklem freebsd_committer freebsd_triage 2026-02-07 22:17:26 UTC
The patch has been committed to main and will be MFC'd.
Comment 15 commit-hook freebsd_committer freebsd_triage 2026-02-21 17:05:48 UTC
A commit in branch stable/15 references this bug:

URL: https://cgit.FreeBSD.org/src/commit/?id=86a6407d028d9a58c93c7d1ac54e737d78b1aaaf

commit 86a6407d028d9a58c93c7d1ac54e737d78b1aaaf
Author:     Rick Macklem <rmacklem@FreeBSD.org>
AuthorDate: 2026-02-07 22:12:55 +0000
Commit:     Rick Macklem <rmacklem@FreeBSD.org>
CommitDate: 2026-02-21 17:03:38 +0000

    linux_file.c: Fix handling of NFS getdents() emulation

    Bugzilla PR#292282 reports a problem, where a Linux
    binary running in the Linuxulator gets bogus entries
    in a readdir()/getdents() reply when the directory is
    an NFS mount.

    This appears to be caused by the NFS client including
    entries with d_fileno == 0, which are always ignored by
    BSD, but are not ignored by Linux.

    This patch filters out the "d_fileno == 0" entries and
    the reporter of the bugzilla PR notes that it fixes the
    problem for him.

    It could be argued that the NFS client should filter out
    the "d_fileno == 0" entries, but the NFS client readdir
    code is "fragile" and any change to it runs a significant
    risk of causing regression type problems.

    As such, since the LInuxulator is already broken for this
    case, it seems safer to filter them out there.

    PR:     292282

    (cherry picked from commit 110f2567cb51f1eeddbd5d9937000ad64f6dc746)

 sys/compat/linux/linux_file.c | 178 ++++++++++++++++++++++++++----------------
 1 file changed, 110 insertions(+), 68 deletions(-)
Comment 16 Rick Macklem freebsd_committer freebsd_triage 2026-02-21 21:57:49 UTC
Patch has been committed and MFC'd.