6.4 Evaluation
Evaluation is pnp_core::eval::evaluate(forest, snapshot, ctx): pure,
allocation-fallible, and the same code under cargo and in the kernel.
Its steps are the ratified design's six, in order.
6.4.1 1. Matching #
A rule matches iff it is enabled and every one of its conditions holds against the snapshot. Conditions are a conjunction; a value written as a list is a disjunction within that one field. A disabled rule never matches, so its subtree is unreachable.
Conditions are evaluated in the order ingestion stored them, which puts
live-time conditions (Time.*) last, and the conjunction stops at
the first false one. Every live-time condition actually reached — true
or false — is consulted, and Rule::matches_traced() records into a
MatchTrace the moment it would next flip (Condition::next_flip()):
for hour, minute, second and day-of-week, the value sequence is scanned
one cycle ahead with the condition's own operator, so Time.Hour.Equal 9-17 at 10:30 flips at 18:00 and Time.Hour.LessThan 24 never does;
day-of-month, month and year answer "next midnight", since exact flips
need calendar arithmetic nobody has asked for. A false condition is
consulted too: a higher-priority rule that missed only on the clock may
match later. A rule whose address or port conditions fail first never
consults its clock and contributes nothing. The earliest consulted flip
is the evaluation's expires_at — the Flow layer's sentence expiry
(§6.8); the per-packet layers compute it and ignore it.
6.4.2 2. The trigger set #
The walk descends each tree from its root. A node triggers iff it matches and none of its children match: a matching child shadows its parent. Shadowing exists only within a lineage — two trees never shadow each other, and their order in the forest carries no meaning. The walk returns "matched" to the parent so the parent knows it was shadowed.
6.4.3 3. Resolving a triggered rule #
Every triggered rule's action list is resolved once. Verdict actions fold
to the strictest listed; TAG and COUNT push effects; REPORT folds
to the highest level listed and pushes one effect if that level clears
CurrentReportingLevel (one report per rule per evaluation); PROMPT
pushes a prompt-issued effect and, with no handler transport in this
release, resolves its fallback in place (nesting bounded by
MAX_PROMPT_CHAIN, 4). Side effects always execute — they are pushed
before anyone knows whether the rule's verdict will win.
6.4.4 4. Abstention #
A triggered rule that yields no verdict abstains. The walk then goes up
the rule's own parentage — the chain it descended by — to the nearest
ancestor whose actions contain a direct verdict (a top-level PASS,
DROP or REJECT; a verdict inside a PROMPT fallback does not count),
and that ancestor speaks for the region: its full action list
resolves, once. A speaker reached by several abstaining descendants in
one evaluation executes only the first time (spoken tracks resolved
speakers by identity). Intermediate abstaining ancestors execute
nothing. If no ancestor bears a verdict, the branch contributes nothing
and other trees or the backstop answer.
The attribution of a speaker's verdict is the speaker's path, not the abstaining descendant's.
6.4.5 5. Collation #
Every yielded verdict is a candidate (verdict, priority, path). The
winner has the highest priority; at equal priority the strictest
verdict: DROP (3) > REJECT(Refused) (2) > REJECT(Prohibited) (1) >
PASS (0). The two reject ranks are a deterministic tie-break — the
quieter story wins — that exists only because candidate order is
meaningless. Priority is the rule's effective priority, resolved at
ingestion: a rule's own Priority value, else its parent's, else 0.
6.4.6 6. The backstop #
No candidate at all: the verdict is DROP, attributed to backstop,
and the evaluation is flagged so the event can say so. The backstop is
compiled in and un-deletable; every permissive statement in a policy is
therefore a visible rule.
6.4.7 The evaluation as data #
Evaluation carries the winning verdict, its attribution, the backstop
flag, every effect to apply, every candidate (winners and losers alike)
for observability, and expires_at. Effects reference their stores by
name and hash: Effect::Tag { hash, op }, Effect::Count { hash, amount } with the amount already resolved (Length becomes the packet
length, or 0 when the packet has none), Effect::Report { rule, level },
Effect::PromptIssued { rule, handler }.
6.4.8 The bridge #
pnp_rust_evaluate(forest, snap, layer, reporting_level, out) is what
policy.c calls under rcu_read_lock(). It:
- lifts the C snapshot (§6.3) and resolves the forest's machinery facts
through the stores —
peios_pnp_tag_lookup()for every tag name the forest mentions (PacketandFlowlayers;RawPacketreads none),peios_pnp_counter_read()for every view — and gives aFlowforest its own facts,RelatedandStart.*; - evaluates with
EvalContext { reporting_level }; - writes the verdict, the reject kind, the backstop flag, the truncated
attribution and
expires_at(0 = never) intostruct peios_pnp_outcome; - then applies the effects —
peios_pnp_tag_apply(),peios_pnp_counter_add(),peios_pnp_report_emit()with the verdict it just computed — counting each species into the outcome.
Ordering 4 after 2 is what the snapshot-immutability law requires: a
COUNT is applied after this packet's own view reads, so a rule cannot
trip its own threshold, and a REPORT can name the verdict. Every
allocation in 1–2 is GFP_ATOMIC (pnp-core's pkm_alloc in kernel
mode); a failure returns -ENOMEM and the hook fails closed (§6.2). The
store calls never sleep.