Skip to content

Commit de939de

Browse files
committed
change: Get mode before +x using open file descriptor, not path
This uses `fstat` rather than a method that delegates to `lstat`, to get the current mode bits of the still-open file using the open file descriptor (obtained from the `File` object) rather than opening the path, which could be a different file in the case of concurrent modification by code outside gitoxide. This is half of the change to using the file descriptor rather than the path for the operations that adjust the mode by adding executable bitsi. (As before, they are added for whoever already had read bits, and we remove setuid, setgid, and sticky bits, so that +x does not cause a security problem or other potentially unexpected effects.) The other half of this change was already done, appearing in a recent `change:` commit. It consisted of setting the mode using `fchmod` instead of a method that delegated to `chmod`. With only that change, the overall effect would be worse, since possible race conditions could introduce even greater inconsistency. With both changes, the effect is consistent. Race conditions can still occur, but their effect is probably less severe than before either of the changes were made. One of the ways this may be less subject to race conditions is that, because neither obtaining the old mode nor setting the new one uses a path, but only the open file descriptor, an unexpected typechange will not occur: having opened a regular file, the open file will not turn into a directory, symlink, or anything else, *as seen by* the open file descriptor. The file may be moved or replaced, but the open file descriptor "follows" the move and does not alias the separate file that now has the original path. Because we only intend to add executable bits to regular files, it is no longer necessary to handle this as a race condition and keep going. Attempting to add +x to any other type of thing should now only be possible if there is a bug in the caller. So the check for that, and the associated Option<...> logic used to keep things testable, is not needed. This still does the check, but panics if the file descriptor does not represent a regular file. The tests of this are mostly removed, but two tests that it does panic for the kinds of non-regular files we are most likely to encounter are added. (It may eventually make sense check this and panic only in debug builds, or not at all.) There are now two `fstat` calls: the one that is added to check the `st_mode` to figure out what to pass to `fchmod`, and the preexisting higher level invocation (that delegates to `fstat`) done just before closing the file, to update `entry.stat`. Using the same call for both and accounting for any effect of `fchmod` might be more complicated, but more importantly, it does not seem like it would be correct. If attempting to set the permissions reports success but has an unexpected effect, perhaps due to restrictions of the underlying filesystem or how it is mounted (which we may not be guarnateed to have detected in a probe to decide if +x is supported), we would want the real stat. Because this no longer uses `Permissions` and `PermissionsExt` facilities to check the mode before adding executable bits or to add the executable bits, it also includes some refactoring that is possible now that they are not being used.
1 parent 492ac8b commit de939de

File tree

1 file changed

+77
-93
lines changed

1 file changed

+77
-93
lines changed

gix-worktree-state/src/checkout/entry.rs

Lines changed: 77 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -285,16 +285,9 @@ pub(crate) fn finalize_entry(
285285
// For possibly existing, overwritten files, we must change the file mode explicitly.
286286
#[cfg(unix)]
287287
if let Some(path) = set_executable_after_creation {
288-
let old_perm = std::fs::symlink_metadata(path)?.permissions();
289-
if let Some(new_perm) = set_mode_executable(old_perm) {
290-
// TODO: If we keep `fchmod`, maybe `set_mode_executable` shouldn't use `std::fs::Permissions`.
291-
use std::os::unix::fs::PermissionsExt;
292-
#[allow(clippy::useless_conversion)] // mode_t is u32 on many but not all OSes. It's u16 on macOS.
293-
let raw_mode = new_perm.mode().try_into().expect("mode fits in `st_mode`");
294-
let mode = rustix::fs::Mode::from_bits(raw_mode)
295-
.expect("`set_mode_executable` shouldn't preserve or add unknown bits");
296-
rustix::fs::fchmod(&file, mode).map_err(std::io::Error::from)?;
297-
}
288+
let old_raw_mode = rustix::fs::fstat(&file).map_err(std::io::Error::from)?.st_mode;
289+
let new_mode = let_readers_execute(old_raw_mode);
290+
rustix::fs::fchmod(&file, new_mode).map_err(std::io::Error::from)?;
298291
}
299292
// NOTE: we don't call `file.sync_all()` here knowing that some filesystems don't handle this well.
300293
// revisit this once there is a bug to fix.
@@ -303,102 +296,93 @@ pub(crate) fn finalize_entry(
303296
Ok(())
304297
}
305298

299+
/// Given the st_mode of a regular file, compute the mode with executable bits safely added.
300+
///
301+
/// Currently this adds executable bits for whoever has read bits already. It doesn't use the umask.
302+
/// Set-user-ID and set-group-ID bits are unset for safety. The sticky bit is also unset.
303+
///
304+
/// This returns only mode bits, not file type. The return value can be passed to chmod or fchmod.
306305
#[cfg(unix)]
307-
fn set_mode_executable(mut perm: std::fs::Permissions) -> Option<std::fs::Permissions> {
308-
use std::os::unix::fs::PermissionsExt;
309-
let mut mode = perm.mode();
310-
if mode & 0o170000 != 0o100000 {
311-
return None; // Stop if we don't have a regular file anymore.
312-
}
313-
mode &= 0o777; // Clear non-rwx bits (setuid, setgid, sticky).
314-
mode |= (mode & 0o444) >> 2; // Let readers also execute.
315-
perm.set_mode(mode);
316-
Some(perm)
306+
fn let_readers_execute(mut raw_mode: rustix::fs::RawMode) -> rustix::fs::Mode {
307+
assert_eq!(
308+
raw_mode & 0o170000,
309+
0o100000,
310+
"bug in caller if not from a regular file"
311+
);
312+
raw_mode &= 0o777; // Clear type, non-rwx mode bits (setuid, setgid, sticky).
313+
raw_mode |= (raw_mode & 0o444) >> 2; // Let readers also execute.
314+
rustix::fs::Mode::from_bits(raw_mode).expect("all bits recognized")
317315
}
318316

319317
#[cfg(all(test, unix))]
320318
mod tests {
321-
fn pretty(maybe_mode: Option<u32>) -> String {
322-
match maybe_mode {
323-
Some(mode) => format!("Some({mode:04o})"),
324-
None => "None".into(),
325-
}
326-
}
327-
328319
#[test]
329-
fn set_mode_executable() {
320+
fn let_readers_execute() {
330321
let cases = [
331322
// Common cases:
332-
(0o100755, Some(0o755)),
333-
(0o100644, Some(0o755)),
334-
(0o100750, Some(0o750)),
335-
(0o100640, Some(0o750)),
336-
(0o100700, Some(0o700)),
337-
(0o100600, Some(0o700)),
338-
(0o100775, Some(0o775)),
339-
(0o100664, Some(0o775)),
340-
(0o100770, Some(0o770)),
341-
(0o100660, Some(0o770)),
342-
(0o100764, Some(0o775)),
343-
(0o100760, Some(0o770)),
323+
(0o100755, 0o755),
324+
(0o100644, 0o755),
325+
(0o100750, 0o750),
326+
(0o100640, 0o750),
327+
(0o100700, 0o700),
328+
(0o100600, 0o700),
329+
(0o100775, 0o775),
330+
(0o100664, 0o775),
331+
(0o100770, 0o770),
332+
(0o100660, 0o770),
333+
(0o100764, 0o775),
334+
(0o100760, 0o770),
344335
// Less common:
345-
(0o100674, Some(0o775)),
346-
(0o100670, Some(0o770)),
347-
(0o100000, Some(0o000)),
348-
(0o100400, Some(0o500)),
349-
(0o100440, Some(0o550)),
350-
(0o100444, Some(0o555)),
351-
(0o100462, Some(0o572)),
352-
(0o100242, Some(0o252)),
353-
(0o100167, Some(0o177)),
336+
(0o100674, 0o775),
337+
(0o100670, 0o770),
338+
(0o100000, 0o000),
339+
(0o100400, 0o500),
340+
(0o100440, 0o550),
341+
(0o100444, 0o555),
342+
(0o100462, 0o572),
343+
(0o100242, 0o252),
344+
(0o100167, 0o177),
354345
// With set-user-ID, set-group-ID, and sticky bits:
355-
(0o104755, Some(0o755)),
356-
(0o104644, Some(0o755)),
357-
(0o102755, Some(0o755)),
358-
(0o102644, Some(0o755)),
359-
(0o101755, Some(0o755)),
360-
(0o101644, Some(0o755)),
361-
(0o106755, Some(0o755)),
362-
(0o106644, Some(0o755)),
363-
(0o104750, Some(0o750)),
364-
(0o104640, Some(0o750)),
365-
(0o102750, Some(0o750)),
366-
(0o102640, Some(0o750)),
367-
(0o101750, Some(0o750)),
368-
(0o101640, Some(0o750)),
369-
(0o106750, Some(0o750)),
370-
(0o106640, Some(0o750)),
371-
(0o107644, Some(0o755)),
372-
(0o107000, Some(0o000)),
373-
(0o106400, Some(0o500)),
374-
(0o102462, Some(0o572)),
375-
// Where it was replaced with a directory due to a race:
376-
(0o040755, None),
377-
(0o040644, None),
378-
(0o040600, None),
379-
(0o041755, None),
380-
(0o041644, None),
381-
(0o046644, None),
382-
// Where it was replaced with a symlink due to a race:
383-
(0o120777, None),
384-
(0o120644, None),
385-
// Where it was replaced with some other non-regular file due to a race:
386-
(0o140644, None),
387-
(0o060644, None),
388-
(0o020644, None),
389-
(0o010644, None),
346+
(0o104755, 0o755),
347+
(0o104644, 0o755),
348+
(0o102755, 0o755),
349+
(0o102644, 0o755),
350+
(0o101755, 0o755),
351+
(0o101644, 0o755),
352+
(0o106755, 0o755),
353+
(0o106644, 0o755),
354+
(0o104750, 0o750),
355+
(0o104640, 0o750),
356+
(0o102750, 0o750),
357+
(0o102640, 0o750),
358+
(0o101750, 0o750),
359+
(0o101640, 0o750),
360+
(0o106750, 0o750),
361+
(0o106640, 0o750),
362+
(0o107644, 0o755),
363+
(0o107000, 0o000),
364+
(0o106400, 0o500),
365+
(0o102462, 0o572),
390366
];
391-
for (old_mode, expected) in cases {
392-
use std::os::unix::fs::PermissionsExt;
393-
let old_perm = std::fs::Permissions::from_mode(old_mode);
394-
let actual = super::set_mode_executable(old_perm).map(|perm| perm.mode());
367+
for (st_mode, raw_expected) in cases {
368+
let expected = rustix::fs::Mode::from_bits(raw_expected).expect("expected mode is a mode");
369+
let actual = super::let_readers_execute(st_mode);
395370
assert_eq!(
396-
actual,
397-
expected,
398-
"{old_mode:06o} should become {}, became {}",
399-
pretty(expected),
400-
pretty(actual)
371+
actual, expected,
372+
"{st_mode:06o} should become {expected:04o}, became {actual:04o}"
401373
);
402374
}
403375
}
376+
377+
#[test]
378+
#[should_panic]
379+
fn let_readers_execute_panics_on_directory() {
380+
super::let_readers_execute(0o040644);
381+
}
382+
383+
#[test]
384+
#[should_panic]
385+
fn let_readers_execute_should_panic_on_symlink() {
386+
super::let_readers_execute(0o120644);
387+
}
404388
}

0 commit comments

Comments
 (0)