From 1bb9ea7e417e7b5247af171304775c8e9b441682 Mon Sep 17 00:00:00 2001 From: weili <541602953@qq.com> Date: Fri, 5 Jun 2026 05:14:30 +0000 Subject: [PATCH] stat: fix char-boundary panic on an invalid directive after a multibyte char MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `stat -c '€%-'` (a multibyte char immediately before an invalid `%` directive) aborted with "byte index N is not a char boundary". The format parser tracks positions as char indices, but `check_bound` built its "invalid directive" message by byte-slicing the format string, so a preceding multibyte char made the byte index land mid-UTF-8. Build the directive substring by chars instead of byte-slicing, so the invalid-directive error is reported (exit 1) like GNU instead of crashing. --- src/uu/stat/src/stat.rs | 5 ++++- tests/by-util/test_stat.rs | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index daa9ba472f1..610dac9f6e0 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -82,10 +82,13 @@ struct Flags { /// where `beg` & `end` is the beginning and end index of sub-string, respectively fn check_bound(slice: &str, bound: usize, beg: usize, end: usize) -> UResult<()> { if end >= bound { + // `beg`/`end` are char indices, so take the directive by chars: byte-slicing + // `slice` could land mid-UTF-8 when a multibyte char precedes the directive. + let directive: String = slice.chars().skip(beg).take(end - beg).collect(); return Err(USimpleError::new( 1, StatError::InvalidDirective { - directive: slice[beg..end].quote().to_string(), + directive: directive.quote().to_string(), } .to_string(), )); diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index 8802c3a17b2..d91ee8b60ed 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -582,6 +582,17 @@ fn test_printf_invalid_directive() { .stderr_contains("'%9%': invalid directive"); } +#[test] +fn test_invalid_directive_after_multibyte_char() { + let ts = TestScenario::new(util_name!()); + for (fmt, directive) in [("€%-", "%-"), ("ä%0", "%0"), ("€%.", "%.")] { + ts.ucmd() + .args(&["-c", fmt, "."]) + .fails_with_code(1) + .stderr_only(format!("stat: '{directive}': invalid directive\n")); + } +} + #[test] #[cfg(all( feature = "feat_selinux",