fapolicyd 2.0 introduces parallel decision workers and several policy-authoring updates. Decision workers let the daemon process independent subjects in parallel. Also new in 2.0, policy authors can use explicit path globs, quote rule values that contain spaces, and extend the built-in `%languages` MIME set from a package rule fragment.
This is an overview of the release. The decision-worker architecture, glob rule contract, and Debian trust filter each have enough operational detail for future articles. Here, the goal is to show what changed, how to observe it, and where to start on a test system.
Parallel decision workers
fapolicyd receives permission events through fanotify, evaluates policy, and returns a response to the kernel. In 2.0, the fanotify reader acts as a dispatcher. It assigns each permission event to a decision worker, and that worker evaluates the event and owns the response.
The configuration option is:
decision_threads = 1The default remains one worker. With a larger value, the dispatcher routes a process's events to one stable worker using its PID. That preserves the order seen by the subject startup and pattern-detection state for that process. It also means that a busy single process stays on one worker; worker parallelism comes from activity across independent processes.
Each worker owns an event queue, decision context, subject cache, object cache, and subject-slot defer array. This keeps mutable decision state local to the worker that evaluates the event. Queue, cache, and defer statistics remain available both as aggregate totals and as per-worker detail.
![]() |
| fapolicyd 2.0 dispatches each process's events to a stable decision worker. |
Choosing a worker count
decision_threads is a capacity setting, not a value to maximize automatically. Start with the default and use the reports under a representative workload. Consider adding a worker when queue pressure is persistent: a high inter-thread maximum queue depth relative to q_size, a non-zero queue-full count, or a growing oldest queued age.
Inspect the relevant report sections, run the following commands :
fapolicyd-cli --check-status | sed -n '/Resource configuration:/,/Resource utilization:/p'
fapolicyd-cli --check-metrics | sed -n '/Inter-thread queue & defer activity:/,/Subject cache effectiveness:/p'
The per-worker lines matter. One worker with a deep queue while others are idle is useful evidence of routing skew. Stable process ownership is intentional, so queue-depth balancing does not move that process's events to another worker.
On a test system, change the count from one to two, restart fapolicyd, and compare the same workload and reports. If aggregate queue pressure decreases without undesirable resource pressure, increase in small steps and repeat the comparison. The configured count cannot exceed the number of online CPU cores or the daemon's resource limits.
The resource cost scales with the worker count. q_size, subj_cache_size, and obj_cache_size are per-worker settings, as is the derived defer-array capacity. Leaving those values unchanged increases total queue and cache capacity. Reducing them to hold memory use steady can increase cache collisions, evictions, and defer fallbacks. The daemon also reserves LMDB reader slots and validates the file-descriptor and fixed-memory budgets at
startup.
decision_threads requires a restart because workers and their resources are created during daemon startup:
# /etc/fapolicyd/fapolicyd.conf
decision_threads = 2
sudo systemctl restart fapolicyd
The worker follow-up article will cover routing, report interpretation, skew, capacity planning, and staged test workloads in more depth.
Worker health and service supervision
The status report identifies the configured worker count and reports health, current queue depth, maximum queue depth, queue-full count, and oldest queued age for each worker. The metrics report includes the corresponding aggregate and per-worker queue, cache, and defer counters. This makes it possible to distinguish overall pressure from a single hot worker.
When built with the default systemd watchdog support, the installed service uses Type=notify, runs fapolicyd in the foreground, and sets WatchdogSec=10s. The daemon reports readiness after startup and sends watchdog messages while the health monitor sees decision workers making progress. A stalled worker with queued work is recorded as a worker_stall failure action, after which systemd applies the unit's restart policy.
Use a systemd drop-in to adjust WatchdogSec for an environment with known slow storage. Builds configured with --disable-systemd-watchdog install the legacy forking service unit instead. See `doc/systemd-watchdog.md` in the source tree for the complete service contract.
Policy drop-ins and MIME classification
The installed %languages set identifies MIME types that policy treats as language or interpreter content. A package can add a MIME type without editing the distribution-owned definition. Place a naturally ordered macro fragment after 10-languages.rules and before policy rules:# /etc/fapolicyd/rules.d/11-vendor-languages.rules
%languages+=application/x-vendor-script
The += form is limited to %languages. It adds to the existing set rather than replacing it, and a repeated MIME type is harmless. Run fagenrules after adding or removing the fragment. The MIME type must already be emitted by fapolicyd's file type detector or the system magic database; adding it to the set does not create file type detection.
The ignored-mount risk report now reads the effective compiled policy, so it also recognizes MIME types added through %languages+= fragments. This keeps the policy and the operational report aligned.
![]() |
| A package policy fragment can extend the effective %languages MIME set without replacing the distribution-owned definition. |
Use --ftype-ext when a script needs both the supplied path and fapolicyd's MIME type. Each invocation writes two NUL-terminated fields to standard output: the path, followed by the MIME type. The interface is intentionally binary; use --ftype for interactive, MIME-only output.
A pathname component can contain spaces, tabs, newlines, quotes, backslashes, colons, terminal-control sequences, and other bytes except NUL and /. A line or colon delimiter could let one of those bytes create an apparent second record or misleading terminal output. NUL framing preserves the path/MIME boundary, but does not make a path safe for shell evaluation or terminal display.
A Bash consumer can read one or more pairs safely:
for file in "$@"; do
fapolicyd-cli --ftype-ext="$file"
done |
while IFS= read -r -d '' path &&
IFS= read -r -d '' mime
do
printf 'path=%q mime=%q\n' "$path" "$mime"
done
Do not use command substitution, unquoted expansions, eval, or newline-delimited file lists for these values. Quote variables when passing paths to commands, and use find -print0 with the same NUL-aware read pattern when discovering paths.
Quoted rule values
Rule values for subject exe, subject or object dir, and object path can now use double quotes when the complete value contains a space:
allow perm=execute all : path="/opt/vendor/My Tool/bin/tool" trust=1
Double quotes also work in named string sets and comma-separated string values. They are rule-file syntax rather than part of the value used for matching. Use shell quoting, rather than rule quoting, when passing a path with spaces to a CLI command.
Explicit path globbing
Rules can now use fnmatch patterns for subject exe and object path attributes. Pattern matching is explicit: prefix a pattern with glob:. Unprefixed values remain exact, so a literal *, ?, or [ in an existing path rule keeps its exact-path meaning.
Patterns must be absolute paths. They do not match a leading period implicitly, and do not support recursive ** matching. The dir attribute remains a literal directory-prefix match. A glob selects a path; it does not make that path trusted, so allow rules for writable locations should normally retain trust=1 or an explicit FILE_HASH constraint.
Globbing will be covered in a future blog, including quoted glob values, named sets, literal metacharacters, rule order, symbolic-link behavior, and policy test cases.
Other operational changes
fagenrules now builds its output in a temporary file, validates it with the daemon policy parser, and atomically renames a successful candidate into compiled.rules. It prints a SHA-256 ruleset identity for an unchanged or installed ruleset. Use fagenrules --check before an update to determine whether the assembled policy differs without replacing compiled.rules. The rule assembler also handles rule-unit file names containing spaces.
For permission events that need a decision log, fapolicyd renders the selected record while the subject is still available, sends the fanotify response, then hands the owned text to the asynchronous logger. The bounded logger queue serializes accepted records through one drain thread.
The Debian debdb source applies fapolicyd-filter.conf before package-owned paths enter its helper-produced trust snapshot. On package sets where the filter excludes many files, this can reduce active LMDB pages, memory residency, and trust lookup time. Debian filter configuration and measurements will be covered in a future blog.
For RPM trust data, rpm_sha256_only=1 applies a SHA-256-or-stronger digest floor during RPM import and relevant SHA-256 or IMA integrity lookups. The configuration and integrity mode together determine whether that policy is appropriate for a system.
The practical takeaway
Use decision workers when reports show queue pressure under the workload that matters to the system. Begin at one, test two, and increase only when the measurements justify the additional per-worker resources.
Use glob: when a policy genuinely needs a variable path component, and use double quotes for a complete rule value with spaces. Package policy can extend %languages without replacing its base definition. Keep exact paths and dir matches where they express the policy, and retain trust constraints when a pattern can cover writable locations.
The relevant references are fapolicyd.conf(5), fapolicyd.state(5), fapolicyd.metrics(5), fapolicyd.rules(5), and fagenrules(8). The upstream project is fapolicyd on GitHub.


No comments:
Post a Comment