Summary
uutils install applies the requested file mode before ownership finalization. If the process can create and chmod the destination, but the later chown/chgrp step fails, install exits non-zero and leaves the requested mode in place.
With a mode such as 4755, this can leave a root-owned setuid executable.
I confirmed this under a capability-restricted root model: UID 0 with CAP_CHOWN removed. In that state, newly-created files are still owned by root, but chown() fails with EPERM. GNU install handles this safely and leaves only a 0600 root:root file. uutils leaves a 4755 root:root executable; a lower-privileged user can execute the leftover and run with euid=0.
Tested commit:
3c81e390952c1d95554d96837fcdf333021c2039
Affected utility:
Affected versions
The first affected release is 0.0.18. The issue was introduced by 05db5f744239d81bdfff00ef8f7c81981dc92a7f; parent 376f4d90e is the last pre-signature commit.
All tags from 0.0.18 through 0.9.0 were built and reproduced the failure-path artifact:
install -m 4755 -o root payload dst
exit != 0
destination present
mode & 0o6000 != 0
Root-impact PoC was confirmed on:
0.0.18 CONFIRMED_ROOT_EUID
0.9.0 CONFIRMED_ROOT_EUID
main CONFIRMED_ROOT_EUID
GNU install was safe in the same tests, leaving 0600 root:root.
0.0.17 and earlier are not included; they predate the introducing commit and do not match this propagated ownership-failure signature. Tags 0.0.1–0.0.14 were not claimed as affected because they were not built on the modern toolchain.
Trigger shape:
install -m 4755 -o <target-user> SRC DST
when the process is UID 0, can create/chmod the destination, but cannot complete the requested ownership change.
Impact
The confirmed impact is local privilege escalation to euid=0 in a capability-restricted root environment.
This is not a claim that every root-run install is exploitable. If root has CAP_CHOWN, the ownership step succeeds and this failure path is not reached. If the filesystem is mounted nosuid, or if NoNewPrivs=1, the artifact may still be created but setuid execution is blocked.
The vulnerable condition is:
uid=0
CAP_CHOWN absent
setuid execution allowed
attacker-influenced source
ownership finalization fails
leftover destination is reachable by the attacker
That condition is realistic in least-privilege root setups: containers with --cap-drop=CHOWN, systemd services using CapabilityBoundingSet=~CAP_CHOWN, and Kubernetes pods using capabilities.drop: ["CHOWN"] while still running as UID 0 and allowing privilege escalation. The Docker PoC below is only a minimal reproducer for the kernel-level condition.
Root cause
The destination starts safe, but finalization raises the mode before ownership is finalized:
copy/create destination -> safe initial mode around 0600
mode::chmod(to, b.mode()) -> applies requested mode, including 0o6000
chown_optional_user_group()? -> ownership/group finalization fails
error propagates -> no rollback, no removal, no clearing of S_ISUID/S_ISGID
Relevant code path:
src/uu/install/src/install.rs
copy_file:
destination is created with a safe initial mode around 0600
set_ownership_and_permissions:
mode::chmod(to, b.mode())
chown_optional_user_group(to, b)?
GNU applies ownership before applying the final privileged mode. When chown fails, GNU has not applied the setuid/setgid mode yet, so the leftover remains non-executable and non-privileged.
uutils applies the mode first. When the later ownership step fails, the already-applied setuid/setgid bits remain on disk. The strip failure path in the same file already removes the destination on failure; the ownership-failure path has no equivalent rollback.
Proof of Concept
Run inside an isolated container or VM, as root, on a filesystem that honors setuid. The payload is a small ELF binary that writes a marker into /root/uutils-install-proof; it does not spawn a shell.
Requirements:
gcc
runuser
capsh or setpriv
NoNewPrivs=0
filesystem not mounted nosuid
this poc_install_root_owned_setuid.sh and run:
bash poc_install_root_owned_setuid.sh target/release/install
PoC:
poc_install_root_owned_setuid.sh
Result
Captured output from the confirming run:
[env]
TARGET OPTIONS
/ rw,relatime,lowerdir=...,upperdir=...,workdir=...
NoNewPrivs: 0
[1] attacker direct run
payload: ruid=2002 euid=2002 open_failed=Permission denied
[2] GNU install with CAP_CHOWN dropped
install: cannot change ownership of '/work/uutils-install-rootpoc.FijzzQ/shared/gnu-helper': Operation not permitted
gnu_exit=1
GNU: 600 -rw------- root:root 0:0 /work/uutils-install-rootpoc.FijzzQ/shared/gnu-helper
runuser: failed to execute /work/uutils-install-rootpoc.FijzzQ/shared/gnu-helper: Permission denied
[3] uutils install with CAP_CHOWN dropped
install: failed to chown '/work/uutils-install-rootpoc.FijzzQ/shared/uu-helper': changing ownership of '/work/uutils-install-rootpoc.FijzzQ/shared/uu-helper': Operation not permitted (os error 1)
uu_exit=1
UU: 4755 -rwsr-xr-x root:root 0:0 /work/uutils-install-rootpoc.FijzzQ/shared/uu-helper
[4] attacker executes uutils leftover
payload: ruid=2002 euid=0 wrote_marker
[5] marker
payload ran: ruid=2002 euid=0
[verdict]
CONFIRMED_ROOT_EUID: attacker executed root-owned setuid artifact left by uutils install
GNU leaves a non-executable 0600 root:root file. uutils leaves a 4755 root:root setuid ELF. The attacker executes the uutils leftover, runs with euid=0, and writes into the root-only proof directory.
Real-world preconditions
This is not universal root escalation. Unrestricted root normally succeeds at chown, so the failure path is not reached. nosuid and NoNewPrivs=1 also block the final setuid-exec step.
The condition is still realistic. Linux capabilities allow UID 0 to run without CAP_CHOWN. In that state:
id -> uid=0(root)
CapEff/CapBnd -> CAP_CHOWN bit absent
chown 12345 /work/probe -> EPERM
newly-created file -> root:root
That maps directly to common hardening mechanisms:
Docker: --cap-drop=CHOWN
systemd: CapabilityBoundingSet=~CAP_CHOWN with User=root and NoNewPrivileges=no
Kubernetes: runAsUser: 0, allowPrivilegeEscalation: true, capabilities.drop: ["CHOWN"]
If Kubernetes sets allowPrivilegeEscalation: false, the kernel sets NoNewPrivs=1, which blocks setuid-based execution. If the volume is nosuid, execution is blocked as well. Those are valid mitigations, but they do not change the unsafe artifact creation.
A real systemd transient unit with CapabilityBoundingSet=~CAP_CHOWN reproduced the required finalization condition locally: chown failed with EPERM, GNU left 600 root:root, and uutils left 4755 root:root.
This has the same security shape as GHSA-x2wv-9p67-mh9w / CVE-2026-35350: setuid/setgid bits survive a failed ownership operation.
That advisory was in cp, and the fix cleared 0o6000 when ownership was not preserved. This report is a separate install path:
utility: install, not cp
trigger: explicit -m mode plus failing -o/-g finalization
root cause: chmod-before-chown finalization order
code path: src/uu/install/src/install.rs
The previous cp fix does not cover this install path.
Suggested fix
Match GNU’s safety ordering:
create destination with safe initial mode
perform ownership/group finalization
apply final requested mode only after ownership/group succeeds
Also consider defensive cleanup:
if chown/chgrp fails:
remove the partial destination
or clear S_ISUID | S_ISGID before returning the error
The same safety principle used for the cp fix for CVE-2026-35350 should be applied to install.
Regression test
Run as UID 0 with CAP_CHOWN removed, on a setuid-capable filesystem:
install -m 4755 -o targetuser src dst
Expected result:
exit code: non-zero
destination: absent OR mode has no S_ISUID/S_ISGID bits
assertion: (mode & 0o6000) == 0
Positive control:
when no ownership/group finalization is requested, should still produce mode 4755.
Fix (2026-07-29)
Branch https://github.com/sylvestre/coreutils/tree/install-chown-before-chmod swaps the order in set_ownership_and_permissions — chown first, then chmod — and skips the chmod entirely when the chown fails. directory() (install -d) had the identical ordering and is fixed the same way. This matches GNU, whose change_attributes() chowns first and only chmods in the else branch.
No root and no race is needed to reproduce — any failing chown exposes it, so an unprivileged user suffices:
$ install -m 4755 -o root src dst
GNU 9.10 : install: cannot change ownership of 'dst' -> dst is 600, no setuid
uutils : install: chown failed 'dst' -> dst is 4755, SETUID
After the fix uutils leaves 600, identical to GNU. For install -d both now drop the setuid bit (uutils 775, GNU 700 — that residual difference is the directory's creation mode on the failure path, a pre-existing non-security divergence this change does not alter).
Second bug fixed incidentally
chown(2) clears setuid/setgid, so applying the mode before the chown silently dropped those bits from files that installed successfully with an ownership change — i.e. install -m 4755 -o someuser was not producing a setuid binary at all. The new order fixes that too, which is presumably why GNU chose it.
Regression tests: test_install_failed_chown_does_not_leave_setuid (skipped under geteuid() == 0) and test_install_setuid_mode_applied_without_chown.
Summary
uutils installapplies the requested file mode before ownership finalization. If the process can create and chmod the destination, but the laterchown/chgrpstep fails,installexits non-zero and leaves the requested mode in place.With a mode such as
4755, this can leave a root-owned setuid executable.I confirmed this under a capability-restricted root model: UID 0 with
CAP_CHOWNremoved. In that state, newly-created files are still owned by root, butchown()fails withEPERM. GNUinstallhandles this safely and leaves only a0600 root:rootfile. uutils leaves a4755 root:rootexecutable; a lower-privileged user can execute the leftover and run witheuid=0.Tested commit:
Affected utility:
Affected versions
The first affected release is
0.0.18. The issue was introduced by05db5f744239d81bdfff00ef8f7c81981dc92a7f; parent376f4d90eis the last pre-signature commit.All tags from
0.0.18through0.9.0were built and reproduced the failure-path artifact:Root-impact PoC was confirmed on:
GNU
installwas safe in the same tests, leaving0600 root:root.0.0.17and earlier are not included; they predate the introducing commit and do not match this propagated ownership-failure signature. Tags0.0.1–0.0.14were not claimed as affected because they were not built on the modern toolchain.Trigger shape:
when the process is UID 0, can create/chmod the destination, but cannot complete the requested ownership change.
Impact
The confirmed impact is local privilege escalation to
euid=0in a capability-restricted root environment.This is not a claim that every root-run
installis exploitable. If root hasCAP_CHOWN, the ownership step succeeds and this failure path is not reached. If the filesystem is mountednosuid, or ifNoNewPrivs=1, the artifact may still be created but setuid execution is blocked.The vulnerable condition is:
That condition is realistic in least-privilege root setups: containers with
--cap-drop=CHOWN, systemd services usingCapabilityBoundingSet=~CAP_CHOWN, and Kubernetes pods usingcapabilities.drop: ["CHOWN"]while still running as UID 0 and allowing privilege escalation. The Docker PoC below is only a minimal reproducer for the kernel-level condition.Root cause
The destination starts safe, but finalization raises the mode before ownership is finalized:
Relevant code path:
GNU applies ownership before applying the final privileged mode. When
chownfails, GNU has not applied the setuid/setgid mode yet, so the leftover remains non-executable and non-privileged.uutils applies the mode first. When the later ownership step fails, the already-applied setuid/setgid bits remain on disk. The strip failure path in the same file already removes the destination on failure; the ownership-failure path has no equivalent rollback.
Proof of Concept
Run inside an isolated container or VM, as root, on a filesystem that honors setuid. The payload is a small ELF binary that writes a marker into
/root/uutils-install-proof; it does not spawn a shell.Requirements:
this poc_install_root_owned_setuid.sh and run:
PoC:
poc_install_root_owned_setuid.sh
Result
Captured output from the confirming run:
GNU leaves a non-executable
0600 root:rootfile. uutils leaves a4755 root:rootsetuid ELF. The attacker executes the uutils leftover, runs witheuid=0, and writes into the root-only proof directory.Real-world preconditions
This is not universal root escalation. Unrestricted root normally succeeds at
chown, so the failure path is not reached.nosuidandNoNewPrivs=1also block the final setuid-exec step.The condition is still realistic. Linux capabilities allow UID 0 to run without
CAP_CHOWN. In that state:That maps directly to common hardening mechanisms:
If Kubernetes sets
allowPrivilegeEscalation: false, the kernel setsNoNewPrivs=1, which blocks setuid-based execution. If the volume isnosuid, execution is blocked as well. Those are valid mitigations, but they do not change the unsafe artifact creation.A real systemd transient unit with
CapabilityBoundingSet=~CAP_CHOWNreproduced the required finalization condition locally:chownfailed withEPERM, GNU left600 root:root, and uutils left4755 root:root.Relationship to CVE-2026-35350
This has the same security shape as
GHSA-x2wv-9p67-mh9w/CVE-2026-35350: setuid/setgid bits survive a failed ownership operation.That advisory was in
cp, and the fix cleared0o6000when ownership was not preserved. This report is a separateinstallpath:The previous
cpfix does not cover thisinstallpath.Suggested fix
Match GNU’s safety ordering:
Also consider defensive cleanup:
The same safety principle used for the
cpfix for CVE-2026-35350 should be applied toinstall.Regression test
Run as UID 0 with
CAP_CHOWNremoved, on a setuid-capable filesystem:Expected result:
Positive control:
when no ownership/group finalization is requested, should still produce mode
4755.Fix (2026-07-29)
Branch https://github.com/sylvestre/coreutils/tree/install-chown-before-chmod swaps the order in
set_ownership_and_permissions— chown first, then chmod — and skips the chmod entirely when the chown fails.directory()(install -d) had the identical ordering and is fixed the same way. This matches GNU, whosechange_attributes()chowns first and only chmods in theelsebranch.No root and no race is needed to reproduce — any failing chown exposes it, so an unprivileged user suffices:
After the fix uutils leaves
600, identical to GNU. Forinstall -dboth now drop the setuid bit (uutils775, GNU700— that residual difference is the directory's creation mode on the failure path, a pre-existing non-security divergence this change does not alter).Second bug fixed incidentally
chown(2)clears setuid/setgid, so applying the mode before the chown silently dropped those bits from files that installed successfully with an ownership change — i.e.install -m 4755 -o someuserwas not producing a setuid binary at all. The new order fixes that too, which is presumably why GNU chose it.Regression tests:
test_install_failed_chown_does_not_leave_setuid(skipped undergeteuid() == 0) andtest_install_setuid_mode_applied_without_chown.