Skip to content

install can leave a root-owned setuid artifact when ownership finalization fails

Moderate
sylvestre published GHSA-cgg3-923w-v53m Sep 16, 2026

Package

cargo uu_install (Rust)

Affected versions

>= 0.0.18, <= 0.9.0

Patched versions

None

Description

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:

install

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.

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 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:

install -m 4755 src dst

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.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
High
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N

CVE ID

No known CVE

Weaknesses

Improper Preservation of Permissions

The product does not preserve permissions or incorrectly preserves permissions when copying, restoring, or sharing objects, which can cause them to have less restrictive permissions than intended. Learn more on MITRE.

Credits