Repository navigation
chmod, chown: check --preserve-root on the directory actually descended into - #14972
abendrothj wants to merge 2 commits into
Conversation
Merging this PR will degrade performance by 29.38%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Simulation | factor_three_39_bit_primes |
300.2 ms | 582.9 ms | -48.49% |
| ❌ | Simulation | factor_thirteen_39_bit_primes |
8.9 s | 9.2 s | -3.19% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing abendrothj:chmod-chown-operand-by-fd (73df228) with main (98f90f2)2
Footnotes
-
70 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. ↩
-
No successful run was found on
main(f406891) during the generation of this report, so 98f90f2 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩
|
GNU testsuite comparison: |
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as outdated.
This comment was marked as outdated.
|
Opened #14990. |
b5edd8c to
ab7438f
Compare
|
The SELinux GNU job failed while booting its VM, before any test ran; unrelated to this change. |
| #[error("{}", translate!("chmod-error-changing-permissions", "file" => _0.quote(), "err" => strip_errno(_1)))] | ||
| ChangingPermissions(PathBuf, std::io::Error), | ||
| #[error("{}", translate!("perms-cannot-access-replaced", "file" => _0.quote()))] | ||
| #[cfg_attr( |
There was a problem hiding this comment.
could you please gate the variant with cfg instead of allow(dead_code)?
There was a problem hiding this comment.
Done in e1382d6: the variant is under cfg(not(any(target_os = "aix", target_os = "hurd", target_os = "redox"))), the same gate as its users, and the allow is gone.
| r = self.safe_traverse_dir(&dir_fd, file_path, ancestors).and(r); | ||
| Ok(dir_fd) => Some(dir_fd), | ||
| Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => None, | ||
| Err(err) => return Err(err.into()), |
There was a problem hiding this comment.
before, chmod_file was still attempted when the open failed. now we bail out without changing the mode, is that intended?
There was a problem hiding this comment.
No, that wasn't intended. Fixed in e1382d6: if the first open or fstat fails for any reason, chmod falls back to the pathname chmod as before. When the open works, the descriptor is used for the root check and the mode change, and it's dropped before descending.
There was a problem hiding this comment.
correction to my answer above: falling back on any error was too broad. a rename that makes the open fail (a file swapped in, say, or on macOS apparently the rename alone, which is what CI hit) got whatever was there changed by path, and the descent after it wasn't checked.
now only EACCES/EMFILE/ENFILE fall back, as in GNU 9.12; other errors are reported with the name.
| .as_ref() | ||
| .is_some_and(|pinned| !Self::same_dir(pinned, &dir_fd)) | ||
| { | ||
| return r.and(Err(ChmodError::Replaced(file_path.into()).into())); |
There was a problem hiding this comment.
please add a test in tests/by-util/test_chmod.rs, this new path isn't covered at all
There was a problem hiding this comment.
Added in 0e38dba: --preserve-root through a symlink operand with -H/-L and with -h, an unreadable operand, a low fd limit, and a test that keeps swapping the operand for a symlink to / across 200 runs. The swap test is the one that reaches the second open; it fails with the merge-base sources and passes now. The fd-limit test fails on ab7438f and passes now. The symlink tests are stopped by the existing earlier check, so they're there for the -h combinations, not the race.
There was a problem hiding this comment.
small correction to my list: the race test swaps the operand between two directories of the fixture, not to /, so a failure stays inside it. it now also swaps in a file. without the fix it fails on the bare error message; the wrong descent itself it only catches by chance (once in 2000 runs on Linux, outside the suite). no test reaches the EMFILE/ENFILE fallback or the macOS case.
There was a problem hiding this comment.
macos ci failed on the race test's error check, not on the descent: macOS 26 returns EINVAL from stat/chmod/open on op while the racer renames over it (a small C loop on the CI image got it ~25k times in 2M lookups, never on macOS 27). the test now accepts any one error naming op, and checks the modes first.
on the macOS CI image it passes 120 runs; with the chmod from before the fix and only the mode check, 32 of 180 runs descend into the wrong directory. so on macOS it now catches the wrong descent itself, not just the error message. on Linux I haven't re-measured that.
| { | ||
| return r.and(Err(ChmodError::Replaced(file_path.into()).into())); | ||
| } | ||
| if self.preserve_root && Self::is_root_fd(&dir_fd) { |
There was a problem hiding this comment.
when pinned is Some, same_dir already proved this isn't /, so do we need this second check?
There was a problem hiding this comment.
No. In e1382d6 the identity comparison handles the Some case, and the root check only runs on the fallback path.
| /// answer describes the file the caller is about to act on even if the path | ||
| /// has been re-pointed since. | ||
| #[cfg(unix)] | ||
| pub fn dev_ino_is_root_dir(dev: u64, ino: u64) -> bool { |
There was a problem hiding this comment.
why not just take a &Metadata? both callers have one
There was a problem hiding this comment.
The two callers have different types: chown has a std::fs::Metadata, but chmod gets safe_traversal's metadata from fstat on the descriptor. So in b8be553 it takes &impl MetadataExt, which both implement, and neither caller has to stat the path again.
| /// descent is pinned to, whatever the path points at by the time it is asked. | ||
| #[cfg(unix)] | ||
| #[test] | ||
| fn test_meta_is_root_ignores_the_path() { |
There was a problem hiding this comment.
this is almost the same test as test_dev_ino_is_root_dir and test_is_root_fd, could we keep just one?
0e38dba to
a400b6f
Compare
1066329 to
9989952
Compare
|
rebased on #15030: the gates here are Redox-only now, like the rest of chmod. with the old ones, chmod wouldn't have built on Hurd/AIX once both were merged (main + this failed Hurd clippy). cross-checked with clippy for Hurd and AIX, not run there. back out of draft once ci is green. |
| // file that was distributed with this source code. | ||
|
|
||
| // spell-checker:ignore (ToDO) Chmoder cmode fmode fperm fref ugoa RFILE RFILE's | ||
| // spell-checker:ignore (ToDO) Chmoder cmode fmode fperm fref ugoa RFILE RFILE's fchmod EMFILE ENFILE |
There was a problem hiding this comment.
consider putting EMFILE and ENFILE in .vscode/cspell.dictionaries/workspace.wordlist.txt in the vars/errno section.
There was a problem hiding this comment.
Done, moved both to the wordlist.
| return self.chmod_file(file_path); | ||
| } | ||
|
|
||
| // Change the mode through a descriptor, checked not to be "/", rather than by |
There was a problem hiding this comment.
nobody will read a 9 lines comment :) please make it 2-3 lines
There was a problem hiding this comment.
Done, down to 3 lines.
| match DirFd::open(file_path, should_follow_symlink.into()) { | ||
| Ok(dir_fd) => { | ||
| r = self.safe_traverse_dir(&dir_fd, file_path, ancestors).and(r); | ||
| if !((!file_path.is_symlink() || should_follow_symlink) && file_path.is_dir()) { |
There was a problem hiding this comment.
the double negation is hard to read, could you please name the condition (e.g. let descend = ...) and test !descend?
| } | ||
| } | ||
|
|
||
| // The mode change can also leave it unreadable: the operand keeps its new |
There was a problem hiding this comment.
this is a different scenario, could you please move it to its own test?
There was a problem hiding this comment.
Done, it's now test_chmod_recursive_operand_made_unreadable.
eec0a25 to
ff34b06
Compare
ff34b06 to
2d04aa0
Compare
The recursive operand's --preserve-root check looked the path up again, separately from the stat that the operand's descriptor is then verified against. A rename landing between the two let that check see a harmless directory while the chown and the descent went into "/" under -H or -L. Also decide on that stat, through a new uucore::fs::metadata_is_root_dir. The path lookup stays, so a symlink to "/" that the stat did not follow is still reported as before.
The operand of a recursive chmod was checked against --preserve-root by path, changed by path and then opened by path to descend, so a rename landing between those steps could send the mode change and the descent into "/" under -H, or through a symlink swapped in under -P. Open the operand first, check that descriptor is not "/", and change the mode through it. The directory is then opened again to descend, as GNU does, so the new mode still decides whether it can be read, and the descent goes ahead only if that is the same directory. If the operand can't be opened because it isn't readable yet or no descriptors are left, or under -h with -H or -L, the mode is changed by path as before; other open errors are reported.
2d04aa0 to
73df228
Compare
The recursive operand of chmod, chown and chgrp was checked against --preserve-root with its own path lookup, separate from the one the change and the descent used. A rename landing in between could send a -H/-L recursion into "/" after the check had passed.
chown/chgrp now also decide on the stat that the operand's descriptor is already verified against. chmod opens the operand, checks that descriptor, and changes its mode through it; it still reopens the directory to descend, like GNU, and only continues if it is the same directory. Under -P this also stops the operand's own mode change from following a symlink swapped in at the directory's name. If the operand can't be opened because it isn't readable yet or no descriptors are left, chmod changes its mode by name, as GNU does; other errors are reported. Run on macOS, Linux and Debian GNU/Hurd (on Hurd, the tests that fail also fail on main); AIX is only cross-checked with clippy (AIX with libc 0.2.189, see #15083).
Closes #14990