Skip to content

fix(auxv): report the program, not the loader, as AT_EXECFN - #446

Open
ehfd wants to merge 3 commits into
proot-me:masterfrom
ehfd:execfn-auxv
Open

ehfd wants to merge 3 commits into
proot-me:masterfrom
ehfd:execfn-auxv

Conversation

@ehfd

@ehfd ehfd commented Sep 26, 2026 •

Copy link
Copy Markdown

On Ubuntu 25.10 and later, no coreutils command runs under PRoot. Ubuntu now ships the Rust coreutils (uutils) as one multi-call binary that works out which command it is from AT_EXECFN, and under PRoot that value is the path of PRoot's loader instead of the program. With the v5.4.1 release binary, only Docker is needed to see it:

$ curl -fsSLO https://github.com/proot-me/proot/releases/download/v5.4.1/proot && chmod +x proot
$ docker run --rm -v "$PWD/proot:/proot:ro" ubuntu:26.04 sh -c '
    /proot -r / /usr/bin/dirname /a/b
    /proot -r / /bin/sh -c "ls -d /tmp"
    printf "#!/usr/bin/env sh\necho hello\n" > /tmp/hello && chmod +x /tmp/hello && /proot -r / /tmp/hello'
coreutils: unknown program 'prooted-7-IyRqZA'
coreutils: unknown program 'prooted-10-O6QK5l'
coreutils: unknown program 'prooted-12-0wABF0'

The same commands work in ubuntu:24.04. How the command is started makes no difference, and every script that starts with #!/usr/bin/env fails through env. It is the guest's coreutils that fail, so an Ubuntu 25.10+ rootfs is affected on any host. The proot package in Ubuntu 26.04 is 5.4.0 and behaves the same.

This is also why the ubuntu-26.04 jobs of this repository's CI report 41 failed checks in "Execute test suite" (hidden by continue-on-error) while the ubuntu-24.04 jobs report none, for example in https://github.com/proot-me/proot/actions/runs/35549493565: the shell tests run the host's coreutils under PRoot. The first commit here brings the 41 down to 4, and the second fixes those 4, which fail for reasons of their own (see below), so both jobs report none.

Why PRoot gets it wrong

The kernel sets AT_EXECFN to the file it executes, and under PRoot that is the loader, extracted to /tmp/prooted-*. The loader already fixes the vector on the program's stack, pointing AT_EXECFN to argv[0] (see src/loader/loader.c), and that is what getauxval(3) returns. The kernel keeps its own copy of the vector, though, and two interfaces return that copy unchanged: prctl(PR_GET_AUXV) (Linux 6.4 and later) and /proc/self/auxv. uutils reads AT_EXECFN through rustix, which asks PR_GET_AUXV first and falls back to /proc/self/auxv on older kernels, so both have to agree with the stack.

What the first commit changes

  • When execve(2) returns, PRoot records where argv[0] is on the new stack, which is the address the loader puts in AT_EXECFN.
  • prctl(PR_GET_AUXV) now also stops at its exit, under seccomp as well, and AT_EXECFN is pointed to argv[0] in the buffer the kernel filled.
  • Opening /proc/self/auxv or /proc/<own pid>/auxv read-only is redirected to a copy of the kernel's vector with AT_EXECFN fixed the same way. The copy is written to a temporary file the first time the program opens it and removed at its next execve(2). QEMU's user-mode emulation answers this file the same way, and rustix opens it with a plain open(2) for that reason. Fixing the data as it is read instead would mean tracing every read(2), pread64(2), readv(2), and so on made on that descriptor, including through dup(2) and fork(2), and PRoot leaves all of those untraced under seccomp. A binding over the file, such as the one bind_proc_pid_auxv() makes for a ptraced tracee, takes precedence.

Only AT_EXECFN changes. The other entries the loader rewrites on the stack (AT_PHDR, AT_PHENT, AT_PHNUM, AT_BASE, AT_ENTRY) still read as the loader's through these two interfaces, as before.

test/auxv.bats runs a new helper, test/execfn.c, which prints AT_EXECFN as read through getauxval(3), PR_GET_AUXV, and /proc/self/auxv (through stdio). It covers a 64-bit build and a 32-bit one, which is skipped without multilib, like exec-m32. The Bats step of the CI now runs it. On master only the getauxval(3) case passes.

What the second commit changes

It fixes the four checks that still fail on Ubuntu 26.04 for reasons outside PRoot, and makes the scripts it touches pass shellcheck, since the CI lints every changed script. $PROOT stays unquoted, under a directive, because memcheck prefixes it with valgrind.

  • test-5996858d and test-carehwcp expect LD_SHOW_AUXV to print a cleared AT_HWCAP as 0, which glibc 2.43 prints as 0x0. Both prerequisite checks were also missing a space before a ].
  • test-82ba4ba1 expects chown root.root /root to fail for a non-root user, but the uutils chown returns success without calling chown(2) when the owner would not change. The check now uses a file the user owns.
  • test-gggggggg binds /bin elsewhere and then runs test from $PATH. PRoot names that command by its path under the bind, where Ubuntu 26.04's relative symlink /bin/test -> ../lib/cargo/bin/coreutils/test does not resolve, as it would not under a real bind mount either. The shell's builtin test checks the same thing without depending on that.

Results

This repository's workflows, run on this branch in my fork because the runs here wait for approval:

"Execute test suite", failed checks master first commit both commits
ubuntu-26.04, with and without seccomp 41 4 0
ubuntu-24.04, with and without seccomp 0 0 0

Runs: master, first commit, both commits. With both commits, every job passes 131 checks and skips 5, the Bats step passes in all four, and shellcheck, the unit tests, and the aarch64 cross-compile pass.

On an Ubuntu 26.04 machine (Linux 7.0.0, glibc 2.43, GCC 15.2, x86_64), with and without PROOT_NO_SECCOMP=1, test/auxv.bats passes 1 of 5 cases on master and 5 of 5 with this change, including the 32-bit build the CI runners skip. I also checked forked children and threads, a program exec'd by one that had already read the file, short and zero-length PR_GET_AUXV buffers, and a write-only open, which still fails with EACCES. No temporary file is left behind. A fork+exec loop and an open+close loop take the same time before and after (medians within 2%, same minimums).

Termux's fork has the same fix in two parts: termux/proot#353 fixes AT_EXECFN in PR_GET_AUXV, and termux/proot#392 answers /proc/self/auxv with a copy. The first commit follows the same approach.

Copilot AI lite review requested due to automatic review settings September 26, 2026 06:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sonarqubecloud

sonarqubecloud Bot commented Oct 1, 2026

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants