Ignore setuid from owners the guest cannot mean - #267
Conversation
02520c8 to
e2633d9
Compare
e2633d9 to
6e9e0e2
Compare
jserv
left a comment
There was a problem hiding this comment.
Built the branch and ran the new test both ways. Against pre-PR exec.c all three substantive cases fail (child: euid=501 expected=1000 twice, and the owner-only exec is refused with 127); after the patch all four pass. So the test is a real regression guard, not decoration. It also compiles clean under -Wall -Wextra, and no Makefile change is needed since mk/config.mk:40 picks it up through wildcard tests/*.c.
Other things I checked and found clean: exec_st comes from fstat(exec_fd, ...) on the already-opened image, so there is no TOCTOU between the check and the load; both interpreter sites apply the overlay before checking; chown_overlay_apply takes its own rwlock and neither new call site holds a conflicting one; exec_st is never overlay-applied in place.
Two notes that are not line-anchored.
The overlay is trusted for the permission check but not for the set-id owner, and the argument for distrusting it applies to both. chown_result (src/syscall/fs.c:2741) records on EPERM with no ownership test, so a guest can chown a file it does not own into its own name and gain exec permission the physical mode denies. There is no real capability gain, since the host open gates readability first, but the comment could say in one line that the asymmetry is a deliberate stat-consistency tradeoff rather than an oversight.
The mapping gap in #264 is untouched. src/syscall/fs-stat.c:52 passes st_uid through raw, so a sysroot file the guest genuinely owns still reports 501 while the guest is 1000, and the new overlay-applied check only helps files the guest explicitly chowned. That is a reasonable first step, but worth saying in the PR so #264 is not closed as fully solved.
One test suggestion beyond the inline comments: test-matrix.sh already runs test-credentials in both modes, and a --fakeroot lane for this test would cover the privileged-caller path that is currently unexercised.
| if ((exec_st.st_mode & S_ISUID) && | ||
| exec_id_is_guest_meaningful((uint32_t) exec_st.st_uid, | ||
| proc_get_uid())) { | ||
| new_euid = (uint32_t) exec_st.st_uid; | ||
| } | ||
| if ((exec_st.st_mode & S_ISGID) && (exec_st.st_mode & S_IXGRP)) { | ||
| if ((exec_st.st_mode & S_ISGID) && (exec_st.st_mode & S_IXGRP) && | ||
| exec_id_is_guest_meaningful((uint32_t) exec_st.st_gid, | ||
| proc_get_gid())) { | ||
| new_egid = (uint32_t) exec_st.st_gid; |
There was a problem hiding this comment.
Three things about this block.
First, the commit message contradicts the code. Its closing paragraph says "Root stays reachable because a guest chown is recorded in the overlay, which is how an emulated-root guest marks a helper setuid-root." The set-id owner is read from the physical stat here, so an overlay-recorded owner of 0 never elevates. The comment at line 548 says exactly that, and test case 3 asserts it. Root is reachable only through a file the host really owns as root. This lands in git history, so worth fixing before merge.
Second, ignoring the bit is fail-open in the drop direction, and the commit message does not mention it. A caller already at euid 0 (--fakeroot, or after a legitimate setuid-root exec) that execs a setuid binary owned by the host user previously dropped to that owner and now keeps euid 0. That is what Linux does for a nosuid mount, so it is defensible, but the message frames the change only as "never an exec failure". Please state the other half. The tempting fix, widening the predicate when the caller is already root, would reintroduce what this PR fixes: a root guest would then take euid 501 from a sysroot file, which is the unmapped-host-uid outcome of #264.
Third, the !exec_is_script gate and the comment above it. The comment claims the current behavior matches bprm_fill_uid, which is half right. Linux ignores set-id on the script, but exec_binprm replaces bprm->file with the interpreter, and the final binfmt's begin_new_exec runs bprm_creds_from_file on that file, so a setuid interpreter named in a #! line does elevate on Linux. Either derive set-id from a physical fstat(exec_fd) taken after the shebang loop, or correct the comment to say the interpreter case is deliberately unsupported. Pre-existing, but this PR rewrites the block and the comment.
| static int caller_is_privileged(void) | ||
| { | ||
| return geteuid() == 0; |
There was a problem hiding this comment.
With euid == 0 cases 2 and 3 skip, case 4 becomes trivial, and the test still prints all tests passed and exits 0. The qemu lane runs as genuine root, so it is a vacuous green there.
Either add test-setuid-exec to QEMU_SKIP in test-matrix.sh with a rationale comment (the file is overlay-specific, in the same spirit as the existing test-credentials entry), or count executed checks and refuse to report success when none of the interesting ones ran.
| if (build_setuid_helper() != 0) { | ||
| printf("FAIL (could not build helper: %m)\n"); | ||
| return 1; |
There was a problem hiding this comment.
This path returns without the cleanup unlink, and every failure inside build_setuid_helper after mkstemp leaves the file behind in /tmp. Use goto cleanup.
246be2e to
3932ee4
Compare
|
Drop-direction fail-open — real, and it's now the assertion the privileged lane makes rather than just prose: under
Matrix regex — Case 2 was detecting the wrong thing — right: the physical 502 is what the set-id path reads; 4242 is virtual and never reaches it. It no longer skips (the physical owner already stages a foreign owner whether or not the chown records), the message names both owners, and the header says which one does the work. Vacuous green at euid 0 — took both remedies rather than either. Helper leak — Overlay asymmetry — the one-line justification you asked for is in the access comment: a guest can chown a file into its own name and pass a check the physical mode refuses, it gains no capability because the host open gates the physical file first, and the alternative is an execve contradicting the guest's own stat. Verified both directions : unprivileged fails all three substantive checks ( On #264 — agreed, and I changed the trailer to |
jserv
left a comment
There was a problem hiding this comment.
Rebase latest main branch and resolve conflicts.
3932ee4 to
a1b93b6
Compare
a1b93b6 to
f242f7b
Compare
execve took the new effective ID straight from a raw host fstat. A sysroot is an ordinary tree owned by whoever unpacked it and no host IDs are mapped into the guest, so a setuid binary left the process at an ID that exists nowhere in the guest -- neither root nor its own. Privilege checks against euid 0 failed, ownership comparisons against guest IDs failed, and the ID granted followed whoever owned the tree. Judge access through chown_overlay_apply, the way fs-stat.c reports ownership, so exec and stat cannot disagree about who owns a file. Trusting the overlay there is a trade rather than an oversight: any guest can write it, but the host open still gates the physical file, so the most it buys is a check the physical mode would have refused. The set-id owner takes both views and grants only where they agree. Reading it through the overlay alone would make root self-service, since an overlay owner of 0 proves nothing about privilege: chown your own file to 0, set the bit, exec. Reading it from the host alone would elevate on ownership the guest's own stat denies, since a physically root-owned file kept elevating after a guest chowned it away. Agreement grants nothing new -- root still means a file the host really owns as root -- and lets a guest chown withdraw an elevation but never conjure one. Honour the bit only for root or the caller's own ID and leave the ID untouched otherwise, matching what Linux does for a setuid binary on a nosuid mount rather than failing the exec. This cuts both ways: a caller already at euid 0 that execs a setuid file owned by a host ID now keeps root instead of dropping to an ID the guest cannot mean. Widening the rule for privileged callers would reintroduce the unmapped-host-ID outcome this fixes. tests/test-setuid-exec.c pins both directions. Unprivileged it stages a foreign owner and a recorded root owner and checks neither moves the effective ID, and that an owner-only file the guest owns still runs. Under --fakeroot it checks root is not pulled down to the host owner. Against the previous exec.c the unprivileged lane fails all three checks and the fakeroot lane fails its one, and a run refuses to report success when no check exercised an exec. Both lanes reason about elfuse-only state, so the qemu lane skips them. Addresses sysprog21#264
f242f7b to
52f882d
Compare
|
Updated |
execve took the new effective ID straight from a raw host fstat. A sysroot is an ordinary tree owned by whoever unpacked it and no host IDs are mapped into the guest, so a setuid binary left the process at an ID that exists nowhere in the guest -- neither root nor its own. Privilege checks against euid 0 failed, ownership comparisons against guest IDs failed, and the ID granted followed whoever owned the tree.
Read ownership through chown_overlay_apply, the way fs-stat.c reports it, so exec and stat cannot disagree about who owns a file. Honour the bit only for root or the caller's own ID and leave the ID untouched otherwise, matching what Linux does for a setuid binary on a nosuid mount rather than failing the exec.
Root stays reachable because a guest chown is recorded in the overlay, which is how an emulated-root guest marks a helper setuid-root.
Fix #264
Summary by cubic
Prevent exec from switching to unmapped host IDs by ignoring setuid/setgid owners the guest cannot mean and by checking exec permissions against guest-visible ownership for the main binary and any shebang interpreter. Set‑id only applies when physical and guest-visible owners agree and the owner is root or the caller; set‑id scripts and interpreters do not elevate; otherwise euid/egid stay unchanged.
chown_overlay_applyfor the main binary and any shebang interpreter so exec and stat agree.test-setuid-execandtest-setuid-exec-fakeroot; include intests/test-matrix.shand skip on QEMU; updateelfuse-aarch64baseline to 241.Written for commit 52f882d. Summary will update on new commits.