Skip to content

Flowbit ordering cyclic/v20 - #16176

Draft
inashivb wants to merge 12 commits into
OISF:mainfrom
inashivb:flowbit-ordering-cyclic/v20
Draft

inashivb wants to merge 12 commits into
OISF:mainfrom
inashivb:flowbit-ordering-cyclic/v20

Conversation

@inashivb

@inashivb inashivb commented Sep 4, 2026

Copy link
Copy Markdown
Member

Previous PR: #16172

Changes since v19:

  • added an exception for action order
  • scan-build issues fixed

Link to tickets:

SV_BRANCH=OISF/suricata-verify#3270

There are known issues still but this is the latest rev.

This solution makes use of the petgraph crate to create a directed
stable graph of signatures. The algorithm is as follows:

- A stable Directed Graph is created
- A Flowbit Analyzer array is created which stores the sids per flowbit
  per command
  - for each entry in the flowbit analyzer "READ" array of type isset
    - set array is walked => a directed edge* from the set sid to isset sid
      with the weight of SET command is added.
  - for each entry in the flowbit analyzer "READ" array of type isnotset
    - unset array is walked => a directed edge from the set sid to isnotset sid
      with the weight of SET command is added.
- If there was no error (cycle), this means that the current graph is a
  DAG (Directed Acyclic Graph)
- Perform a topological sort of the DAG

1. If a cycle is formed by varying edge weights, it's an invalid cycle.
   This would be formed by usage of different "WRITE" commands in
   different signatures e.g.
   sid: 1 => set, A; isset, B; # a directed edge of weight 0 is created
   from 1 to 2 for flowbit A.
   sid: 2 => isset, A; unset, B; # a directed edge of weight 2 is
   created from 2 to 1 for flowbit B.
2. If the cycle is formed by the same edge weights, it is considered
   unsatisfiable at runtime.

Bug 7771
Bug 7638
Bug 1399
before adding them to the Detection Context's signature list. The
de_ctx->sig_list serves as a sorted signature list that is later passed
on to the grouping fns. If no property of higher value changes the order
of the signatures, the order coming from de_ctx->sig_list is final.

Add the appropriate calls to resolve flowbit dependencies before adding
them to the sig_list. This is especially important for flowbits with
complex ordering involved.

Bug 7771
Bug 7638
Bug 1399
The final list of signatures must be sorted with all the sorting fns
available. However, flowbits SET_READ have their order derived from
special handling between the dependent flowbits. Using the usual sorting
criteria will mess up the order that was created.

Bug 7771
Bug 7638
Bug 1399
Add an output file called "flowbits_dependency_graph.json" that outputs
the graph in the following format:
12 : { # sid of the node
    in: [{ # all incoming edges
        id: "1234" # internal ID of the node,
        weight: 2,
        sid: 13
    }],
    out: [{ # all outgoing edges
        id: "124" # internal ID of the node,
        weight: 2,
        sid: 10
    }]
}

in two conditions:
1. Right before any unrecoverable error.
2. At the end of the successful graph creation.
In order to deal with the complex flowbits ordering, Suricata internally
makes a graph of the signature dependencies created by the flowbits. In
this graph, some dependencies may lead to cycles that are not always
valid. Suricata tries to resolve these cycles and come up with a final
order of signatures that makes sense.

Add a configurable limit detect.flowbits.max-cycle-resolution that can
be used to asses when to give up.

Bug 7638
Merge sort by default is a stable sorting algorithm which means that the
relative order of all elements must be preserved in the final list.
However, the implementation in SCSigOrder, instead of breaking each of
the arrays into halves and going about the usual divide and conquer,
ends up doing two things that make it unstable:
1. each array has a reversed order
2. the two halves at any point are created as by picking one item from
   the main array and putting it in the first half followed by picking
   second item from the main array and putting it in the second half.

Fix this by first finding the midpoint of the main array in the current
recursive call and breaking the array there. The midpoint is found using
the Tortoise and Hare algorithm. The heads of each of the divided arrays
point to the beginning.

This guarantees the relative order among the elements to be stable.
This becomes a bug when the final order is not to be messed with as done
in the solution for

Bug 7638
Bug 7771
Bug 1399
@suricata-review-bot

Copy link
Copy Markdown

AI-generated review posted automatically by Suricata ai-review.

Verdict: request changes

  • Medium — src/detect-engine-sigorder.c:546: The SET_READ shortcut bypasses the flowint, flowvar, pktvar, and xbits comparators for same-action rules, even when they have no flowbit dependency. An independent flowint reader can therefore run before its setter and miss an alert that existing ordering guaranteed.

  • Medium — src/detect-engine-sigorder.c:733: Every alternative in flowbits:isset,A|B becomes a mandatory dependency edge, although runtime matching requires only one alternative. set B → isset A|B; set C → isset C; set A has a valid execution order, but the artificial A/C cycle rejects it. suricata-verify PR #3270 has no OR-flowbit coverage.

  • Medium — src/detect-engine-sigorder.c:1134: Dependency-resolution failure is not returned as an error. Default startup continues after FatalErrorOnInit, and reload logs that the existing ruleset is retained but proceeds to activate the unresolved new context. This leaves Redmine #7771’s invalid cycles accepted in normal operation. The linked cycle tests always use --init-errors-fatal, masking both paths.

  • Commit message typo — 659e7f9: “a directed edge* from” → “a directed edge from”.

  • Commit message typo — 84995ea: “used to asses when” → “used to assess when”.

@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.47100% with 54 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.12%. Comparing base (b8084b3) to head (688141f).

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #16176      +/-   ##
==========================================
- Coverage   83.14%   83.12%   -0.02%     
==========================================
  Files        1004     1005       +1     
  Lines      277579   277989     +410     
==========================================
+ Hits       230789   231084     +295     
- Misses      46790    46905     +115     
Flag Coverage Δ
fuzzcorpus 61.74% <57.30%> (+0.09%) ⬆️
livemode 18.49% <10.20%> (+0.08%) ⬆️
netns 22.77% <10.20%> (-0.11%) ⬇️
pcap 45.46% <64.50%> (+0.01%) ⬆️
suricata-verify 67.35% <87.47%> (+0.02%) ⬆️
unittests 58.53% <57.54%> (-0.01%) ⬇️

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.

@inashivb

inashivb commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

AI-generated review posted automatically by Suricata ai-review.

Verdict: request changes

* Medium — `src/detect-engine-sigorder.c:546`: The SET_READ shortcut bypasses the flowint, flowvar, pktvar, and xbits comparators for same-action rules, even when they have no flowbit dependency. An independent flowint reader can therefore run before its setter and miss an alert that existing ordering guaranteed.

True. The only thing taking precedence over flowbits order is action order. I also plan to add "priority" in that precedence list.

* Medium — `src/detect-engine-sigorder.c:733`: Every alternative in `flowbits:isset,A|B` becomes a mandatory dependency edge, although runtime matching requires only one alternative. `set B` → `isset A|B; set C` → `isset C; set A` has a valid execution order, but the artificial A/C cycle rejects it. [suricata-verify PR #3270](https://github.com/OISF/suricata-verify/pull/3270) has no OR-flowbit coverage.

Yes and there's no way to know which of the OR'd flowbit should or should not be counted as a mandatory dependency edge. I am going to add a note about this. Up for discussion.

* Medium — `src/detect-engine-sigorder.c:1134`: Dependency-resolution failure is not returned as an error. Default startup continues after `FatalErrorOnInit`, and reload logs that the existing ruleset is retained but proceeds to activate the unresolved new context. This leaves [Redmine #7771](https://redmine.openinfosecfoundation.org/issues/7771)’s invalid cycles accepted in normal operation. The linked cycle tests always use `--init-errors-fatal`, masking both paths.

yeah. Can't fatal error on rule reload. In case someone adds a new rule that causes an unresolvable dependency, the only route is generate an error about it and load it anyway and hope the user will take care of it.

* Commit message typo — `659e7f9`: “a directed edge* from” → “a directed edge from”.

* Commit message typo — `84995ea`: “used to asses when” → “used to assess when”.

@suricata-qa

Copy link
Copy Markdown

WARNING:

field baseline test %
SURI_TLPR1_stats_chk
.uptime 642 663 103.27%

Pipeline = 33747

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.

3 participants