Skip to content

ROX-30296: track POSIX ACL changes via inode_set_acl LSM hook#878

Open
Stringy wants to merge 13 commits into
mainfrom
giles/ROX-30296-track-acl
Open

ROX-30296: track POSIX ACL changes via inode_set_acl LSM hook#878
Stringy wants to merge 13 commits into
mainfrom
giles/ROX-30296-track-acl

Conversation

@Stringy

@Stringy Stringy commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Description

Adds ACL tracking via inode_set_acl hook. Due to lack of path info in this hook, it is only possible to monitor inode-tracked files (i.e. those on the host file system) this is similar to how xattrs are tracked.

An extension to the BPF checks.c file has been added, because this hook does not exist on older kernels (where ACLs were applied via xattrs, so we still have coverage on those old kernels, just through a different mechanism.)

Tests factor in this optional nature of the hook, and will only run when we know the kernel supports it.

Relies on stackrox/stackrox#21357

Checklist

  • Patch has a change log entry OR does not need one.
  • Investigated and inspected CI test results
  • Updated documentation accordingly

Automated testing

  • Added unit tests
  • Added integration tests
  • Added regression tests

If any of these don't apply, please comment below.

Testing Performed

Tested locally, with setfacl, and the integration tests.

Summary by CodeRabbit

  • New Features
    • Added support for tracking ACL changes in file activity events, including ACL type and entry details.
    • Broadened event handling and test coverage for ACL-related activity.
  • Bug Fixes
    • Improved detection of supported kernel hooks so unsupported hooks no longer block startup.
    • Updated event filtering so ACL events can be included or skipped more flexibly in tests.
  • Chores
    • Updated the changelog and refreshed bundled third-party code reference.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new eBPF LSM hook inode_set_acl to capture POSIX ACL changes, propagating them through kernel metrics, ring-buffer event submission, userspace decoding into new AclSet/AclTag/AclEntry/AclType types, protobuf conversion, kernel capability detection, and corresponding Python integration tests.

Changes

ACL Event Tracking

Layer / File(s) Summary
Kernel ACL data shapes and metrics fields
fact-ebpf/src/bpf/types.h
Adds ACL constants, acl_entry_t, event_t ACL union variant, FILE_ACTIVITY_ACL_SET activity type, and inode_set_acl metrics field.
eBPF LSM hook and event submission
fact-ebpf/src/bpf/checks.c, fact-ebpf/src/bpf/main.c, fact-ebpf/src/bpf/events.h, fact-ebpf/src/lib.rs
Adds check_inode_set_acl LSM stub, trace_inode_set_acl probe with monitoring/metrics, submit_acl_event helper, and extends the metrics accumulate macro.
Kernel capability detection
fact/src/bpf/checks.rs, fact/src/bpf/mod.rs
Adds supports_inode_set_acl via a shared probe_hook helper and conditionally skips loading/attaching the hook when unsupported.
Userspace ACL decoding and protobuf conversion
fact/src/event/mod.rs
Adds FileData::AclSet, AclTag/AclEntry/AclType/AclSetFileData types, accessor wiring, and protobuf FileAclChange conversion.
Userspace metrics registration
fact/src/metrics/kernel_metrics.rs
Registers inode_set_acl counter via define_kernel_metrics!.
Test infrastructure
tests/event.py, tests/server.py, tests/utils.py
Adds EventType.ACL support, replaces skip_xattr boolean with configurable skip tuple, and adds btf_has_symbol helper.
ACL integration tests and xattr updates
tests/test_acl.py, tests/test_xattr.py
Adds new ACL test scenarios and updates xattr tests to use skip=().
Changelog and submodule bump
CHANGELOG.md, third_party/stackrox
Adds changelog entry and bumps submodule pin.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Kernel
  participant check_inode_set_acl
  participant trace_inode_set_acl
  participant submit_acl_event
  participant Ringbuffer
  participant Userspace

  Kernel->>check_inode_set_acl: inode_set_acl LSM hook
  check_inode_set_acl-->>Kernel: return 0
  Kernel->>trace_inode_set_acl: inode_set_acl tracepoint
  trace_inode_set_acl->>trace_inode_set_acl: inode_is_monitored check
  trace_inode_set_acl->>submit_acl_event: submit_acl_event(args, acl_name, kacl)
  submit_acl_event->>Ringbuffer: reserve_event + submit FILE_ACTIVITY_ACL_SET
  Ringbuffer->>Userspace: FileData::new decodes acl entries
  Userspace->>Userspace: FileData::AclSet -> FileAclChange protobuf
Loading

Possibly related PRs

  • stackrox/fact#806: Builds on the same per-hook metrics macro pattern extended here to add the inode_set_acl counter.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.74% 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 clearly and concisely summarizes the main change: adding POSIX ACL tracking via the inode_set_acl LSM hook.
Description check ✅ Passed The description includes a clear summary, checklist, testing notes, and integration-test mention, matching the template well enough.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch giles/ROX-30296-track-acl

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

@codecov-commenter

codecov-commenter commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 120 lines in your changes missing coverage. Please review.
✅ Project coverage is 32.61%. Comparing base (0116956) to head (77c7d4a).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
fact/src/event/mod.rs 0.00% 87 Missing ⚠️
fact/src/bpf/checks.rs 0.00% 21 Missing ⚠️
fact/src/bpf/mod.rs 0.00% 12 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #878      +/-   ##
==========================================
+ Coverage   32.23%   32.61%   +0.37%     
==========================================
  Files          21       21              
  Lines        2736     2919     +183     
  Branches     2736     2919     +183     
==========================================
+ Hits          882      952      +70     
- Misses       1851     1964     +113     
  Partials        3        3              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

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

@Stringy Stringy force-pushed the giles/ROX-30296-track-acl branch 4 times, most recently from 46d84a9 to 11683ba Compare June 29, 2026 09:45
@Stringy Stringy force-pushed the giles/ROX-30296-track-acl branch from 67a3b12 to be7fb22 Compare June 30, 2026 12:28
@Stringy Stringy force-pushed the giles/ROX-30296-track-acl branch from eae45f0 to fc73ed7 Compare June 30, 2026 14:02
@Stringy Stringy marked this pull request as ready for review July 1, 2026 10:40
@Stringy Stringy requested a review from a team as a code owner July 1, 2026 10:40
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
fact/src/event/mod.rs (1)

538-555: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add the missing Chown equality arm.

While extending this manual matcher, FileData::Chown still falls through to _ => false, so two identical ownership-change events compare unequal.

Proposed fix
             (FileData::Unlink(this), FileData::Unlink(other)) => this == other,
             (FileData::Chmod(this), FileData::Chmod(other)) => this == other,
+            (FileData::Chown(this), FileData::Chown(other)) => this == other,
             (FileData::Rename(this), FileData::Rename(other)) => this == other,
             (FileData::SetXattr(this), FileData::SetXattr(other)) => this == other,
🤖 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 `@fact/src/event/mod.rs` around lines 538 - 555, The manual PartialEq
implementation for FileData is missing the FileData::Chown arm, so identical
ownership-change events currently fall through to false. Update the eq method in
FileData’s PartialEq match to add a Chown(this), Chown(other) branch that
compares the contained values the same way as the other variants, keeping the
fallback _ => false only for truly different variants.
🧹 Nitpick comments (1)
fact/src/bpf/checks.rs (1)

34-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider logging the underlying error before treating any load failure as "unsupported."

probe_hook returns false for missing programs, wrong program type, and load failures alike, discarding the error from prog.load(...). This is fine for genuinely unsupported kernels, but it also silently masks a real bug (e.g. a verifier failure in checks.c) as "capability not supported," making such regressions hard to diagnose.

♻️ Suggested diagnostic logging
     fn probe_hook(obj: &mut aya::Ebpf, prog_name: &str, hook: &str, btf: &Btf) -> bool {
         let Some(prog) = obj.program_mut(prog_name) else {
             return false;
         };
         let Ok(prog): Result<&mut Lsm, _> = prog.try_into() else {
             return false;
         };
-        prog.load(hook, btf).is_ok()
+        match prog.load(hook, btf) {
+            Ok(()) => true,
+            Err(e) => {
+                debug!("Failed to probe {hook} support via {prog_name}: {e:#?}");
+                false
+            }
+        }
     }
🤖 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 `@fact/src/bpf/checks.rs` around lines 34 - 42, `probe_hook` is swallowing real
load errors by returning false for every failure case. Update the function to
log the underlying error from `prog.load(hook, btf)` before mapping it to
unsupported, while keeping the existing false behavior for missing programs and
wrong program types. Use the `probe_hook` function and the `Lsm` load path to
locate the change, and make sure the log clearly distinguishes load failures
from genuinely unsupported kernels.

Source: Path instructions

🤖 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 `@fact-ebpf/src/bpf/events.h`:
- Around line 193-199: The ACL copy loop in events.h is always assigning
entry.e_uid.val to args->event->acl.entries[i].e_id, but posix_acl_entry.e_uid
is only valid for ACL_USER and ACL_GROUP. Update the logic in the ACL entry
population path to branch on entry.e_tag and set e_id from e_uid.val only for
those tags; for ACL_USER_OBJ, ACL_GROUP_OBJ, ACL_MASK, and ACL_OTHER, assign
ACL_UNDEFINED_ID instead. Keep the change localized to the ACL event-building
loop so the rest of the field copying in args->event->acl.entries remains
unchanged.

---

Outside diff comments:
In `@fact/src/event/mod.rs`:
- Around line 538-555: The manual PartialEq implementation for FileData is
missing the FileData::Chown arm, so identical ownership-change events currently
fall through to false. Update the eq method in FileData’s PartialEq match to add
a Chown(this), Chown(other) branch that compares the contained values the same
way as the other variants, keeping the fallback _ => false only for truly
different variants.

---

Nitpick comments:
In `@fact/src/bpf/checks.rs`:
- Around line 34-42: `probe_hook` is swallowing real load errors by returning
false for every failure case. Update the function to log the underlying error
from `prog.load(hook, btf)` before mapping it to unsupported, while keeping the
existing false behavior for missing programs and wrong program types. Use the
`probe_hook` function and the `Lsm` load path to locate the change, and make
sure the log clearly distinguishes load failures from genuinely unsupported
kernels.
🪄 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: Path: .coderabbit.yml

Review profile: CHILL

Plan: Enterprise

Run ID: 5b916de2-f11e-48d7-b345-c3ed9b660223

📥 Commits

Reviewing files that changed from the base of the PR and between 3a4a860 and 4be488a.

📒 Files selected for processing (16)
  • CHANGELOG.md
  • fact-ebpf/src/bpf/checks.c
  • fact-ebpf/src/bpf/events.h
  • fact-ebpf/src/bpf/main.c
  • fact-ebpf/src/bpf/types.h
  • fact-ebpf/src/lib.rs
  • fact/src/bpf/checks.rs
  • fact/src/bpf/mod.rs
  • fact/src/event/mod.rs
  • fact/src/metrics/kernel_metrics.rs
  • tests/event.py
  • tests/server.py
  • tests/test_acl.py
  • tests/test_xattr.py
  • tests/utils.py
  • third_party/stackrox

Comment thread fact-ebpf/src/bpf/events.h

@Molter73 Molter73 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Overall looks good! Thanks for tackling this one!

There's a couple comments, mostly questions so I expect we should be able to merge this soon.

Comment thread fact-ebpf/src/bpf/events.h
Comment thread fact-ebpf/src/bpf/events.h
Comment thread fact-ebpf/src/bpf/events.h Outdated
args->event->acl.acl_type = FACT_ACL_TYPE_ACCESS;
}

if (kacl == NULL) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[ultra-nit] Can we negate this condition so the largest block is the one for the true condition and not the else?

Comment thread fact-ebpf/src/bpf/types.h
Comment thread fact/src/event/mod.rs Outdated
Comment on lines +681 to +687
0x01 => AclTag::UserObj,
0x02 => AclTag::User,
0x04 => AclTag::GroupObj,
0x08 => AclTag::Group,
0x10 => AclTag::Mask,
0x20 => AclTag::Other,
other => AclTag::Unknown(other),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Have you considered creating a enum acl_tag_t in the bpf code to encode these values at a lower level?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good plan, will add.

Comment thread fact/src/event/mod.rs Outdated
perm: e.perm as u32,
// ACL_UNDEFINED_ID (0xFFFFFFFF) for entries that don't
// carry a uid/gid (USER_OBJ, GROUP_OBJ, MASK, OTHER).
id: e.id.unwrap_or(0xFFFFFFFF),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
id: e.id.unwrap_or(0xFFFFFFFF),
id: e.id.unwrap_or(u32::MAX),

Comment thread fact/src/event/mod.rs Outdated
Comment on lines +752 to +758
.map(|e| fact_api::AclEntry {
tag: i32::from(e.tag),
perm: e.perm as u32,
// ACL_UNDEFINED_ID (0xFFFFFFFF) for entries that don't
// carry a uid/gid (USER_OBJ, GROUP_OBJ, MASK, OTHER).
id: e.id.unwrap_or(0xFFFFFFFF),
})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Consider adding impl From<AclEntry> for fact_api::AclEntry and replacing this block with

Suggested change
.map(|e| fact_api::AclEntry {
tag: i32::from(e.tag),
perm: e.perm as u32,
// ACL_UNDEFINED_ID (0xFFFFFFFF) for entries that don't
// carry a uid/gid (USER_OBJ, GROUP_OBJ, MASK, OTHER).
id: e.id.unwrap_or(0xFFFFFFFF),
})
.map(fact_api::AclEntry::from)

Comment thread tests/test_acl.py Outdated
_ACL_VERSION = 2
_ACL_UNDEFINED_ID = 0xFFFFFFFF

# Kernel ACL tag values (from include/uapi/linux/posix_acl.h)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This comment should be added to the main code as well, better if it is a permalink to github or elixir.bootlin

Comment thread tests/test_acl.py Outdated
Comment on lines +114 to +117
The monitored_dir fixture is tracked by path prefix, but since
the ACL hook only monitors by inode, we need the directory to
be inode-tracked. monitored_dir is included in fact's paths
config, so fact tracks it by inode after the initial scan.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What? There's no configuration change happening in this test, what?

Comment thread tests/test_acl.py Outdated
Comment on lines +279 to +282
# Verify the server is working by doing a chmod on a monitored file
process = Process.from_proc()
mode = 0o644
os.chmod(test_file, mode)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why chmod? Why not just setting an acl on that file? If you use different type of events a misconfiguration on the server could exclude all acl events and inadvertently pass the test when the acl event is wrongly generated.

@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.

🧹 Nitpick comments (1)
fact-ebpf/src/bpf/types.h (1)

82-103: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider fixed-width types for wire-format enum fields.

acl_type_t and acl_tag_t are plain C enums with implementation-defined underlying size, but they're used directly as fields in acl_entry_t/the acl union member (Line 100, Line 149) that gets copied byte-for-byte into a ring-buffer event later decoded by a separate Rust binary. If the enum's compiled width ever diverges from what the Rust decoder assumes, the ACL payload would be silently misparsed. Using an explicit fixed-width type (e.g., unsigned char) for e_tag/acl_type at the struct-field level (while keeping the enum for symbolic constants elsewhere) would make the cross-language layout contract explicit and robust.

♻️ Proposed fix
 struct acl_entry_t {
-  acl_tag_t e_tag;
+  unsigned char e_tag; // acl_tag_t value
   unsigned short e_perm;
   unsigned int e_id;
 };
🤖 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 `@fact-ebpf/src/bpf/types.h` around lines 82 - 103, `acl_type_t` and
`acl_tag_t` are being used as wire-format field types in `acl_entry_t` and the
`acl` union, but plain enums have implementation-defined size. Update the struct
fields that are serialized across the ring buffer to use an explicit fixed-width
type such as `unsigned char`, while keeping `acl_type_t` and `acl_tag_t` only as
symbolic enums for constants. Make sure the `acl_entry_t` definition and the
`acl` union member stay layout-compatible with the Rust decoder.
🤖 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.

Nitpick comments:
In `@fact-ebpf/src/bpf/types.h`:
- Around line 82-103: `acl_type_t` and `acl_tag_t` are being used as wire-format
field types in `acl_entry_t` and the `acl` union, but plain enums have
implementation-defined size. Update the struct fields that are serialized across
the ring buffer to use an explicit fixed-width type such as `unsigned char`,
while keeping `acl_type_t` and `acl_tag_t` only as symbolic enums for constants.
Make sure the `acl_entry_t` definition and the `acl` union member stay
layout-compatible with the Rust decoder.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Enterprise

Run ID: ba942558-b1cd-4991-8566-24551f09d46b

📥 Commits

Reviewing files that changed from the base of the PR and between 4be488a and 77c7d4a.

📒 Files selected for processing (6)
  • fact-ebpf/src/bpf/events.h
  • fact-ebpf/src/bpf/types.h
  • fact/src/bpf/checks.rs
  • fact/src/bpf/mod.rs
  • fact/src/event/mod.rs
  • tests/test_acl.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • fact/src/bpf/checks.rs
  • fact-ebpf/src/bpf/events.h
  • tests/test_acl.py
  • fact/src/bpf/mod.rs
  • fact/src/event/mod.rs

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