From 71a907944428112c44ed08c7b285f74b210e62fd Mon Sep 17 00:00:00 2001 From: zfaustk <4340287+zfaustk@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:58:36 +0800 Subject: [PATCH] printf: reject malformed hex and unicode escapes in %b arguments In %b arguments, malformed \x, \u, and \U escape sequences were previously swallowed by parse_escape_only, printing a literal \x and exiting 0 while dropping consumed characters in \u and \U. Make parse_escape_only return Result and propagate errors from %b argument parsing so printf reports missing hex numbers and invalid universal character names with exit code 1 to match GNU coreutils. Also ensure parse_unicode only consumes input when hex digits are present. Fixes #14404 --- src/uu/echo/src/echo.rs | 8 +++++- src/uu/printf/src/printf.rs | 4 +-- src/uucore/src/lib/features/format/escape.rs | 7 +++-- src/uucore/src/lib/features/format/mod.rs | 9 ++---- src/uucore/src/lib/features/format/spec.rs | 2 +- tests/by-util/test_printf.rs | 30 ++++++++++++++++++++ 6 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/uu/echo/src/echo.rs b/src/uu/echo/src/echo.rs index 8a76155dae2..17155d72251 100644 --- a/src/uu/echo/src/echo.rs +++ b/src/uu/echo/src/echo.rs @@ -9,7 +9,7 @@ use std::env; use std::ffi::{OsStr, OsString}; use std::io::{StdoutLock, Write, stdout}; use uucore::error::UResult; -use uucore::format::{FormatChar, OctalParsing, parse_escape_only}; +use uucore::format::{EscapedChar, FormatChar, FormatError, OctalParsing, parse_escape_only}; use uucore::{crate_version, format_usage, os_str_as_bytes}; use uucore::translate; @@ -239,6 +239,12 @@ fn execute( if options.escape { for item in parse_escape_only(bytes, OctalParsing::ThreeDigits) { + let item = match item { + Ok(c) => c, + Err(FormatError::MissingHex(_)) => EscapedChar::Backslash(b'x'), + Err(FormatError::InvalidCharacter(c, _, _)) => EscapedChar::Backslash(c as u8), + Err(_) => EscapedChar::Byte(b'\\'), + }; if item.write(&mut *stdout)?.is_break() { return Ok(()); } diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index 3666400ea0a..0e849bad475 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -75,7 +75,7 @@ fn print_formatted(args: impl uucore::Args) -> UResult<()> { if let Ok(FormatItem::Spec(_)) = item { format_seen = true; } - match item.map_err(&raise)?.write(stdout(), &mut args)? { + match item.map_err(&raise)?.write(stdout(), &mut args).map_err(&raise)? { ControlFlow::Continue(()) => {} ControlFlow::Break(()) => return Ok(()), } @@ -102,7 +102,7 @@ fn print_formatted(args: impl uucore::Args) -> UResult<()> { while !args.is_exhausted() { for item in parse_spec_and_escape(format) { - match item.map_err(&raise)?.write(stdout(), &mut args)? { + match item.map_err(&raise)?.write(stdout(), &mut args).map_err(&raise)? { ControlFlow::Continue(()) => {} ControlFlow::Break(()) => return Ok(()), } diff --git a/src/uucore/src/lib/features/format/escape.rs b/src/uucore/src/lib/features/format/escape.rs index 75d44a36e2b..a6f94ef0519 100644 --- a/src/uucore/src/lib/features/format/escape.rs +++ b/src/uucore/src/lib/features/format/escape.rs @@ -94,12 +94,13 @@ fn parse_code(input: &mut &[u8], base: Base) -> Option { /// Parse `\uHHHH` and `\UHHHHHHHH` fn parse_unicode(input: &mut &[u8], digits: u8) -> Result { if let Some((new_digits, rest)) = input.split_at_checked(digits as usize) { - *input = rest; - let ret = new_digits + let hex_bytes = new_digits .iter() .map(|c| Base::Hex.convert_digit(*c)) .collect::>>() - .ok_or(EscapeError::MissingHexadecimalNumber)? + .ok_or(EscapeError::MissingHexadecimalNumber)?; + *input = rest; + let ret = hex_bytes .iter() .map(|n| *n as u32) .reduce(|ret, n| ret.wrapping_mul(Base::Hex.as_base() as u32).wrapping_add(n)) diff --git a/src/uucore/src/lib/features/format/mod.rs b/src/uucore/src/lib/features/format/mod.rs index 7524e958eea..0b8c90c037c 100644 --- a/src/uucore/src/lib/features/format/mod.rs +++ b/src/uucore/src/lib/features/format/mod.rs @@ -318,20 +318,17 @@ pub fn parse_spec_only( pub fn parse_escape_only( fmt: &[u8], zero_octal_parsing: OctalParsing, -) -> impl Iterator + '_ { +) -> impl Iterator> + '_ { let mut current = fmt; std::iter::from_fn(move || match current { [] => None, [b'\\', rest @ ..] => { current = rest; - Some( - parse_escape_code(&mut current, zero_octal_parsing) - .unwrap_or(EscapedChar::Backslash(b'x')), - ) + Some(parse_escape_code(&mut current, zero_octal_parsing)) } [c, rest @ ..] => { current = rest; - Some(EscapedChar::Byte(*c)) + Some(Ok(EscapedChar::Byte(*c))) } }) } diff --git a/src/uucore/src/lib/features/format/spec.rs b/src/uucore/src/lib/features/format/spec.rs index d2169ba7e24..316f611a4ee 100644 --- a/src/uucore/src/lib/features/format/spec.rs +++ b/src/uucore/src/lib/features/format/spec.rs @@ -389,7 +389,7 @@ impl Spec { let mut parsed = Vec::::new(); for c in parse_escape_only(bytes, OctalParsing::ThreeDigits) { - match c.write(&mut parsed)? { + match c?.write(&mut parsed)? { ControlFlow::Continue(()) => {} ControlFlow::Break(()) => { // A `\c` inside the argument stops output for the diff --git a/tests/by-util/test_printf.rs b/tests/by-util/test_printf.rs index 2e6cde0337d..2eae72e6524 100644 --- a/tests/by-util/test_printf.rs +++ b/tests/by-util/test_printf.rs @@ -112,6 +112,36 @@ fn sub_b_string_handle_escapes() { .stdout_only("hello \tworld"); } +#[test] +fn sub_b_string_missing_hex() { + for arg in [ + r"\x", + r"\u", + r"\U", + r"\uabc", + r"\Uabcd", + "C:\\users\\file", + r"\uABCZ|TAIL", + r"\U0000004Z|TAIL", + r"x\unit", + ] { + new_ucmd!() + .args(&["%b", arg]) + .fails_with_code(1) + .stderr_only("printf: missing hexadecimal number in escape\n"); + } +} + +#[test] +fn sub_b_string_invalid_unicode() { + for arg in [r"\ud800", r"\ud9d0", r"\U0000D8F9"] { + new_ucmd!() + .args(&["%b", arg]) + .fails_with_code(1) + .stderr_only(format!("printf: invalid universal character name {arg}\n")); + } +} + #[test] fn sub_b_string_variable_size_unicode() { for x in ["\\5|", "\\05|", "\\005|", "\\0005|"] {