Skip to content

detect/bsize: apply as content depth ##15825

Open
jlucovsky wants to merge 4 commits into
OISF:mainfrom
jlucovsky:4226/4
Open

detect/bsize: apply as content depth ##15825
jlucovsky wants to merge 4 commits into
OISF:mainfrom
jlucovsky:4226/4

Conversation

@jlucovsky

Copy link
Copy Markdown
Contributor

Continuation of #15806

Optimize content checks by using bsize as an upper bound, similar to what
dsize and urilen do. This improves performance on large buffers; small buffers see less, if any, boost.

Link to ticket: https://redmine.openinfosecfoundation.org/issues/4226

Describe changes:

  • Use bsize as an upper bound and apply it as a depth so content inspection is bounded to the buffer instead of scanning the whole buffer.
  • When an exact bsize equals a lone content's length, mark the content
    startswith/endswith so the mpm is anchored and the engine prefilters on length.
  • When a buffer carries more than one bsize, apply the tightest (smallest) upper bound, and reject at load a buffer whose bsize keywords can never be satisfied together (two differing exact values, or a lower bound above an upper bound). A legitimate lower+upper pair forming a range stays valid.
  • Have engine-analysis suggest bsize where a content is pinned to the whole buffer the long way (startswith/endswith), and suggest folding a genuine lower+upper bsize pair into a single bsize:lo<>hi range.
  • Extend documentation: the bsize keyword reference and the engine analysis notes/warnings section.

Changes since #15806

  • Rebase s-v and suri PRs

Changes since #15674:

  • Dropped the earlier --strict-rule-keywords idea for multiple bsize.
    Instead of rejecting multiple bsize, a real lower+upper range is accepted
    (e.g. bsize:<370; bsize:>359) and only infeasible combinations are rejected at load.
  • Made the "fold into a range" analyzer note shape-aware: it fires only for a
    genuine lower+upper pair, not for same-direction or exact bounds.
  • Added suricata-verify tests bug-4226-08 (infeasible bounds rejected) and
    bug-4226-09 (shape-aware suggestion).

SV_BRANCH=OISF/suricata-verify#3155

Benchmark: bsize applied as content depth

Measured with a standalone profiling harness (paired rules: each scenario has a bsize-form rule and the hand-written equivalent), run over three captures. Reproduction kit (script + synthetic pcap) attached separately.

smallFlows.pcap.gz
smallFlows.pcap.gz
bsize-bench.pcap.gz
bsize-bench.pcap.gz
bsize-bench.rules.gz
bsize-bench.rules.gz
Definitions

  • base — Suricata master (no bsize-as-depth optimization).
  • opt — this branch (bsize upper bound applied as content depth; for an
    exact bsize equal to a lone content's length, the content is also marked
    startswith+endswith).
  • anchored — the hand-written equivalent that pins the content to the buffer
    with startswith; endswith.
  • plain — the same content with no length constraint at all (a floating match).
  • cost ratio — the profiled cost of the bsize-form rule ÷ the cost of its
    hand-written equivalent, computed within the same run. ~1.0 means parity;
    >1.0 means it is still paying extra. The within-run ratio cancels run-to-run
    and cross-binary tick noise.
  • check counts — how often each bsize rule is evaluated. A drop from base
    to opt means the depth bound made the MPM prefilter more selective.

1. Synthetic capture (uniform bodies — isolates the anchoring path)

scenario (cost ratio, ~1.0 == parity) base opt
A file.data (large) bsize/depth 1.44 1.51
B user_agent (small) bsize/depth 1.40 1.45
C http.method (exact) bsize/anchored 2.00 2.45
D http.uri (exact) bsize/anchored 2.42 1.35
D http.uri (exact) bsize/plain 1.77 0.90
bsize-rule check counts (rule evaluations) base opt
A file.data (large) bsize 100 100
B user_agent (small) bsize 200 200
C http.method (exact) bsize 100 100
D http.uri (exact) bsize 300 300

2. smallFlows.pcap (real mixed traffic, ~9 MB)

scenario (cost ratio, ~1.0 == parity) base opt
A file.data (large) bsize/depth 2.27 0.93
B user_agent (small) bsize/depth 1.54 1.55
C http.method (exact) bsize/anchored 1.78 2.00
D http.uri (exact) bsize/anchored 2.04 1.15
D http.uri (exact) bsize/plain 1.56 0.81
bsize-rule check counts (rule evaluations) base opt
A file.data (large) bsize 699 396
B user_agent (small) bsize 525 525
C http.method (exact) bsize 533 533
D http.uri (exact) bsize 544 544

3. bigFlows.pcap (real mixed traffic, ~368 MB)

scenario (cost ratio, ~1.0 == parity) base opt
A file.data (large) bsize/depth 2.56 1.17
B user_agent (small) bsize/depth 1.62 1.66
C http.method (exact) bsize/anchored 1.90 2.05
D http.uri (exact) bsize/anchored 2.15 1.16
D http.uri (exact) bsize/plain 1.54 0.79
bsize-rule check counts (rule evaluations) base opt
A file.data (large) bsize 7006 3759
B user_agent (small) bsize 6403 6403
C http.method (exact) bsize 6414 6414
D http.uri (exact) bsize 6603 6603

Reading the results

The anchoring win (D) reproduces on every capture: the bsize-form rule
drops from ~2x the cost of the manually-anchored equivalent to ~1.15x, and to
cheaper than an unconstrained content (0.79–0.90x of plain).

The file.data depth win (A) is prefilter-driven and only shows on
realistic traffic. On the synthetic capture the bodies are uniform, so the
fast-pattern can't become more selective and A is flat (100/100). On real
traffic the depth bound roughly halves how often the rule is evaluated —
smallFlows 699→396 (ratio 2.27→0.93), bigFlows 7006→3759 (ratio 2.56→1.17).

B (small user_agent) and C (fixed-length http.method) are flat by
design: the buffer is tiny or always equals the pinned length, so neither the
depth bound nor the endswith short-circuit has room to help.

jlucovsky added 4 commits July 8, 2026 08:58
The bsize keyword only checked the buffer length at inspection time; the
content patterns in the same buffer were still searched across the whole
buffer before that length check ran. That made bsize noticeably more
expensive than the equivalent depth/isdataat form.

When bsize gives a usable upper bound (bsize:N, bsize:<N, or a range),
apply it as a depth to every content in that buffer: a content can't
match past the end of the buffer, and the buffer is at most bsize bytes,
so the mpm and content inspection can bound their search instead of
scanning the entire buffer. This mirrors the existing dsize and urilen
optimizations. When a buffer carries more than one bsize, the tightest
(smallest) upper bound is used, since the buffer must satisfy them all.

When an exact bsize equals the length of a single content in the buffer,
that content must span the whole buffer, so it is marked
startswith/endswith -- setting the mpm anchoring and the endswith
inspection short-circuit -- rather than only being bounded
by depth.

The depth is applied after DetectContentPropagateLimits, like dsize,
because the DETECT_CONTENT_DEPTH flag feeds propagation and setting it
earlier would change how limits chain across contents.

The derived depth is recorded with a DETECT_CONTENT_BSIZE2DEPTH flag so
engine-analysis can report that the content depth came from bsize.

Issue: 4226
Both the content-depth optimization and the content-length validation
need the usable bsize upper bound for a buffer. Extract that into a
shared helper returning the tightest (smallest) bound across all of the
buffer's bsize keywords, and use it in both places.

Previously the validation callback inspected only the first bsize, so a
signature whose required content exceeds a later, tighter bsize -- e.g. a
10-byte content with bsize:<20; bsize:<8 -- passed validation and became a
silently never-matching rule. Validating against the tightest bound
rejects it at load with a clear error, and both code paths now agree on
which bound applies.

A buffer's bsize keywords must all hold at once, so the same validation
also rejects a buffer whose bounds share no satisfiable length -- two
different exact bsizes, or a lower bound above an upper bound -- rather
than letting it load as a silently never-matching rule. A lower and an
upper bound that overlap (a range) stay valid. The bsize keyword
reference documents this multiple-bsize behavior.

Issue: 4226
Two engine-analysis hints for the bsize keyword:

- When a sticky buffer holds a single content marked both startswith and
  endswith, the content must span the whole buffer, so its length is fixed.
  (isdataat:!1,relative folds into the same endswith flag.) bsize:<len>
  expresses that in one keyword and lets the engine prefilter on length, so
  the analyzer suggests it.

- When a buffer combines a lower and an upper bsize bound (e.g. bsize:>N;
  bsize:<M), the analyzer suggests folding them into a single bsize range
  (bsize:lo<>hi), which expresses the two-sided bound more clearly. The
  suggestion is limited to genuine bound-pairs; same-direction or exact
  bsizes don't trigger it.

These are rule-author-facing notes; neither rejects a rule.

Issue: 4226
The JSON rule analysis report carries optional "notes" and "warnings"
arrays, but they weren't described anywhere even though the surrounding
report structure is. Add a short section explaining both fields and list
a few representative notes, including the bsize-related ones, so signature
writers know where to look for the optimizations the engine applied and
the hints it offers.

Issue: 4226
@suricata-qa

Copy link
Copy Markdown

ERROR:

ERROR: QA failed on ASAN_TLPR1_cfg.

Pipeline = 32494

@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.70115% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.95%. Comparing base (1731805) to head (18a10a1).

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #15825      +/-   ##
==========================================
+ Coverage   82.93%   82.95%   +0.02%     
==========================================
  Files        1003     1003              
  Lines      275204   275353     +149     
==========================================
+ Hits       228239   228429     +190     
+ Misses      46965    46924      -41     
Flag Coverage Δ
fuzzcorpus 61.52% <94.82%> (+0.02%) ⬆️
livemode 18.33% <13.79%> (-0.01%) ⬇️
netns 22.69% <35.05%> (-0.01%) ⬇️
pcap 45.34% <52.87%> (+0.02%) ⬆️
suricata-verify 66.91% <95.40%> (-0.01%) ⬇️
unittests 58.44% <45.97%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants