Skip to content

chmod -R: raw fchmodat2 syscall bypasses libc, breaking LD_PRELOAD tools #14028

Closed

Description

@nadzyah

chmod -R changes entries below its command-line argument with a raw syscall(SYS_fchmodat2, ...) instead of a libc call, so LD_PRELOAD tools (fakeroot, fakechroot, pseudo) never see it. The mode changes on disk, but their bookkeeping does not, so later stat() calls report the old mode. Exit status is 0 and stderr is empty, so the loss is silent

This can be reproduced by running the following commands using uutils 0.10.0, fakeroot 1.37.1.2, Linux 6.16:

mkdir -p d/sub
fakeroot sh -c 'chown -R daemon:daemon d && chmod -R u+s d && find d -printf "%M %p\n" | sort'

GNU coreutils:

drwsrwxr-x d
drwsrwxr-x d/sub

uutils:

drwsrwxr-x d
drwxrwxr-x d/sub     # setuid bit lost

The cause, as I have found, is that DirFd::chmod_at in src/uucore/src/lib/features/safe_traversal.rs calls libc::syscall(452, ...) directly. The command-line argument itself is unaffected because it goes through fs::set_permissions -> chmod()

The raw syscall is #[cfg]-gated to asm-generic arches (x86_64, x86, arm, aarch64, riscv), so it does not reproduce on e.g. ppc64el or s390x, which take the fchmodat fallback. Introduced by #11918

Same class as #13399, different call site (that one is rustix in id, this one is a hand-written libc::syscall in uucore)

Activity

  1. oech3 commented on Aug 19, 2026

    @oech3
    Contributor

    Please fill bug report to tools using LD_PRELOAD. It is actually their bug breaking static musl binaries.

  2. nadzyah commented on Aug 20, 2026

    @nadzyah
    Author

    Please fill bug report to tools using LD_PRELOAD. It is actually their bug breaking static musl binaries.

    I understand the point but please let me first elaborate on why I decided to open the bug specifically here

    fakeroot hooks fchmodat(), which is the libc function for this. GNU coreutils calls it, and it all works.

    glibc doesn't export fchmodat2 as a symbol at all. It's an internal detail of how fchmodat() is implemented on new kernels, not a libc function in its own right. So there's nothing there for fakeroot to hook, and asking LD_PRELOAD tools to fix this means asking them to intercept a raw syscall number, per arch, because one program went around the API

    And the bypass gains nothing, as fchmodat(fd, name, mode, AT_SYMLINK_NOFOLLOW) on glibc issues the exact same fchmodat2 syscall. GNU chmod -R and uutils chmod -R produce identical straces. The only difference is that GNU goes through the symbol, and uutils doesn't

    (chmod_at uses libc::syscall, so it's already in libc, and static musl can't interpose that any more than fchmodat)

    Moreover, the fix on the uutils side would be small, as it can call fchmodat() first and fall back to the raw syscall only on EOPNOTSUPP/ENOSYS. musl behaviour stays unchanged

    The impact is that this fails silently. The chmod succeeds, the exit code is 0, nothing on stderr, so the tool just never sees the change and the resulting file permissions are wrong

  3. oech3 commented on Aug 20, 2026

    @oech3
    Contributor
  4. nadzyah commented on Aug 20, 2026

    @nadzyah
    Author

    Imagine what happen if glibc added support for new syscall for some reason e.g. security before fixing fakeroot. It is new version of fakeroot's role if new syscall does same things. Rust cannot guarantee matching all calls with C. (It would be nightmare if we re-write everything in C (libc crate) and remove std to support LD_PRELOAD'ing all functions.)

    I've done some research, and actually that case already happened, and it went the other way. glibc moved fchmodat() onto the new syscall (in commit 65341f7bbea824d2ff9d37db15d8be162df42bd3)

    "linux: Use fchmodat2 on fchmod for flags different than 0 (BZ 26401)" (in sysdeps/unix/sysv/linux/fchmodat.c):

    int
    fchmodat (int fd, const char *file, mode_t mode, int flag)
    {
    #if __ASSUME_FCHMODAT2
      return INLINE_SYSCALL_CALL (fchmodat2, fd, file, mode, flag);
    #else
      if (flag == 0)
        return INLINE_SYSCALL_CALL (fchmodat, fd, file, mode);
    
      int r = INLINE_SYSCALL_CALL (fchmodat2, fd, file, mode, flag);
      if (r != 0 && errno == ENOSYS)
        return fchmodat_fallback (fd, file, mode, flag);
      return r;
    #endif
    }

    fakeroot needed no change for that, because it hooks the symbol, not the syscall. So that scenario argues for actually going through libc, not against it :)

    It's also the same fchmodat2-first-then-ENOSYS-fallback that chmod_at implements by hand as glibc already does it behind fchmodat()

    And this isn't about rewriting in C or dropping std. std already calls libc here: https://github.com/rust-lang/rust/blob/f7d782a3be46d6bb4b9792fe69a61db389ba1769/library/std/src/sys/fs/unix.rs#L1865-L1867 which is why chmod's top-level path works under fakeroot and only chmod_at doesn't

  5. oech3 commented on Aug 21, 2026

    @oech3
    Contributor

    fakeroot needed no change for that, because it hooks the symbol, not the syscall. So that scenario argues for actually going through libc, not against it :)

    No. fakeroot is using a wrong tool: LD_PRELOAD. fakeroot itself should be updated to use ptrace in any cases.
    Rust can remove dependency on glibc from std in the future theorically.

  6. julian-klode commented on Aug 24, 2026

    @julian-klode
    Contributor

    As far as Ubuntu is concerned, this is a critical regression, LD_PRELOAD tools are an essential part of distributions.

  7. oech3 commented on Aug 24, 2026

    @oech3
    Contributor

    @julian-klode Please send a fix to fakeroot's upstream to use ptrace or something else. Adding hack to support hack is not ideal and static binaries are still ignored by them.

  8. HackingRepo commented on Aug 24, 2026

    @HackingRepo
    Contributor

    however LD_PRELOAD supporting it is a massive security problem and also perfomance issue, fakeroot needs fix the issue and use proper way

  9. sylvestre commented on Aug 24, 2026

    @sylvestre
    SponsorContributor

    I am evaluating a solution

  10. added a commit that references this issue on Aug 24, 2026
    fc0062b
  11. oech3 commented on Aug 25, 2026

    @oech3
    Contributor

    I still don't want to implicitly assume that Rust does not remove dependency on glibc.
    We don't viorate anything about chmod's role with our impl.

  12. added a commit that references this issue on Aug 25, 2026
    62d40de
  13. added a commit that references this issue on Sep 28, 2026
    ed7bb15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions