Skip to content

install: create ancestor directories at 0755 regardless of umask - #12715

Open
abendrothj wants to merge 4 commits into
uutils:mainfrom
abendrothj:fix/install-zero-umask-ancestor-dirs
Open

abendrothj wants to merge 4 commits into
uutils:mainfrom
abendrothj:fix/install-zero-umask-ancestor-dirs

Conversation

@abendrothj

@abendrothj abendrothj commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

install -d and install -D passed 0755 to mkdir(2), so the caller's umask still applied: ancestors came out 0750 under umask 027 and 0700 under 077. GNU 9.12 creates them at 0755 whatever the umask is, and --mode only applies to the final target.

install never reads the umask, so the Directory/Standard dispatch now runs once inside with_umask(0, ...). That also covers the staging file, so -s works under a umask that would leave it unreadable to strip, and the strip child sees a umask of 0, like GNU's. Mode parsing happens before, with an explicit mask of 0.

This is also why test_install_ancestors_mode_directories_with_file failed for anyone whose umask wasn't 022 (#11363): it compared against a probe directory made with mkdir(2). It now asserts 0755 directly. The ancestor tests run under umask 077 and the strip tests under 0777. The executable test fixtures are written by a child process, so the test binary doesn't hold a writable fd to them.

Based on #15121; until it merges, the diff here shows its commits too. #15121 also moves mkdir onto the helper, and #14768 does mkfifo and mknod.

Fixes #12714. Fixes #11363.

@github-actions

github-actions Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

GNU testsuite comparison:

Skip an intermittent issue tests/date/resolution (fails in this run but passes in the 'main' branch)

@codspeed

codspeed Bot commented Jun 12, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 2 benchmarks

⚡ 8 improved benchmarks
❌ 2 regressed benchmarks
✅ 389 untouched benchmarks
⏩ 54 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ Simulation three_39_bit_primes 371.3 ms 542.7 ms -31.58%
❌ Simulation thirteen_39_bit_primes 9 s 9.5 s -5.41%
⚡ Simulation split_lines 11.1 ms 8.5 ms +29.92%
⚡ Simulation split_numeric_suffix 11.3 ms 8.7 ms +29.22%
⚡ Simulation cut_fields_custom_delim 65.9 ms 51.9 ms +27%
⚡ Simulation cut_fields_tab 57.4 ms 45.5 ms +26.24%
⚡ Simulation cut_bytes 18.1 ms 15.5 ms +16.72%
⚡ Simulation cut_characters 25.4 ms 22.9 ms +10.89%
⚡ Simulation unexpand_large_file[10] 286.3 ms 274.8 ms +4.18%
⚡ Simulation unexpand_many_lines[100000] 136.9 ms 131.4 ms +4.17%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing abendrothj:fix/install-zero-umask-ancestor-dirs (592f925) with main (8cf2e4f)

Open in CodSpeed

Footnotes

  1. 54 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@abendrothj
abendrothj force-pushed the fix/install-zero-umask-ancestor-dirs branch from 9cfc786 to 11d3c90 Compare July 4, 2026 00:00
@abendrothj

abendrothj commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Rebased onto current main, dropped an empty ci: retrigger CI commit and resolved a real merge conflict in directory(): main independently added the "existing path is not a directory" check since this branch was opened, so I kept that guard and moved this PR's DirBuilder-with-explicit-mode fix into its else (creation) branch, preserving both behaviors. All 96 install tests pass (up from 92 — main added 4 more since), fmt/clippy clean, and I manually re-verified the actual fix: under umask 0027, install -D now creates ancestor dirs at 0755 instead of 0750.

@abendrothj
abendrothj force-pushed the fix/install-zero-umask-ancestor-dirs branch from 11d3c90 to 81625e6 Compare August 17, 2026 03:46
Copilot AI lite review requested due to automatic review settings August 17, 2026 03:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR aligns install with GNU behavior by zeroing the process umask early so that ancestor directories created via -D/-d are reliably created at DEFAULT_MODE (0755), independent of the caller’s umask.

Changes:

  • Add uucore::mode::zero_umask() (unix-only) to set umask to 0.
  • Call zero_umask() at the start of install::uumain and adjust -d directory creation to use DirBuilder::mode(DEFAULT_MODE) (instead of create_dir_all).
  • Update ancestor-mode tests to assert GNU’s guaranteed 0755 behavior for ancestor directories.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
tests/by-util/test_install.rs Updates ancestor permission assertions to match GNU’s fixed 0755 guarantee.
src/uucore/src/lib/features/safe_traversal.rs Updates docs to clarify umask interaction and the need to zero umask for exact modes.
src/uucore/src/lib/features/mode.rs Adds a unix-only zero_umask() helper based on rustix.
src/uu/install/src/install.rs Zeros umask early; switches -d directory creation to DirBuilder::mode(DEFAULT_MODE); updates comments for new semantics.
Suppressed comments (2)

tests/by-util/test_install.rs:108

  • These assertions now hard-code the expected ancestor mode, but the test no longer exercises the original bug (behavior under a restrictive inherited umask). On typical CI umask=0022, ancestors would still be 0755 even if install didn’t zero umask, so this can pass while regressing. Set a restrictive umask for the spawned install process (and keep the 0755 assertions) to ensure the test fails if zero-umask behavior is removed.

This issue also appears on line 143 of the same file.

    ucmd.args(&[mode_arg, directories_arg, target_dir])
        .succeeds()
        .no_stderr();

tests/by-util/test_install.rs:146

  • This test hard-codes 0755 for ancestor dirs, but it doesn’t currently validate the fix under a restrictive inherited umask. On common umask=0022, ancestors would still be 0755 even if install stopped zeroing umask, so this could pass while regressing. Force a restrictive umask for the spawned install process to ensure the test actually exercises the behavior change.
    // GNU install zeros umask at startup and creates ancestor dirs at exactly
    // 0755 (DEFAULT_MODE). --mode applies only to the final target.
    assert_eq!(0o40_755_u32, at.metadata(ancestor1).permissions().mode());
    assert_eq!(0o40_755_u32, at.metadata(ancestor2).permissions().mode());

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/uu/install/src/install.rs Outdated
Copilot AI review requested due to automatic review settings August 17, 2026 09:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/uucore/src/lib/features/safe_traversal.rs:1307

  • This test asserts the created file mode is exactly 0o600, but file creation modes are always masked by the process umask; a restrictive umask could legitimately clear bits and make this assertion fail even if open_file_at_with_mode honors the requested mode. To keep the test robust across environments, assert that no permission bits outside the requested mode are set (umask can only remove bits).
        assert_eq!(mode, 0o600);

src/uucore/src/lib/features/safe_traversal.rs:453

  • The new open_file_at_with_mode docs imply the provided mode is applied as-is, but the kernel still applies the process umask to O_CREAT modes. Calling this out helps prevent callers from assuming they can get an exact mode without zeroing/restoring umask.

This issue also appears on line 1307 of the same file.

    /// The mode is used only when creating a new file; an existing file keeps
    /// its current permissions. Callers that will apply final permissions
    /// later should use a restrictive initial mode so failures cannot leave a
    /// partially-created file overly permissive.

Copilot AI review requested due to automatic review settings August 18, 2026 03:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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.

Copilot AI review requested due to automatic review settings September 4, 2026 22:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new mode assertions in test_install.rs compare full st_mode (including type/special bits), which can make tests brittle across environments even when permission bits are correct.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

tests/by-util/test_install.rs:160

  • Same as above: permissions().mode() includes type/special bits, so asserting the full value can be brittle (e.g. setgid inheritance). Mask to the permission bits so the test checks only the intended contract.
    assert_eq!(0o40_755_u32, at.metadata(ancestor1).permissions().mode());
    assert_eq!(0o40_755_u32, at.metadata(ancestor2).permissions().mode());

    // Expected mode only on the target_file.
    assert_eq!(0o100_200_u32, at.metadata(target_file).permissions().mode());
  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread tests/by-util/test_install.rs Outdated
Comment thread src/uucore/src/lib/features/safe_traversal.rs Outdated
@abendrothj
abendrothj force-pushed the fix/install-zero-umask-ancestor-dirs branch from 5743285 to 07d815b Compare September 7, 2026 04:43
Copilot AI review requested due to automatic review settings September 7, 2026 04:43
@abendrothj abendrothj changed the title fix(install): zero umask at startup so ancestor dirs are created at exact 0755 install: create ancestor directories at 0755 regardless of umask Sep 7, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

It changes process-wide umask handling across multiple utilities and introduces new global serialization behavior, which warrants final human validation despite strong test coverage.

Review details
  • Files reviewed: 9/10 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/uucore/src/lib/features/safe_traversal.rs Outdated
Copilot AI review requested due to automatic review settings September 7, 2026 05:24
@abendrothj
abendrothj force-pushed the fix/install-zero-umask-ancestor-dirs branch from 07d815b to 3fb489c Compare September 7, 2026 05:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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.

@abendrothj
abendrothj force-pushed the fix/install-zero-umask-ancestor-dirs branch from 3fb489c to c2688a6 Compare September 7, 2026 05:27
Copilot AI review requested due to automatic review settings September 7, 2026 05:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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.

@abendrothj
abendrothj force-pushed the fix/install-zero-umask-ancestor-dirs branch from c2688a6 to 7d85692 Compare September 21, 2026 01:59
Copilot AI review requested due to automatic review settings September 21, 2026 01:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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.

@abendrothj

Copy link
Copy Markdown
Contributor Author

@sylvestre since you liked #14768: it's stacked on this one, so this needs to land first. Would you have time for a look?

Comment thread src/uu/install/src/install.rs Outdated
use std::os::unix::fs::DirBuilderExt;
let mut builder = fs::DirBuilder::new();
builder.recursive(true).mode(DEFAULT_MODE);
if let Err(e) = uucore::mode::with_umask(0, || {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we now have with_umask(0, ...) in 5 places.
install never reads the umask, so could we just wrap the whole uumain body once instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 66b40f6: one with_umask(0, ...) around the Directory/Standard dispatch. Mode parsing happens before it with an explicit mask of 0, and strip is a plain status() inside the wrapper, so it inherits the cleared umask.

Comment thread src/uu/install/src/install.rs Outdated
continue;
}
}
#[cfg(not(unix))]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the unix and non-unix branches are almost the same code, could be dedup, no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The duplicated branches this PR added are gone. The older unix/non-unix splits already in install are untouched.

Comment thread src/uu/install/src/install.rs Outdated

#[cfg(unix)]
#[test]
fn test_uumain_restores_umask() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this is a lot of machinery (re-exec the test binary, unsafe umask) to check a Drop impl.
do we really need it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, dropped test_uumain_restores_umask. The restore is tested once, in-process, on the helper itself, in #15121.

/// The previous umask is restored when the operation returns or unwinds.
/// Calls through this module are serialized because the umask is process-wide.
#[cfg(unix)]
pub fn with_umask<T>(mask: u32, operation: impl FnOnce() -> T) -> T {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the lock isn't reentrant, so calling get_umask() or with_umask() inside the closure deadlocks.
please document that

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Documented: the doc comment says calling get_umask or with_umask inside the closure deadlocks. The helper moved to its own PR (#15121), and #14768 adds a debug_assert for it, so debug builds panic instead of hanging.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

update: #15121 makes the lock reentrant instead, as you asked there, so a nested call works now and #14768 no longer adds a debug_assert.

/// * `mode` - The mode to use when creating new directories (e.g., 0o755). The actual
/// mode will be modified by the process umask.
/// * `mode` - The mode to use when creating new directories (e.g., 0o755). The
/// kernel applies the umask to the mode on each mkdir, so callers that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this doesn't need to mention install, please make it shorter

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dropped; this PR doesn't touch safe_traversal anymore.

Comment thread src/uucore/src/lib/features/mode.rs Outdated

#[cfg(unix)]
#[test]
fn test_with_umask_serializes_overlapping_calls() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

60 lines, threads and channels to test a mutex. could we drop it or make it much simpler?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dropped it. What's left is the short set/restore test in #15121.

Copilot AI lite review requested due to automatic review settings October 6, 2026 07:15
@abendrothj
abendrothj force-pushed the fix/install-zero-umask-ancestor-dirs branch from be13503 to 66b40f6 Compare October 6, 2026 07:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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.

Utilities that create files or directories at an exact mode, whatever
the caller's umask, change the process umask around the call. Done by
hand, overlapping calls can restore each other's mask and leave the
process at the wrong value, and an unwind between the set and the
restore leaks the temporary mask.

with_umask runs an operation with a temporary umask, holds a mutex
shared with get_umask, and restores the previous mask on return or
unwind. Calls nest: the thread that holds the mutex skips it, and
get_umask there returns the mask in effect. Only Unix sets the umask
this way, so Windows' get_umask takes no lock.
Replace mkdir's own umask guard with the shared helper, so its change
is serialized with get_umask and other callers. The shaped umask is
computed as a plain mask, which drops mkdir's rustix dependency.
The test set and restored the umask with its own guard, outside the
lock that get_umask and with_umask share.
install -d and -D passed 0755 to mkdir(2), so the caller's umask still
applied to the ancestors they created. Run the Directory/Standard
dispatch under a umask of 0, as GNU does, so new ancestors get exactly
0755 and the strip program sees a umask of 0.
@abendrothj
abendrothj force-pushed the fix/install-zero-umask-ancestor-dirs branch from 66b40f6 to 592f925 Compare October 7, 2026 00:39
Copilot AI lite review requested due to automatic review settings October 7, 2026 00:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

3 participants