diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py
index f37e0d273f..8dedd75c13 100644
--- a/backend/open_webui/tools/builtin.py
+++ b/backend/open_webui/tools/builtin.py
@@ -2462,6 +2462,7 @@ async def grep_chat_files(
"""
Search exact text across files attached to the current chat.
Pass file_id from the attached_files block to search one file.
+ Auto-detected regex uses RE2 syntax; no lookarounds/backreferences, and shorthand classes are ASCII-only.
:param pattern: The text pattern to search for
:param file_id: Optional attached file ID to search within a single file
@@ -2638,6 +2639,7 @@ async def grep_knowledge_files(
Search for exact text across knowledge files. Returns matching lines with line numbers.
Unlike query_knowledge_files (semantic/vector search), this performs exact string matching.
Automatically detects regex patterns (e.g. "error|warn", "version \\d+").
+ Regex uses RE2 syntax; no lookarounds/backreferences, and shorthand character classes are ASCII-only.
Helpful for literal strings, identifiers, error messages, or regex-style searches.
:param pattern: The text pattern to search for (regex auto-detected)
diff --git a/backend/open_webui/tools/knowledge_fs.py b/backend/open_webui/tools/knowledge_fs.py
index 53440b7bbf..355ae3cc58 100644
--- a/backend/open_webui/tools/knowledge_fs.py
+++ b/backend/open_webui/tools/knowledge_fs.py
@@ -17,7 +17,7 @@ from collections.abc import Callable
from contextlib import contextmanager
from typing import Optional
-import regex
+import re2
from fastapi import Request
from open_webui.env import (
@@ -31,12 +31,9 @@ log = logging.getLogger(__name__)
DEFAULT_HEAD_LINES = 10
DEFAULT_TAIL_LINES = 10
-# Matching time allowed per tool call. Backtracking cost is exponential in the length of the
-# matched text, so capping the pattern or the line does not bound it.
+# Total matching time allowed per tool call, checked between RE2's linear-time searches.
MATCH_BUDGET_SECONDS = 2.0
-MAX_REGEX_QUANTIFIER_COUNT = 2_000
-MAX_REGEX_QUANTIFIER_EXPANSION = 100_000
-_COUNTED_QUANTIFIER_RE = re.compile(r'(? str:
return pattern.replace(r'\|', '|').replace(r'\|', '|')
-def validate_regex_quantifiers(pattern: str) -> str | None:
- """Reject counted quantifiers that make regex compilation expand too much."""
- quantifier_expansion = 1
- for quantifier in _COUNTED_QUANTIFIER_RE.finditer(pattern):
- count_text = quantifier.group(1)
- count = int(count_text) if len(count_text) <= 6 else MAX_REGEX_QUANTIFIER_COUNT + 1
- if count > MAX_REGEX_QUANTIFIER_COUNT:
- return f'Regex quantifier counts over {MAX_REGEX_QUANTIFIER_COUNT:g} are not supported'
-
- # ponytail: conservative expansion catches nested quantifier bombs without mirroring regex syntax.
- quantifier_expansion *= max(count, 1)
- if quantifier_expansion > MAX_REGEX_QUANTIFIER_EXPANSION:
- return 'Regex quantifiers expand too much, lower the counts'
-
- return None
-
-
def build_matcher(pattern: str, case_insensitive: bool = False, use_regex: bool = False) -> tuple:
"""Build a matcher function. Returns (match_fn, error_str_or_None)."""
+ if len(pattern) > MAX_SEARCH_PATTERN_LENGTH:
+ return None, f'Search patterns over {MAX_SEARCH_PATTERN_LENGTH} characters are not supported'
+
if not use_regex and is_regex_pattern(pattern):
use_regex = True
if use_regex:
normalized = normalize_regex(pattern)
- quantifier_error = validate_regex_quantifiers(normalized)
- if quantifier_error:
- return None, quantifier_error
try:
- re_flags = regex.IGNORECASE if case_insensitive else 0
- compiled = regex.compile(normalized, re_flags)
- except regex.error as e:
- return None, f'Invalid regex: {e}'
+ options = re2.Options()
+ options.case_sensitive = not case_insensitive
+ options.max_mem = 1 << 20 # Bound compiled programs and the engine's matching cache to 1 MiB.
+ options.log_errors = False
+ compiled = re2.compile(normalized, options=options)
+ except re2.error as e:
+ return None, f'Invalid or unsupported regex (RE2 syntax): {e}'
budget = _active_budget.get() or MatchBudget()
def matches(line: str) -> bool:
started = time.monotonic()
try:
- # A negative timeout disables it, so an exhausted budget must not reach search().
if budget.remaining <= 0:
raise TimeoutError
- return bool(compiled.search(line, timeout=budget.remaining))
+ matched = bool(compiled.search(line))
+ if time.monotonic() - started >= budget.remaining:
+ raise TimeoutError
+ return matched
except TimeoutError:
raise MatchBudgetExceeded(f'Search exceeded {MATCH_BUDGET_SECONDS:g}s, narrow the pattern') from None
finally:
@@ -1188,6 +1173,7 @@ async def kb_exec(
Pipes: grep "auth" | head -5
Files: reference by path (docs/api/auth.md), filename, or file ID
+ Regex: RE2 syntax; no lookarounds/backreferences. Shorthand character classes are ASCII-only.
:param command: A filesystem command string
:return: Command output as text
diff --git a/backend/requirements-slim.txt b/backend/requirements-slim.txt
index 8171a6f9e6..e482bf330d 100644
--- a/backend/requirements-slim.txt
+++ b/backend/requirements-slim.txt
@@ -19,7 +19,8 @@ authlib==1.7.2
joserfc==1.7.4
requests==2.34.2
-regex==2026.5.9 # supports a per-search timeout, which `re` does not
+regex==2026.5.9
+google-re2==1.1.20251105 # bounded compilation and linear-time knowledge searches
aiohttp==3.13.5 # do not update to 3.13.3 - broken
aiodns==3.6.1 # keep pinned: 4.x pulls pycares 5 (c-ares 1.34.6) which breaks DNS on some hosts (#28013, #28215); opt-in via AIOHTTP_CLIENT_ASYNC_DNS_RESOLVER
aiocache==0.12.3
diff --git a/backend/requirements.txt b/backend/requirements.txt
index e929beafb6..11282b2807 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -14,7 +14,8 @@ authlib==1.7.2
joserfc==1.7.4
requests==2.34.2
-regex==2026.5.9 # supports a per-search timeout, which `re` does not
+regex==2026.5.9
+google-re2==1.1.20251105 # bounded compilation and linear-time knowledge searches
aiohttp==3.13.5 # do not update to 3.13.3 - broken
aiodns==3.6.1 # keep pinned: 4.x pulls pycares 5 (c-ares 1.34.6) which breaks DNS on some hosts (#28013, #28215); opt-in via AIOHTTP_CLIENT_ASYNC_DNS_RESOLVER
aiocache==0.12.3
diff --git a/pyproject.toml b/pyproject.toml
index 2fc362ae97..0f67b1b120 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -50,6 +50,7 @@ dependencies = [
"tiktoken==0.13.0",
"regex==2026.5.9",
+ "google-re2==1.1.20251105",
"mcp==1.27.2",
"openai==2.29.0",
diff --git a/src/lib/components/chat/Messages/Message.svelte b/src/lib/components/chat/Messages/Message.svelte
index 97c8cd69a4..30904ee27d 100644
--- a/src/lib/components/chat/Messages/Message.svelte
+++ b/src/lib/components/chat/Messages/Message.svelte
@@ -87,7 +87,6 @@
{compactPreview}
{editCodeBlock}
{topPadding}
- {onInsertToNote}
/>
{:else if (history.messages[history.messages[messageId].parentId]?.models?.length ?? 1) === 1}