Skip to content

(11) feat(acl): crate scaffolding + software reference backend#1575

Merged
daniel-noland merged 1 commit into
mainfrom
pr/daniel-noland/acl-reference
Jun 18, 2026
Merged

(11) feat(acl): crate scaffolding + software reference backend#1575
daniel-noland merged 1 commit into
mainfrom
pr/daniel-noland/acl-reference

Conversation

@daniel-noland

@daniel-noland daniel-noland commented May 31, 2026

Copy link
Copy Markdown
Collaborator

Stack (11). Base: pr/daniel-noland/match-action.

The ACL crate scaffolding and the software reference backend -- the readable
oracle that defines ACL semantics and the public API surface.

  • feat(acl): crate scaffolding + software reference backend.

The DPDK backend (proven equivalent to this oracle via a differential test)
lands in the next PR.

Review stack (merge bottom -> top):

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new dataplane-acl workspace crate that defines the ACL public API surface and implements a pure-software “reference/oracle” backend (typed and dynamic) behind the lookup::Lookup interface.

Changes:

  • Add acl as a workspace member and workspace dependency, plus package metadata for miri/wasm selection.
  • Introduce dataplane-acl crate scaffolding with strict linting and a reference module.
  • Implement ReferenceTable (typed MatchKey) and DynReferenceTable (runtime FieldSpec) linear-scan classifiers with unit tests.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
Cargo.toml Adds acl to workspace + workspace deps; introduces miri/wasm metadata entry.
Cargo.lock Locks the new dataplane-acl crate and its dependencies.
acl/Cargo.toml Defines the new dataplane-acl package and dependencies.
acl/src/lib.rs Crate-level lint policy and top-level documentation; exports reference module.
acl/src/reference/mod.rs Wires up reference backend modules + re-exports.
acl/src/reference/table.rs Implements typed ReferenceTable/RefRule and Lookup<K, A> integration.
acl/src/reference/dyn_table.rs Implements runtime-shaped DynReferenceTable with validation and byte-slice lookup.

Comment on lines +29 to +35
pub(crate) fn matches_packed(&self, specs: &[FieldSpec], buf: &[u8]) -> bool {
debug_assert_eq!(self.fields.len(), specs.len());
self.fields
.iter()
.zip(specs)
.all(|(pred, spec)| pred.matches(&buf[spec.offset..spec.offset + spec.size]))
}
Comment on lines +66 to +73
fn pack(key: &K) -> Option<[u8; MAX_KEY_BYTES]> {
if K::KEY_SIZE > MAX_KEY_BYTES {
return None;
}
let mut buf = [0u8; MAX_KEY_BYTES];
key.as_key_into(&mut buf[..K::KEY_SIZE]);
Some(buf)
}
Comment on lines +88 to +103
#[must_use]
pub fn lookup_bytes(&self, key: &[u8]) -> Option<&A> {
assert_eq!(key.len(), self.key_size, "key length must equal key_size");
self.rules
.iter()
.find(|rule| rule.matches_packed(&self.specs, key))
.map(RefRule::action)
}
#[must_use]
pub fn matches_bytes(&self, key: &[u8]) -> Vec<&RefRule<A>> {
assert_eq!(key.len(), self.key_size, "key length must equal key_size");
self.rules
.iter()
.filter(|rule| rule.matches_packed(&self.specs, key))
.collect()
}
Comment thread Cargo.toml
Comment on lines +265 to +272
[workspace.metadata.package.acl]
package = "dataplane-acl"
# Default features enable the DPDK `rte_acl` backend, which pulls in
# `dpdk-sys` (bindgen against the system DPDK headers). miri can't
# build that path on the cross target, and the reference backend's
# unit tests run fine outside the miri profile.
miri = false # hopeless + pointless
wasm = false # hopeless + pointless
Comment thread acl/src/lib.rs
Comment on lines +17 to +18
//! - [`reference`](mod@reference): linear-scan software classifier;
//! differential oracle and a mutable cascade front. Always built.
@daniel-noland daniel-noland force-pushed the pr/daniel-noland/match-action branch from c071ee9 to 7d5e7ad Compare June 3, 2026 19:50
@daniel-noland daniel-noland force-pushed the pr/daniel-noland/acl-reference branch from 3220e66 to 710b08b Compare June 3, 2026 19:50
Base automatically changed from pr/daniel-noland/match-action to pr/daniel-noland/dpdk-test-eal June 5, 2026 06:21
@daniel-noland daniel-noland force-pushed the pr/daniel-noland/dpdk-test-eal branch 4 times, most recently from df2ad93 to 3741795 Compare June 5, 2026 06:45
Base automatically changed from pr/daniel-noland/dpdk-test-eal to main June 5, 2026 07:44

@mvachhar mvachhar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like I have reviewed a bunch of this before, perhaps a prior branch got merged onto this one? In any case, the stuff I don't remember looks good to go. I'm a bit torn on the dataplane-acl name since there is a user facing feature for that, which may ore may not end up in this crate (probably not). But we can always rename it if we want.

@daniel-noland daniel-noland force-pushed the pr/daniel-noland/acl-reference branch from 710b08b to 598ba60 Compare June 15, 2026 20:18
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 737407fa-4c6e-40e2-8d06-5de7d692ba28

📥 Commits

Reviewing files that changed from the base of the PR and between 08b4c60 and 7a7d109.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • Cargo.toml
  • acl/Cargo.toml
  • acl/src/lib.rs
  • acl/src/reference/dyn_table.rs
  • acl/src/reference/mod.rs
  • acl/src/reference/table.rs
🚧 Files skipped from review as they are similar to previous changes (6)
  • acl/src/reference/mod.rs
  • acl/Cargo.toml
  • Cargo.toml
  • acl/src/lib.rs
  • acl/src/reference/table.rs
  • acl/src/reference/dyn_table.rs

📝 Walkthrough

Walkthrough

A new dataplane-acl Rust crate is added to the workspace. It implements two ACL match-action classifier backends under acl::reference: a typed ReferenceTable<K, A> that packs a MatchKey into bytes for rule matching, and a byte-oriented DynReferenceTable<A> that validates FieldSpec layout and predicate widths at construction time.

Changes

dataplane-acl crate introduction

Layer / File(s) Summary
Workspace registration and crate wiring
Cargo.toml, acl/Cargo.toml, acl/src/lib.rs, acl/src/reference/mod.rs
Adds acl to workspace members, dependencies, and build-target metadata; creates the crate manifest with workspace-backed dependencies; sets crate-level lint policy and exports pub mod reference; wires dyn_table and table submodules with centralized re-exports.
Typed reference table
acl/src/reference/table.rs
Introduces RefRule<A> (field predicates plus action with packed-byte matching) and ReferenceTable<K, A> (constructs from rules, packs MatchKey into bytes, returns all matches or first-match action via Lookup trait). Unit tests cover hit/miss, empty table, first-match precedence, and non-lossy matches.
Dynamic reference table
acl/src/reference/dyn_table.rs
Introduces DynShapeError enum for five validation failure variants and DynReferenceTable<A>, which enforces contiguous FieldSpec layout and per-rule predicate widths at construction, then exposes lookup_bytes and matches_bytes on raw byte slices. Unit tests cover all error variants, lookup behavior, and agreement with the typed ReferenceTable.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: ACL crate scaffolding and the software reference backend implementation.
Description check ✅ Passed The description clearly explains the PR's purpose, scope, and role within the development stack, relating directly to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@acl/src/reference/dyn_table.rs`:
- Around line 89-99: The lookup_bytes and matches_bytes methods use assert_eq!
to validate key length at runtime, which will panic if the key length doesn't
match key_size. For public APIs accepting runtime byte inputs, panicking is not
appropriate. Replace the assert_eq! statements in both methods with graceful
error handling: in lookup_bytes, return None when the key length doesn't match
the expected key_size; in matches_bytes, return an empty Vec when the key length
doesn't match. This ensures the code fails closed without crashing on invalid
input.

In `@acl/src/reference/table.rs`:
- Around line 66-73: The pack function in table.rs silently returns None when
K::KEY_SIZE exceeds MAX_KEY_BYTES, causing every lookup to fail
deterministically without distinguishing between a legitimate "no match" and an
invalid key size. Replace this silent failure with a compile-time assertion or
validation mechanism that ensures KEY_SIZE is validated upfront (such as using
const assertions within the trait implementation or by panicking if the
condition is violated at runtime), so that oversized keys are caught early as a
logic error rather than causing cryptic lookup failures.
- Around line 29-35: The matches_packed function uses debug_assert_eq to check
that fields and specs have matching lengths, but this assertion is compiled away
in release builds. When zip silently truncates in release mode due to a shape
mismatch, missing predicates implicitly become wildcards, unexpectedly widening
ACL matches. Replace the debug_assert_eq on line 30 with an explicit runtime
check that returns false immediately if self.fields.len() does not equal
specs.len(), ensuring shape mismatches are properly rejected in both debug and
release builds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c7bd1fbb-5744-48d6-aed3-aeca13a25166

📥 Commits

Reviewing files that changed from the base of the PR and between 18cd178 and 598ba60.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • Cargo.toml
  • acl/Cargo.toml
  • acl/src/lib.rs
  • acl/src/reference/dyn_table.rs
  • acl/src/reference/mod.rs
  • acl/src/reference/table.rs

Comment on lines +89 to +99
pub fn lookup_bytes(&self, key: &[u8]) -> Option<&A> {
assert_eq!(key.len(), self.key_size, "key length must equal key_size");
self.rules
.iter()
.find(|rule| rule.matches_packed(&self.specs, key))
.map(RefRule::action)
}
#[must_use]
pub fn matches_bytes(&self, key: &[u8]) -> Vec<&RefRule<A>> {
assert_eq!(key.len(), self.key_size, "key length must equal key_size");
self.rules

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not panic on invalid key length in public byte lookup APIs.

Line 90 and Line 98 panic on length mismatch. For runtime byte inputs, this should fail closed without crashing.

As per coding guidelines, “Find logic errors in the code under review. If confident that code is incorrect, suggest a fix.”

Proposed fix
 pub fn lookup_bytes(&self, key: &[u8]) -> Option<&A> {
-    assert_eq!(key.len(), self.key_size, "key length must equal key_size");
+    if key.len() != self.key_size {
+        return None;
+    }
     self.rules
         .iter()
         .find(|rule| rule.matches_packed(&self.specs, key))
         .map(RefRule::action)
 }
@@
 pub fn matches_bytes(&self, key: &[u8]) -> Vec<&RefRule<A>> {
-    assert_eq!(key.len(), self.key_size, "key length must equal key_size");
+    if key.len() != self.key_size {
+        return Vec::new();
+    }
     self.rules
         .iter()
         .filter(|rule| rule.matches_packed(&self.specs, key))
         .collect()
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn lookup_bytes(&self, key: &[u8]) -> Option<&A> {
assert_eq!(key.len(), self.key_size, "key length must equal key_size");
self.rules
.iter()
.find(|rule| rule.matches_packed(&self.specs, key))
.map(RefRule::action)
}
#[must_use]
pub fn matches_bytes(&self, key: &[u8]) -> Vec<&RefRule<A>> {
assert_eq!(key.len(), self.key_size, "key length must equal key_size");
self.rules
pub fn lookup_bytes(&self, key: &[u8]) -> Option<&A> {
if key.len() != self.key_size {
return None;
}
self.rules
.iter()
.find(|rule| rule.matches_packed(&self.specs, key))
.map(RefRule::action)
}
#[must_use]
pub fn matches_bytes(&self, key: &[u8]) -> Vec<&RefRule<A>> {
if key.len() != self.key_size {
return Vec::new();
}
self.rules
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@acl/src/reference/dyn_table.rs` around lines 89 - 99, The lookup_bytes and
matches_bytes methods use assert_eq! to validate key length at runtime, which
will panic if the key length doesn't match key_size. For public APIs accepting
runtime byte inputs, panicking is not appropriate. Replace the assert_eq!
statements in both methods with graceful error handling: in lookup_bytes, return
None when the key length doesn't match the expected key_size; in matches_bytes,
return an empty Vec when the key length doesn't match. This ensures the code
fails closed without crashing on invalid input.

Source: Coding guidelines

Comment on lines +29 to +35
pub(crate) fn matches_packed(&self, specs: &[FieldSpec], buf: &[u8]) -> bool {
debug_assert_eq!(self.fields.len(), specs.len());
self.fields
.iter()
.zip(specs)
.all(|(pred, spec)| pred.matches(&buf[spec.offset..spec.offset + spec.size]))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject shape-mismatched rules during matching.

Line 30 only checks rule/spec arity in debug builds. In release, Line 33 truncates with zip, so missing predicates become implicit wildcards and can widen ACL matches unexpectedly.

As per coding guidelines, “Find logic errors in the code under review. If confident that code is incorrect, suggest a fix.”

Proposed fix
 pub(crate) fn matches_packed(&self, specs: &[FieldSpec], buf: &[u8]) -> bool {
-    debug_assert_eq!(self.fields.len(), specs.len());
+    if self.fields.len() != specs.len() {
+        return false;
+    }
     self.fields
         .iter()
         .zip(specs)
-        .all(|(pred, spec)| pred.matches(&buf[spec.offset..spec.offset + spec.size]))
+        .all(|(pred, spec)| {
+            pred.width() == spec.size
+                && pred.matches(&buf[spec.offset..spec.offset + spec.size])
+        })
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@acl/src/reference/table.rs` around lines 29 - 35, The matches_packed function
uses debug_assert_eq to check that fields and specs have matching lengths, but
this assertion is compiled away in release builds. When zip silently truncates
in release mode due to a shape mismatch, missing predicates implicitly become
wildcards, unexpectedly widening ACL matches. Replace the debug_assert_eq on
line 30 with an explicit runtime check that returns false immediately if
self.fields.len() does not equal specs.len(), ensuring shape mismatches are
properly rejected in both debug and release builds.

Source: Coding guidelines

Comment on lines +66 to +73
fn pack(key: &K) -> Option<[u8; MAX_KEY_BYTES]> {
if K::KEY_SIZE > MAX_KEY_BYTES {
return None;
}
let mut buf = [0u8; MAX_KEY_BYTES];
key.as_key_into(&mut buf[..K::KEY_SIZE]);
Some(buf)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid silent lookup failure for large MatchKey::KEY_SIZE.

Lines 67-68 make every lookup miss when K::KEY_SIZE > 256, which is a deterministic wrong result rather than “no match”.

As per coding guidelines, “Find logic errors in the code under review. If confident that code is incorrect, suggest a fix.”

Proposed fix
-const MAX_KEY_BYTES: usize = 256;
@@
-    fn pack(key: &K) -> Option<[u8; MAX_KEY_BYTES]> {
-        if K::KEY_SIZE > MAX_KEY_BYTES {
-            return None;
-        }
-        let mut buf = [0u8; MAX_KEY_BYTES];
-        key.as_key_into(&mut buf[..K::KEY_SIZE]);
-        Some(buf)
+    fn pack(key: &K) -> Vec<u8> {
+        let mut buf = vec![0u8; K::KEY_SIZE];
+        key.as_key_into(&mut buf);
+        buf
     }
@@
-        let Some(buf) = Self::pack(key) else {
-            return Vec::new();
-        };
+        let buf = Self::pack(key);
@@
-        let buf = Self::pack(key)?;
+        let buf = Self::pack(key);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@acl/src/reference/table.rs` around lines 66 - 73, The pack function in
table.rs silently returns None when K::KEY_SIZE exceeds MAX_KEY_BYTES, causing
every lookup to fail deterministically without distinguishing between a
legitimate "no match" and an invalid key size. Replace this silent failure with
a compile-time assertion or validation mechanism that ensures KEY_SIZE is
validated upfront (such as using const assertions within the trait
implementation or by panicking if the condition is violated at runtime), so that
oversized keys are caught early as a logic error rather than causing cryptic
lookup failures.

Source: Coding guidelines

@daniel-noland daniel-noland force-pushed the pr/daniel-noland/acl-reference branch from 598ba60 to 08b4c60 Compare June 15, 2026 22:08
Introduces the dataplane-acl crate with the software reference
classifier.  The DPDK rte_acl backend lands behind a feature gate in
a follow-up PR.

The reference backend is a linear-scan software classifier built on
the canonical FieldPredicate form from match-action
(rule.into_backend_fields::<Erased>()), so it speaks the same four
predicate kinds (Prefix / Mask / Range / Exact) as every other
backend.  Two roles:

1. Differential-testing oracle against rte_acl (a future PR's
   differential property tests pit both backends against the same
   random rule + packet draws).
2. Non-lossy substrate for a small-delta cascade front over a slow
   tail backend.

Layout:

- src/lib.rs declares the crate-level docs and re-exports the
  reference module.  The dpdk feature gate and dpdk_table_alias!
  macro land alongside the rte_acl backend itself in the next PR.
- src/reference/table.rs is the typed surface: ReferenceTable<K, A>
  parameterised by a MatchKey and an action; RefRule wraps the
  lowered Erased predicates plus an action.  Inline unit tests cover
  positional precedence (first match wins) and the four predicate
  kinds.
- src/reference/dyn_table.rs is the runtime-shape twin:
  DynReferenceTable carries its FieldSpec layout at runtime so
  property tests can fuzz the schema itself.  Returns DynShapeError
  on shape mismatch.

just fmt; cargo check --workspace --all-targets passes.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
@daniel-noland daniel-noland force-pushed the pr/daniel-noland/acl-reference branch from 08b4c60 to 7a7d109 Compare June 18, 2026 19:34
@daniel-noland daniel-noland enabled auto-merge June 18, 2026 19:36
@daniel-noland daniel-noland added this pull request to the merge queue Jun 18, 2026
Merged via the queue into main with commit fe0ec69 Jun 18, 2026
35 checks passed
@daniel-noland daniel-noland deleted the pr/daniel-noland/acl-reference branch June 18, 2026 20:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants