Skip to content

Commit dd2832c

Browse files
Copilotjgarzik
andcommitted
Fix from_str_radix misuse in user input parsing
Co-authored-by: jgarzik <494411+jgarzik@users.noreply.github.com>
1 parent b0fa034 commit dd2832c

File tree

8 files changed

+73
-9
lines changed

8 files changed

+73
-9
lines changed

display/printf.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,10 @@ fn parse_integer_arg(arg: &[u8]) -> Result<(i64, bool), String> {
840840
.or_else(|| num_str.strip_prefix("0X"))
841841
{
842842
// Hexadecimal
843+
// Reject if hex part starts with a sign (after we already handled the sign above)
844+
if hex.starts_with('+') || hex.starts_with('-') {
845+
return Err(format!("invalid number: {arg_str}"));
846+
}
843847
match i64::from_str_radix(hex, 16) {
844848
Ok(n) => (n, true),
845849
Err(_) => {
@@ -863,6 +867,10 @@ fn parse_integer_arg(arg: &[u8]) -> Result<(i64, bool), String> {
863867
.unwrap_or(false)
864868
{
865869
// Octal
870+
// Reject if octal part starts with a sign (after we already handled the sign above)
871+
if num_str.starts_with('+') || num_str.starts_with('-') {
872+
return Err(format!("invalid number: {arg_str}"));
873+
}
866874
let valid_len = num_str
867875
.chars()
868876
.take_while(|c| ('0'..='7').contains(c))

display/tests/printf/mod.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,3 +532,26 @@ fn test_float_missing_arg() {
532532
fn test_float_char_constant() {
533533
printf_test(&["%f", "'A"], "65.000000");
534534
}
535+
536+
// Regression tests for from_str_radix security issue
537+
// These should fail because double signs are invalid
538+
#[test]
539+
#[should_panic]
540+
fn test_reject_double_plus_hex() {
541+
// This should fail: "+0x+55" should be rejected, not parsed as 0x55
542+
printf_test(&["%d", "+0x+55"], "85");
543+
}
544+
545+
#[test]
546+
#[should_panic]
547+
fn test_reject_double_minus_hex() {
548+
// This should fail: "-0x-55" should be rejected
549+
printf_test(&["%d", "-0x-55"], "-85");
550+
}
551+
552+
#[test]
553+
#[should_panic]
554+
fn test_reject_double_plus_octal() {
555+
// This should fail: "+0+55" should be rejected
556+
printf_test(&["%d", "+0+55"], "45");
557+
}

file/find.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -476,12 +476,14 @@ fn parse_perm_mode(mode_str: &str) -> Result<PermMode, String> {
476476

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

487489
// Parse as symbolic mode starting from 0

file/od.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,19 @@ fn parse_count<T: FromStr<Err = ParseIntError> + FromStrRadix>(
219219
count: &str,
220220
) -> Result<T, Box<dyn std::error::Error>> {
221221
if count.starts_with("0x") || count.starts_with("0X") {
222-
T::from_str_radix(&count[2..], 16).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
222+
let hex_part = &count[2..];
223+
// Reject if hex part contains a sign
224+
if hex_part.starts_with('+') || hex_part.starts_with('-') {
225+
return Err("invalid hexadecimal number".into());
226+
}
227+
T::from_str_radix(hex_part, 16).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
223228
} else if count.starts_with('0') && count.len() > 1 {
224-
T::from_str_radix(&count[1..], 8).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
229+
let oct_part = &count[1..];
230+
// Reject if octal part contains a sign
231+
if oct_part.starts_with('+') || oct_part.starts_with('-') {
232+
return Err("invalid octal number".into());
233+
}
234+
T::from_str_radix(oct_part, 8).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
225235
} else {
226236
count
227237
.parse::<T>()
@@ -279,6 +289,11 @@ fn parse_offset(offset: &str) -> Result<u64, ParseIntError> {
279289
offset
280290
};
281291

292+
// Reject if offset contains a sign (offsets should be unsigned)
293+
if offset.starts_with('+') || offset.starts_with('-') {
294+
return Err("invalid offset".parse::<u64>().unwrap_err());
295+
}
296+
282297
let parsed_offset = u64::from_str_radix(offset, base)?;
283298

284299
Ok(parsed_offset * multiplier)

plib/src/modestr.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,11 @@ enum ParseState {
7272
}
7373

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

7982
let mut done_with_char;

sh/builtin/umask.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ impl BuiltinUtility for Umask {
5656
} else if option_parser.next_argument() == args.len() - 1 {
5757
// TODO: support symbolic umask
5858
let new_umask = &args[option_parser.next_argument()];
59+
// Reject if umask contains a sign
60+
if new_umask.starts_with('+') || new_umask.starts_with('-') {
61+
return Err(format!("umask: invalid mask '{new_umask}'").into());
62+
}
5963
let new_umask = u32::from_str_radix(new_umask, 8)
6064
.map_err(|_| format!("umask: invalid mask '{new_umask}'"))?;
6165
if new_umask > 0o777 {

sys/ipcrm.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ use libc::{semctl, semget, shmctl, shmget, shmid_ds};
2020
/// Parse an IPC key value that may be decimal or hexadecimal (0x prefix)
2121
fn parse_ipc_key(s: &str) -> Result<i32, String> {
2222
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
23+
// Reject if hex part starts with a sign
24+
if hex.starts_with('+') || hex.starts_with('-') {
25+
return Err("invalid hex key: unexpected sign after 0x prefix".to_string());
26+
}
2327
u32::from_str_radix(hex, 16)
2428
.map(|v| v as i32)
2529
.map_err(|e| format!("invalid hex key: {}", e))

text/tr.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,11 @@ mod parsing {
625625
let char_repetition = match repeat_string.as_str() {
626626
"" => CharRepetition::AsManyAsNeeded,
627627
st => {
628+
// Reject if repeat count starts with a sign
629+
if st.starts_with('+') || st.starts_with('-') {
630+
return Err(format!("invalid repeat count '{st}' in [c*n] construct",));
631+
}
632+
628633
let radix = if st.starts_with('0') {
629634
// Octal
630635
8_u32

0 commit comments

Comments
 (0)