java-topology/defects/puppet/unit/test_puppet_cwe1333.rb
russell@unturf.com fd8ae3ba8b test: add CWE-1333/407/362 benchmarks for bleach, salt, ansible, capistrano, puppet, katago, pachi
Every patch now ships with a runnable benchmark verifying complexity claims:
- bleach/unit/test_bleach_cwe1333.py: length guard truncates 1001-char adversarial
  input to 1000 chars (removes '@' tail), gauntlet matches fast (<0.5s)
- salt/unit/test_salt_cwe1333.py: ThreadPoolExecutor timeout wrapper tested at N=20
  adversarial, GIL behavior documented
- ansible/unit/test_ansible_cwe1333.py: same timeout wrapper model for ~-prefix
  inventory patterns
- capistrano/unit/test_capistrano_cwe1333.rb: Regexp.timeout= / Timeout fallback
  guard for host/role filter patterns
- puppet/unit/test_puppet_cwe1333.rb: RegexGuard.safe_compile timeout for all
  three Puppet regex call sites (match(), =~, PRegexpType)
- katago/unit/test_katago_cwe407.cpp: bool seen[] bitset vs O(N*k) linear scan;
  23x speedup at chain=80, scaling ratio 2.5x at 3x chain size (limit 4x)
- pachi/unit/test_pachi_cwe362.c: 8-thread hammer, 100k iterations, zero
  double-expansion events with __atomic_store_n fix
2026-04-13 12:46:35 -04:00

204 lines
6.2 KiB
Ruby

#!/usr/bin/env ruby
# frozen_string_literal: true
#
# CWE-1333 benchmark for Puppet defects puppet-0001 and puppet-0002.
#
# UNDF: UNDF-2026-000001269 (puppet-0001, timeout wrapper)
# UNDF-2026-000001273 (puppet-0002, RE2 correct fix)
# Patches:
# puppet-0001.patch
# puppet-0002-regex-redos-re2.patch
#
# Defect: Three Puppet code paths compile user-supplied strings with bare Regexp.new():
# 1. lib/puppet/functions/match.rb:88 -- match() built-in
# 2. lib/puppet/pops/evaluator/evaluator_impl.rb:616 -- =~ operator
# 3. lib/puppet/pops/types/types.rb:1697 -- PRegexpType constructor
# A catalog containing "(a+)+$" as a pattern hangs the agent indefinitely.
#
# Fix puppet-0001: RegexGuard.safe_compile() with Regexp.timeout= / Timeout fallback.
# Fix puppet-0002: RE2::Regexp.new() -- Thompson NFA, O(N), no backtracking.
#
# Complexity gate:
# pattern "(a+)+$" against "a"*25+"b" must complete in <2s with puppet-0001
# RE2 version must complete in <50ms
require "timeout"
ADVERSARIAL_PATTERN = "(a+)+$"
ADVERSARIAL_INPUT_25 = "a" * 25 + "b"
ADVERSARIAL_INPUT_20 = "a" * 20 + "b"
BENIGN_PATTERN = "^foo-\\d+$"
BENIGN_STRING = "foo-42"
NONMATCH_STRING = "bar-42"
COMPILE_TIMEOUT = 1.0 # seconds
# ---------------------------------------------------------------------------
# Before: bare Regexp.new without timeout
# ---------------------------------------------------------------------------
def safe_compile_before(pattern)
Regexp.new(pattern)
end
def do_match_before(string, pattern_str)
re = safe_compile_before(pattern_str)
re.match(string)
end
# ---------------------------------------------------------------------------
# After (puppet-0001): RegexGuard.safe_compile equivalent
# ---------------------------------------------------------------------------
def safe_compile_with_timeout(pattern, options = 0)
return pattern if pattern.is_a?(Regexp)
if Regexp.respond_to?(:timeout=)
old = Regexp.timeout
Regexp.timeout = COMPILE_TIMEOUT
begin
Regexp.new(pattern, options)
rescue Regexp::TimeoutError
raise "Regular expression #{pattern.inspect} timed out (CWE-1333)"
ensure
Regexp.timeout = old
end
else
Timeout.timeout(COMPILE_TIMEOUT) { Regexp.new(pattern, options) }
end
rescue RegexpError => e
raise "Invalid regular expression #{pattern.inspect}: #{e}"
rescue Timeout::Error
raise "Regular expression #{pattern.inspect} timed out (CWE-1333)"
end
def do_match_safe(string, pattern_str)
re = safe_compile_with_timeout(pattern_str)
re.match(string)
end
# ---------------------------------------------------------------------------
# After (puppet-0002): RE2 correct fix
# ---------------------------------------------------------------------------
def do_match_re2(string, pattern_str)
begin
require "re2"
re = RE2::Regexp.new(pattern_str)
re.match(string)
rescue LoadError
raise "re2 gem not installed -- skipping RE2 test"
end
end
# ---------------------------------------------------------------------------
# Test runner
# ---------------------------------------------------------------------------
def assert_truthy(val, msg)
if val
puts "PASS #{msg}"
else
puts "FAIL #{msg}: expected truthy, got #{val.inspect}"
exit 1
end
end
def assert_falsy(val, msg)
if !val
puts "PASS #{msg}"
else
puts "FAIL #{msg}: expected falsy, got #{val.inspect}"
exit 1
end
end
def assert_raises(msg, &block)
begin
block.call
puts "FAIL #{msg}: expected exception but none raised"
exit 1
rescue => e
puts "PASS #{msg}: raised #{e.class}"
end
end
def assert_lt(value, limit, msg)
if value < limit
puts "PASS #{msg} (#{(value * 1000).round(1)}ms < #{(limit * 1000).round}ms)"
else
puts "FAIL #{msg}: #{(value * 1000).round(1)}ms >= #{(limit * 1000).round}ms"
exit 1
end
end
# Test 1: benign pattern correctness -- before and after agree
before_match = do_match_before(BENIGN_STRING, BENIGN_PATTERN)
after_match = do_match_safe(BENIGN_STRING, BENIGN_PATTERN)
assert_truthy before_match, "puppet-0001 benign match: before"
assert_truthy after_match, "puppet-0001 benign match: after"
before_nomatch = do_match_before(NONMATCH_STRING, BENIGN_PATTERN)
after_nomatch = do_match_safe(NONMATCH_STRING, BENIGN_PATTERN)
assert_falsy before_nomatch, "puppet-0001 benign non-match: before"
assert_falsy after_nomatch, "puppet-0001 benign non-match: after"
# Test 2: invalid pattern raises (not silently ignored)
assert_raises("puppet-0001 invalid pattern raises") do
safe_compile_with_timeout("[invalid")
end
# Test 3: adversarial pattern raises timeout error in <2s
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
begin
do_match_safe(ADVERSARIAL_INPUT_25, ADVERSARIAL_PATTERN)
puts "FAIL puppet-0001 adversarial N=25: expected timeout/error"
exit 1
rescue => e
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
assert_lt elapsed, 2.0, "puppet-0001 complexity gate: adversarial N=25 timeout"
puts "PASS puppet-0001 adversarial N=25: #{e.message[0..60]}"
end
# Test 4: N=20 adversarial also completes fast
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
begin
do_match_safe(ADVERSARIAL_INPUT_20, ADVERSARIAL_PATTERN)
rescue => _e
end
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
assert_lt elapsed, 2.0, "puppet-0001 N=20 adversarial"
# Test 5: RE2 correct fix (if available)
begin
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
result_re2 = do_match_re2(ADVERSARIAL_INPUT_25, ADVERSARIAL_PATTERN)
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
assert_falsy result_re2, "puppet-0002 RE2 adversarial N=25: no match"
assert_lt elapsed, 0.05, "puppet-0002 complexity gate: RE2 N=25"
# Verify RE2 still matches benign patterns
result_benign = do_match_re2(BENIGN_STRING, BENIGN_PATTERN)
assert_truthy result_benign, "puppet-0002 RE2 benign match"
rescue RuntimeError => e
puts "SKIP puppet-0002 RE2: #{e}"
end
# Test 6: PRegexpType-style escaped pattern (escape=true path)
escaped = Regexp.escape("foo.bar") # safe path, no backtracking risk
re = safe_compile_with_timeout(escaped)
assert_truthy re.match?("foo.bar"), "puppet-0001 escaped pattern matches literal"
assert_falsy re.match?("fooXbar"), "puppet-0001 escaped pattern rejects non-literal"
puts "ALL PASS"