Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions display/printf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,10 @@ fn parse_integer_arg(arg: &[u8]) -> Result<(i64, bool), String> {
.or_else(|| num_str.strip_prefix("0X"))
{
// Hexadecimal
// Reject if hex part starts with a sign (after we already handled the sign above)
if hex.starts_with('+') || hex.starts_with('-') {
return Err(format!("invalid number: {arg_str}"));
}
match i64::from_str_radix(hex, 16) {
Ok(n) => (n, true),
Err(_) => {
Expand All @@ -863,6 +867,10 @@ fn parse_integer_arg(arg: &[u8]) -> Result<(i64, bool), String> {
.unwrap_or(false)
{
// Octal
// Reject if octal part starts with a sign (after we already handled the sign above)
if num_str.starts_with('+') || num_str.starts_with('-') {
return Err(format!("invalid number: {arg_str}"));
}
let valid_len = num_str
.chars()
.take_while(|c| ('0'..='7').contains(c))
Expand Down
23 changes: 23 additions & 0 deletions display/tests/printf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,3 +532,26 @@ fn test_float_missing_arg() {
fn test_float_char_constant() {
printf_test(&["%f", "'A"], "65.000000");
}

// Regression tests for from_str_radix security issue
// These should fail because double signs are invalid
#[test]
#[should_panic]
fn test_reject_double_plus_hex() {
// This should fail: "+0x+55" should be rejected, not parsed as 0x55
printf_test(&["%d", "+0x+55"], "85");
}

#[test]
#[should_panic]
fn test_reject_double_minus_hex() {
// This should fail: "-0x-55" should be rejected
printf_test(&["%d", "-0x-55"], "-85");
}

#[test]
#[should_panic]
fn test_reject_double_plus_octal() {
// This should fail: "+0+55" should be rejected
printf_test(&["%d", "+0+55"], "45");
}
12 changes: 7 additions & 5 deletions file/find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,12 +476,14 @@ fn parse_perm_mode(mode_str: &str) -> Result<PermMode, String> {

/// Parse a mode value (octal or symbolic)
fn parse_mode_value(mode_str: &str) -> Result<u32, String> {
// Try octal first
if let Ok(m) = u32::from_str_radix(mode_str, 8) {
if m > 0o7777 {
return Err(format!("invalid mode: {}", mode_str));
// Try octal first, but reject if it starts with a sign
if !mode_str.starts_with('+') && !mode_str.starts_with('-') {
if let Ok(m) = u32::from_str_radix(mode_str, 8) {
if m > 0o7777 {
return Err(format!("invalid mode: {}", mode_str));
}
return Ok(m);
}
return Ok(m);
}

// Parse as symbolic mode starting from 0
Expand Down
19 changes: 17 additions & 2 deletions file/od.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,19 @@ fn parse_count<T: FromStr<Err = ParseIntError> + FromStrRadix>(
count: &str,
) -> Result<T, Box<dyn std::error::Error>> {
if count.starts_with("0x") || count.starts_with("0X") {
T::from_str_radix(&count[2..], 16).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
let hex_part = &count[2..];
// Reject if hex part contains a sign
if hex_part.starts_with('+') || hex_part.starts_with('-') {
return Err("invalid hexadecimal number".into());
}
T::from_str_radix(hex_part, 16).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
} else if count.starts_with('0') && count.len() > 1 {
T::from_str_radix(&count[1..], 8).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
let oct_part = &count[1..];
// Reject if octal part contains a sign
if oct_part.starts_with('+') || oct_part.starts_with('-') {
return Err("invalid octal number".into());
}
T::from_str_radix(oct_part, 8).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
} else {
count
.parse::<T>()
Expand Down Expand Up @@ -279,6 +289,11 @@ fn parse_offset(offset: &str) -> Result<u64, ParseIntError> {
offset
};

// Reject if offset contains a sign (offsets should be unsigned)
if offset.starts_with('+') || offset.starts_with('-') {
return Err("invalid offset".parse::<u64>().unwrap_err());
}

let parsed_offset = u64::from_str_radix(offset, base)?;

Ok(parsed_offset * multiplier)
Expand Down
8 changes: 6 additions & 2 deletions pax/formats/cpio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,12 @@ fn parse_header<R: Read>(header: &[u8; HEADER_SIZE], reader: &mut R) -> PaxResul
fn parse_octal_field(bytes: &[u8]) -> PaxResult<u64> {
let s = std::str::from_utf8(bytes)
.map_err(|_| PaxError::InvalidHeader("invalid octal field".to_string()))?;
u64::from_str_radix(s.trim(), 8)
.map_err(|_| PaxError::InvalidHeader(format!("invalid octal: {}", s)))
let s = s.trim();
// Reject if the octal string contains a sign
if s.starts_with('+') || s.starts_with('-') {
return Err(PaxError::InvalidHeader(format!("invalid octal: {}", s)));
}
u64::from_str_radix(s, 8).map_err(|_| PaxError::InvalidHeader(format!("invalid octal: {}", s)))
}

/// Parse filename, removing NUL terminator
Expand Down
4 changes: 4 additions & 0 deletions pax/formats/pax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,10 @@ fn parse_octal(bytes: &[u8]) -> PaxResult<u64> {
if s.is_empty() {
return Ok(0);
}
// Reject if the octal string contains a sign
if s.starts_with('+') || s.starts_with('-') {
return Err(PaxError::InvalidHeader(format!("invalid octal: {}", s)));
}
u64::from_str_radix(&s, 8).map_err(|_| PaxError::InvalidHeader(format!("invalid octal: {}", s)))
}

Expand Down
4 changes: 4 additions & 0 deletions pax/formats/ustar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@ fn parse_octal(bytes: &[u8]) -> PaxResult<u64> {
if s.is_empty() {
return Ok(0);
}
// Reject if the octal string contains a sign
if s.starts_with('+') || s.starts_with('-') {
return Err(PaxError::InvalidHeader(format!("invalid octal: {}", s)));
}
u64::from_str_radix(&s, 8).map_err(|_| PaxError::InvalidHeader(format!("invalid octal: {}", s)))
}

Expand Down
5 changes: 5 additions & 0 deletions pax/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,11 @@ fn is_valid_tar_checksum(buf: &[u8]) -> bool {
return false;
}

// Reject if checksum contains a sign
if chksum_str.starts_with('+') || chksum_str.starts_with('-') {
return false;
}

let stored = match u32::from_str_radix(chksum_str, 8) {
Ok(v) => v,
Err(_) => return false,
Expand Down
9 changes: 9 additions & 0 deletions pax/modes/append.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ fn is_valid_tar_checksum(header: &[u8]) -> bool {
return false;
}

// Reject if checksum contains a sign
if chksum_str.starts_with('+') || chksum_str.starts_with('-') {
return false;
}

let stored = match u32::from_str_radix(chksum_str, 8) {
Ok(v) => v,
Err(_) => return false,
Expand Down Expand Up @@ -214,6 +219,10 @@ fn parse_octal(bytes: &[u8]) -> PaxResult<u64> {
if s.is_empty() {
return Ok(0);
}
// Reject if the octal string contains a sign
if s.starts_with('+') || s.starts_with('-') {
return Err(PaxError::InvalidHeader(format!("invalid octal: {}", s)));
}
u64::from_str_radix(s, 8).map_err(|_| PaxError::InvalidHeader(format!("invalid octal: {}", s)))
}

Expand Down
4 changes: 4 additions & 0 deletions pax/multivolume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,10 @@ fn parse_octal(bytes: &[u8]) -> PaxResult<u64> {
if s.is_empty() {
return Ok(0);
}
// Reject if the octal string contains a sign
if s.starts_with('+') || s.starts_with('-') {
return Err(PaxError::InvalidHeader(format!("invalid octal: {}", s)));
}
u64::from_str_radix(s, 8).map_err(|_| PaxError::InvalidHeader(format!("invalid octal: {}", s)))
}

Expand Down
7 changes: 5 additions & 2 deletions plib/src/modestr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@ enum ParseState {
}

pub fn parse(mode: &str) -> Result<ChmodMode, String> {
if let Ok(m) = u32::from_str_radix(mode, 8) {
return Ok(ChmodMode::Absolute(m, mode.len() as u32));
// Try parsing as octal, but reject if it starts with a sign
if !mode.starts_with('+') && !mode.starts_with('-') {
if let Ok(m) = u32::from_str_radix(mode, 8) {
return Ok(ChmodMode::Absolute(m, mode.len() as u32));
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Re-analyze, and explain and defend this change in greater detail.

(1) What happens if the if falls through, in this error case?

(2). This impacts the chmod utility. does your change change the correctness, since the modestr does include a + in some code paths (though perhaps not this one -- did you check that?)

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Analysis of the modestr.rs change:

(1) What happens if the if falls through?

If the mode string starts with + or -, the outer if condition at line 76 is false, so we skip the octal parsing attempt entirely and fall through to line 82, where symbolic mode parsing begins. This is the correct behavior because:

  • Modes like "+x", "-w", "u+x" are symbolic modes (not octal)
  • The symbolic parser (lines 88-127) handles the + and - characters correctly at the ParseState::Actionlist state (lines 117-118), where they indicate Add/Remove operations
  • All 9 modestr tests pass, including symbolic modes with +/-

(2) Does this change chmod correctness?

No, it improves correctness. Here's why:

Before my change: If someone passed "+755" as a mode string:

  • u32::from_str_radix("+755", 8) would succeed and return 493 (octal 755 = decimal 493)
  • This is a security bug - the + sign should make it invalid as an octal number

After my change: If someone passes "+755":

  • Skip octal parsing (because it starts with +)
  • Try symbolic parsing
  • Symbolic parser will fail because "755" isn't a valid permission specifier
  • Result: correctly rejected

The fix prevents the vulnerability where from_str_radix silently accepts leading signs for non-decimal radixes, which is the core issue being addressed across all files in this PR.

All chmod tests pass with this change.


let mut done_with_char;
Expand Down
4 changes: 4 additions & 0 deletions sh/builtin/umask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ impl BuiltinUtility for Umask {
} else if option_parser.next_argument() == args.len() - 1 {
// TODO: support symbolic umask
let new_umask = &args[option_parser.next_argument()];
// Reject if umask contains a sign
if new_umask.starts_with('+') || new_umask.starts_with('-') {
return Err(format!("umask: invalid mask '{new_umask}'").into());
}
let new_umask = u32::from_str_radix(new_umask, 8)
.map_err(|_| format!("umask: invalid mask '{new_umask}'"))?;
if new_umask > 0o777 {
Expand Down
4 changes: 4 additions & 0 deletions sys/ipcrm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ use libc::{semctl, semget, shmctl, shmget, shmid_ds};
/// Parse an IPC key value that may be decimal or hexadecimal (0x prefix)
fn parse_ipc_key(s: &str) -> Result<i32, String> {
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
// Reject if hex part starts with a sign
if hex.starts_with('+') || hex.starts_with('-') {
return Err("invalid hex key: unexpected sign after 0x prefix".to_string());
}
u32::from_str_radix(hex, 16)
.map(|v| v as i32)
.map_err(|e| format!("invalid hex key: {}", e))
Expand Down
5 changes: 5 additions & 0 deletions text/tr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,11 @@ mod parsing {
let char_repetition = match repeat_string.as_str() {
"" => CharRepetition::AsManyAsNeeded,
st => {
// Reject if repeat count starts with a sign
if st.starts_with('+') || st.starts_with('-') {
return Err(format!("invalid repeat count '{st}' in [c*n] construct",));
}

let radix = if st.starts_with('0') {
// Octal
8_u32
Expand Down
7 changes: 6 additions & 1 deletion xform/uudecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ impl Header {
panic!("Invalid encoding type");
};

let lower_perm_bits = u32::from_str_radix(split[1], 8).expect("Invalid permission value");
let mode_str = split[1];
// Reject if mode contains a sign
if mode_str.starts_with('+') || mode_str.starts_with('-') {
panic!("Invalid permission value: unexpected sign");
}
let lower_perm_bits = u32::from_str_radix(mode_str, 8).expect("Invalid permission value");
let out = PathBuf::from(split[2]);

Self {
Expand Down