From 2d5e77deb2b0c3b5b7078f6fb7d631fe7723544e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BCp=20Can=20Akman?= Date: Wed, 15 Apr 2026 13:39:12 +0300 Subject: [PATCH] expr: avoid panic on regex retry-limit Treat onig retry-limit-in-match errors as no match. GNU expr returns exit 1 for the same input. Fixes #11612 --- src/uu/expr/src/syntax_tree.rs | 53 +++++++++++++++------------------- tests/by-util/test_expr.rs | 9 ++++++ 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/uu/expr/src/syntax_tree.rs b/src/uu/expr/src/syntax_tree.rs index 22d0bde13..2c2599a84 100644 --- a/src/uu/expr/src/syntax_tree.rs +++ b/src/uu/expr/src/syntax_tree.rs @@ -9,7 +9,7 @@ use std::{cell::Cell, collections::BTreeMap}; use num_bigint::BigInt; use num_traits::ToPrimitive; -use onig::{Regex, RegexOptions, Syntax}; +use onig::{MatchParam, Regex, RegexOptions, SearchOptions, Syntax}; use crate::{ ExprError, ExprResult, @@ -366,6 +366,25 @@ fn build_regex(pattern_bytes: Vec) -> ExprResult<(Regex, String)> { Ok((re, re_string)) } +/// Run a regex search, treating runtime match errors as no match. +fn regex_search( + regex: &Regex, + chars: T, + to: usize, + region: &mut onig::Region, +) -> Option { + regex + .search_with_param( + chars, + 0, + to, + SearchOptions::SEARCH_OPTION_NONE, + Some(region), + MatchParam::default(), + ) + .unwrap_or(None) +} + /// Find matches in the input using the compiled regex fn find_match(regex: Regex, re_string: String, left_bytes: Vec) -> String { use onig::EncodedBytes; @@ -380,13 +399,7 @@ fn find_match(regex: Regex, re_string: String, left_bytes: Vec) -> String { // In UTF-8 locale, check if input is valid UTF-8 if let Ok(left_str) = std::str::from_utf8(&left_bytes) { // Valid UTF-8, match as UTF-8 - let pos = regex.search_with_encoding( - left_str, - 0, - left_str.len(), - onig::SearchOptions::SEARCH_OPTION_NONE, - Some(&mut region), - ); + let pos = regex_search(®ex, left_str, left_str.len(), &mut region); if pos.is_some() { if regex.captures_len() > 0 { @@ -421,13 +434,7 @@ fn find_match(regex: Regex, re_string: String, left_bytes: Vec) -> String { .ok(); if let Some(re_ascii) = re_ascii { - let pos = re_ascii.search_with_encoding( - left_encoded, - 0, - left_bytes.len(), - onig::SearchOptions::SEARCH_OPTION_NONE, - Some(&mut region), - ); + let pos = regex_search(&re_ascii, left_encoded, left_bytes.len(), &mut region); if pos.is_some() { if re_ascii.captures_len() > 0 { @@ -469,13 +476,7 @@ fn find_match(regex: Regex, re_string: String, left_bytes: Vec) -> String { UEncoding::Ascii => { // In ASCII/C locale, work with bytes directly let left_encoded = EncodedBytes::ascii(&left_bytes); - let pos = regex.search_with_encoding( - left_encoded, - 0, - left_bytes.len(), - onig::SearchOptions::SEARCH_OPTION_NONE, - Some(&mut region), - ); + let pos = regex_search(®ex, left_encoded, left_bytes.len(), &mut region); if pos.is_some() { if regex.captures_len() > 0 { @@ -515,13 +516,7 @@ fn evaluate_match_expression(left_bytes: Vec, right_bytes: Vec) -> ExprR // Try to find the actual capture bytes for ASCII locale let mut region = onig::Region::new(); let left_encoded = onig::EncodedBytes::ascii(&left_bytes); - let pos = regex.search_with_encoding( - left_encoded, - 0, - left_bytes.len(), - onig::SearchOptions::SEARCH_OPTION_NONE, - Some(&mut region), - ); + let pos = regex_search(®ex, left_encoded, left_bytes.len(), &mut region); if pos.is_some() { if let Some((start, end)) = region.pos(1) { diff --git a/tests/by-util/test_expr.rs b/tests/by-util/test_expr.rs index c95465af7..dc051318f 100644 --- a/tests/by-util/test_expr.rs +++ b/tests/by-util/test_expr.rs @@ -488,6 +488,15 @@ fn test_regex_newline() { .stdout_only("18\n"); } +#[test] +fn test_regex_catastrophic_backtracking() { + let input = "a".repeat(30) + "c"; + new_ucmd!() + .args(&[input.as_str(), ":", "\\(a\\+a\\+\\)\\+b"]) + .fails_with_code(1) + .stdout_only("\n"); +} + #[test] fn test_substr() { new_ucmd!()