terminal: bound link regex search work with Oniguruma retry limits (#11181)

Fixes #11177

Use per-search Oniguruma match params (retry_limit_in_search) in
StringMap-backed link detection to avoid pathological backtracking hangs
on very long lines.

The units are ticks in the internal loop so its kind of opaque but this
seems to still match some very long URLs. The test case in question was
a 169K character line (which is now rejected).
This commit is contained in:
Mitchell Hashimoto
2026-03-04 21:09:10 -08:00
committed by GitHub
4 changed files with 117 additions and 12 deletions

View File

@@ -11,6 +11,12 @@ const Screen = @import("Screen.zig");
const Pin = @import("PageList.zig").Pin;
const Allocator = std.mem.Allocator;
// Retry budget for StringMap regex searches.
//
// Units are Oniguruma retry steps (internal backtracking/retry counter),
// not bytes/characters/time.
const oni_search_retry_limit = 100_000;
string: [:0]const u8,
map: []Pin,
@@ -44,11 +50,26 @@ pub const SearchIterator = struct {
pub fn next(self: *SearchIterator) !?Match {
if (self.offset >= self.map.string.len) return null;
var region = self.regex.search(
// Use per-search match params so we can bound regex retry steps
// (Oniguruma's internal backtracking work counter).
var match_param = try oni.MatchParam.init();
defer match_param.deinit();
try match_param.setRetryLimitInSearch(oni_search_retry_limit);
var region = self.regex.searchWithParam(
self.map.string[self.offset..],
.{},
&match_param,
) catch |err| switch (err) {
error.Mismatch => {
// Retry/stack-limit errors mean we hit our work budget and
// aborted matching.
// For iterator callers this is equivalent to "no further matches".
error.Mismatch,
error.RetryLimitInMatchOver,
error.RetryLimitInSearchOver,
error.MatchStackLimitOver,
error.SubexpCallLimitInSearchOver,
=> {
self.offset = self.map.string.len;
return null;
},