1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

chmod: implement special handling of directory setuid/setgid

This commit is contained in:
Michael Debertol 2021-08-16 21:56:09 +02:00
parent 9697c89d17
commit 15b40f6aa2
4 changed files with 66 additions and 36 deletions

View file

@ -282,7 +282,7 @@ impl Chmoder {
// cmode is guaranteed to be Some in this case // cmode is guaranteed to be Some in this case
let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
let result = if mode.contains(arr) { let result = if mode.contains(arr) {
mode::parse_numeric(fperm, mode) mode::parse_numeric(fperm, mode, file.is_dir())
} else { } else {
mode::parse_symbolic(fperm, mode, file.is_dir()) mode::parse_symbolic(fperm, mode, file.is_dir())
}; };

View file

@ -9,7 +9,7 @@ pub fn parse(mode_string: &str, considering_dir: bool) -> Result<u32, String> {
// Passing 000 as the existing permissions seems to mirror GNU behavior. // Passing 000 as the existing permissions seems to mirror GNU behavior.
if mode_string.contains(numbers) { if mode_string.contains(numbers) {
mode::parse_numeric(0, mode_string) mode::parse_numeric(0, mode_string, considering_dir)
} else { } else {
mode::parse_symbolic(0, mode_string, considering_dir) mode::parse_symbolic(0, mode_string, considering_dir)
} }

View file

@ -9,23 +9,26 @@
use libc::{mode_t, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR}; use libc::{mode_t, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR};
pub fn parse_numeric(fperm: u32, mut mode: &str) -> Result<u32, String> { pub fn parse_numeric(fperm: u32, mut mode: &str, considering_dir: bool) -> Result<u32, String> {
let (op, pos) = parse_op(mode, Some('='))?; let (op, pos) = parse_op(mode).map_or_else(|_| (None, 0), |(op, pos)| (Some(op), pos));
mode = mode[pos..].trim(); mode = mode[pos..].trim();
match u32::from_str_radix(mode, 8) { let change = if mode.is_empty() {
Ok(change) => { 0
if change > 0o7777 { } else {
Err(format!("mode is too large ({} > 7777", mode)) u32::from_str_radix(mode, 8).map_err(|e| e.to_string())?
} else { };
Ok(match op { if change > 0o7777 {
'+' => fperm | change, Err(format!("mode is too large ({} > 7777", change))
'-' => fperm & !change, } else {
'=' => change, Ok(match op {
_ => unreachable!(), Some('+') => fperm | change,
}) Some('-') => fperm & !change,
} // If this is a directory, we keep the setgid and setuid bits,
} // unless the mode contains 5 or more octal digits or the mode is "="
Err(err) => Err(err.to_string()), None if considering_dir && mode.len() < 5 => change | (fperm & (0o4000 | 0o2000)),
None | Some('=') => change,
Some(_) => unreachable!(),
})
} }
} }
@ -45,7 +48,7 @@ pub fn parse_symbolic(
let last_umask = unsafe { umask(0) }; let last_umask = unsafe { umask(0) };
mode = &mode[pos..]; mode = &mode[pos..];
while !mode.is_empty() { while !mode.is_empty() {
let (op, pos) = parse_op(mode, None)?; let (op, pos) = parse_op(mode)?;
mode = &mode[pos..]; mode = &mode[pos..];
let (mut srwx, pos) = parse_change(mode, fperm, considering_dir); let (mut srwx, pos) = parse_change(mode, fperm, considering_dir);
if respect_umask { if respect_umask {
@ -55,7 +58,13 @@ pub fn parse_symbolic(
match op { match op {
'+' => fperm |= srwx & mask, '+' => fperm |= srwx & mask,
'-' => fperm &= !(srwx & mask), '-' => fperm &= !(srwx & mask),
'=' => fperm = (fperm & !mask) | (srwx & mask), '=' => {
if considering_dir {
// keep the setgid and setuid bits for directories
srwx |= fperm & (0o4000 | 0o2000);
}
fperm = (fperm & !mask) | (srwx & mask)
}
_ => unreachable!(), _ => unreachable!(),
} }
} }
@ -70,9 +79,9 @@ fn parse_levels(mode: &str) -> (u32, usize) {
let mut pos = 0; let mut pos = 0;
for ch in mode.chars() { for ch in mode.chars() {
mask |= match ch { mask |= match ch {
'u' => 0o7700, 'u' => 0o4700,
'g' => 0o7070, 'g' => 0o2070,
'o' => 0o7007, 'o' => 0o1007,
'a' => 0o7777, 'a' => 0o7777,
_ => break, _ => break,
}; };
@ -84,24 +93,22 @@ fn parse_levels(mode: &str) -> (u32, usize) {
(mask, pos) (mask, pos)
} }
fn parse_op(mode: &str, default: Option<char>) -> Result<(char, usize), String> { fn parse_op(mode: &str) -> Result<(char, usize), String> {
let ch = mode let ch = mode
.chars() .chars()
.next() .next()
.ok_or_else(|| "unexpected end of mode".to_owned())?; .ok_or_else(|| "unexpected end of mode".to_owned())?;
Ok(match ch { match ch {
'+' | '-' | '=' => (ch, 1), '+' | '-' | '=' => Ok((ch, 1)),
_ => { _ => Err(format!(
let ch = default.ok_or_else(|| { "invalid operator (expected +, -, or =, but found {})",
format!("invalid operator (expected +, -, or =, but found {})", ch) ch
})?; )),
(ch, 0) }
}
})
} }
fn parse_change(mode: &str, fperm: u32, considering_dir: bool) -> (u32, usize) { fn parse_change(mode: &str, fperm: u32, considering_dir: bool) -> (u32, usize) {
let mut srwx = fperm & 0o7000; let mut srwx = 0;
let mut pos = 0; let mut pos = 0;
for ch in mode.chars() { for ch in mode.chars() {
match ch { match ch {
@ -132,7 +139,7 @@ pub fn parse_mode(mode: &str) -> Result<mode_t, String> {
let fperm = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; let fperm = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
let result = if mode.contains(arr) { let result = if mode.contains(arr) {
parse_numeric(fperm as u32, mode) parse_numeric(fperm as u32, mode, true)
} else { } else {
parse_symbolic(fperm as u32, mode, true) parse_symbolic(fperm as u32, mode, true)
}; };

View file

@ -1,5 +1,5 @@
use crate::common::util::*; use crate::common::util::*;
use std::fs::{metadata, set_permissions, OpenOptions}; use std::fs::{metadata, set_permissions, OpenOptions, Permissions};
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::sync::Mutex; use std::sync::Mutex;
@ -490,3 +490,26 @@ fn test_chmod_strip_minus_from_mode() {
assert_eq!(test.1, args.join(" ")); assert_eq!(test.1, args.join(" "));
} }
} }
#[test]
fn test_chmod_keep_setgid() {
for &(from, arg, to) in &[
(0o7777, "777", 0o46777),
(0o7777, "=777", 0o40777),
(0o7777, "0777", 0o46777),
(0o7777, "=0777", 0o40777),
(0o7777, "00777", 0o40777),
(0o2444, "a+wx", 0o42777),
(0o2444, "a=wx", 0o42333),
(0o1444, "g+s", 0o43444),
(0o4444, "u-s", 0o40444),
(0o7444, "a-s", 0o41444),
] {
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir("dir");
set_permissions(at.plus("dir"), Permissions::from_mode(from)).unwrap();
let r = ucmd.arg(arg).arg("dir").succeeds();
println!("{}", r.stderr_str());
assert_eq!(at.metadata("dir").permissions().mode(), to);
}
}