Page MenuHomeVyOS Platform

repro_standalone.py

Authored By
cr0ntab
May 1 2026, 3:58 PM
Size
4 KB
Referenced Files
None
Subscribers
None

repro_standalone.py

#!/usr/bin/env python3
"""
Standalone reproducer for the vyos.vyos rm_templates regex slowdown.
No ansible or vyos.vyos dependency: just stdlib `re` and `time`.
The pattern below is copied verbatim from
vyos.vyos/plugins/module_utils/network/vyos/rm_templates/route_maps_14.py
parser name="route_map" (lines ~33-39). The script times it against
realistic non-matching input, alongside a `\\s*$`-fixed variant for
comparison.
Run: python3 repro_standalone.py
"""
import re
import time
# Pattern copied verbatim from rm_templates/route_maps_14.py lines 33-39.
BAD = re.compile(
r"""
^set\spolicy\sroute-map\s(?P<route_map>\S+)
*$""",
re.VERBOSE,
)
# Under re.VERBOSE the literal newline + spaces between `\S+` and `*$`
# are stripped at compile time, so the pattern that actually executes
# is `^set\spolicy\sroute-map\s(?P<route_map>\S+)*$`. The trailing `*`
# ends up quantifying the named group, which forces the regex engine
# into O(2^n) backtracking on inputs that share the prefix but don't
# match overall.
# Same positive matches, no group-quantifier.
FIXED = re.compile(
r"""^set\spolicy\sroute-map\s(?P<route_map>\S+)\s*$""",
re.VERBOSE,
)
# ---------------------------------------------------------------------------
# Inputs
# ---------------------------------------------------------------------------
# Positive case: these lines DO match the parser.
POSITIVE = [
"set policy route-map MY-MAP",
"set policy route-map ANOTHER",
]
# Negative case: lines that share the `set policy route-map <name>`
# prefix but don't match the route_map parser overall. These are
# what `show configuration commands | grep route-map` returns on any
# non-trivial device.
#
# The runtime cost is exponential in the length of the FIRST contiguous
# \S+ run after the prefix. 6-char names like "MY-MAP" (2^5 backtracks
# per line) finish in microseconds; 20-29 char names like the ones below
# (2^19 .. 2^28 per line) take seconds to minutes per line.
NEGATIVE = [
"set policy route-map ADVERTISE-ANYCAST-v6 rule 10 action 'permit'",
"set policy route-map ADVERTISE-ANYCAST-v6 rule 10 match ipv6 address prefix-list 'ANYCAST-AGGREGATE-v6'",
"set policy route-map DEFAULT-ORIGINATE-SENTINEL-v6 rule 10 action 'permit'",
"set policy route-map DEFAULT-ORIGINATE-SENTINEL-v6 rule 10 match ipv6 address prefix-list 'AS64496-SENTINEL-v6'",
"set policy route-map DEFAULT-ORIGINATE-SENTINEL-v6 rule 10 set local-preference '120'",
"set policy route-map IXP-PEER-IN rule 10 action 'permit'",
"set policy route-map IXP-PEER-IN rule 10 match ip address prefix-list 'IXP-INBOUND-v4'",
"set policy route-map IXP-PEER-IN rule 10 set community 'additive 65000:100'",
"set policy route-map TRANSIT-OUT rule 100 action 'permit'",
"set policy route-map TRANSIT-OUT rule 100 match ip address prefix-list 'CUSTOMER-AGGREGATES-v4'",
"set policy route-map TRANSIT-OUT rule 100 set as-path prepend '65000 65000'",
"set policy route-map UPSTREAM-IN-v4 rule 10 action 'permit'",
]
def time_match(pattern, lines, label):
t0 = time.perf_counter()
matches = 0
for ln in lines:
if pattern.match(ln):
matches += 1
dt = time.perf_counter() - t0
print(f" {label:<35s} matched={matches:<3d} elapsed={dt:8.4f}s ({len(lines)} lines)")
return dt
def main():
print("vyos.vyos rm_templates regex reproducer")
print()
print("Compiled pattern strings (after re.VERBOSE whitespace strip):")
print(f" BAD : {BAD.pattern.replace(chr(10), '').strip()!r}")
print(f" FIXED: {FIXED.pattern.replace(chr(10), '').strip()!r}")
print("\nPositive case (lines that match the route_map parser):")
time_match(BAD, POSITIVE, "bad pattern")
time_match(FIXED, POSITIVE, "fixed pattern")
print("\nNegative case (rule-lines that share the prefix but don't match):")
time_match(BAD, NEGATIVE, "bad pattern")
time_match(FIXED, NEGATIVE, "fixed pattern")
print("\nNegative case, 25 lines (closer to real device output):")
big = NEGATIVE * 2 + NEGATIVE[:1]
time_match(BAD, big, "bad pattern")
time_match(FIXED, big, "fixed pattern")
if __name__ == "__main__":
main()

File Metadata

Mime Type
text/x-script.python
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3637785
Default Alt Text
repro_standalone.py (4 KB)

Event Timeline