The fapolicyd 1.5 release was about making important failure modes visible. The 1.6 release builds on that by making live daemon state safer to replace.
The short version is:
Build the replacement state separately.
Publish it only after validation.
Let in-flight decisions finish on the generation they started with.
That sounds like a lot of internal engineering work, and it really is. But it improves the reliability of normal operations. A reload should not make one decision using the old integrity mode, a new ruleset, and half of a new trust database. A failed trust database rebuild should not destroy the last good trust database. A maintenance command should not silently replace the physical LMDB storage without a controlled handoff.
fapolicyd 1.6 moves toward an enterprise-style configuration model: live state is published as generations. The daemon can report which generation is active, and a decision worker pins the generation it needs while it is evaluating the access request.
This release also prepares the daemon for a future with multiple decision threads. It does not turn on a worker pool yet. Instead, it removes several shared mutable assumptions that would make a worker pool unsafe. That is really the importance of the 1.6 release - its preparation for the 2.0 release which will bring multiple decision worker threads.
![]() |
| fapolicyd 1.6 makes live state generational so one decision uses one coherent view. |
Generational configuration
Configuration reloads are easy to underestimate.Some settings can only be set at startup. Queue size, cache size, daemon uid/gid, fanotify mark configuration, and the trust database map sizing policy are tied to long-lived runtime objects. They still require a restart.
Other settings affect decisions while the daemon is running. In 1.6, the decision-used settings are published as an immutable configuration generation. That currently includes fields such as permissive mode and integrity mode.
The important behavior is not just that the values can be reloaded. The important behavior is that a decision does not mix values from different reloads. When a permission event starts, fapolicyd pins the active decision configuration generation. That same generation is used while the event is constructed, evaluated, logged, audited, and answered.
This is the kind of operational property administrators expect from enterprise software. Reloads should be observable. They should be atomic from the point of view of work already in progress. They should leave enough evidence in status and metrics reports to explain which version of live state was active.
Run this on a test system with fapolicyd running:
printf 'Before reload:\n' && \
fapolicyd-cli --check-status | grep -E '^(Config generation|Ruleset generation|Trust database generation|LMDB environment generation):' && \
printf '\nMetrics context:\n' && \
fapolicyd-cli --check-metrics | grep -E '^(Last metrics reset|Config generation|Ruleset generation|Trust database generation|LMDB environment generation|Trust database entries|Trust DB lookups|Trust DB reader slots full):' && \
printf '\nAfter reload:\n' && \
sudo kill -HUP "$(cat /run/fapolicyd.pid)" && \
fapolicyd-cli --check-status | grep -E '^(Config generation|Ruleset generation|Trust database generation|LMDB environment generation):'
Before reload:
Config generation: 1 (effective since 2026-07-03 14:09:18 -0400)
Ruleset generation: 1 (effective since 2026-07-03 14:09:18 -0400)
Trust database generation: 1 (effective since 2026-07-03 14:09:24 -0400)
LMDB environment generation: 1 (effective since 2026-07-03 14:09:23 -0400)
Metrics context:
Last metrics reset: never
Config generation: 1 (effective since 2026-07-03 14:09:18 -0400)
Ruleset generation: 1 (effective since 2026-07-03 14:09:18 -0400)
Trust database generation: 1 (effective since 2026-07-03 14:09:24 -0400)
LMDB environment generation: 1 (effective since 2026-07-03 14:09:23 -0400)
Trust database entries: 213870
Trust DB lookups: 407
Trust DB reader slots full: 0
After reload:
Config generation: 2 (effective since 2026-07-03 14:09:54 -0400)
Ruleset generation: 1 (effective since 2026-07-03 14:09:18 -0400)
Trust database generation: 1 (effective since 2026-07-03 14:09:24 -0400)
LMDB environment generation: 1 (effective since 2026-07-03 14:09:23 -0400)
The exact numbers are not important. The point is that the status report now names the generations that make up the daemon's current decision state, and the metrics report carries the same context for the current counter window.
Rulesets are pinned during decisions
The same generation idea now applies to policy snapshots.
fapolicyd rules are ordered. The first matching rule wins. Rules also carry attribute sets, syslog formatting fields, proc-status requirements, a ruleset generation number, and rule identity data. Those pieces need to move together.
In 1.6, a ruleset reload builds a candidate policy snapshot. The candidate owns the rule list, attribute sets, syslog fields, proc-status mask, rule count, and rule-file identity. The daemon publishes the snapshot only after it has been fully built and validated.
A decision pins the active policy snapshot while it evaluates the event and formats the result. If a reload publishes a new ruleset during that time, the decision that already started can still finish on the old snapshot. New decisions use the new generation.
That is a practical correctness improvement today. It is also necessary before multiple decision threads can evaluate different events at the same time. Concurrent readers cannot safely walk a rules list if reload can free or mutate the objects they are walking.
Trust reloads preserve the last good database
The largest visible change in 1.6 is the trust database model.Trust data is one of fapolicyd's major policy inputs. Package backends and local trust files describe files that are expected to be present and trusted. The daemon stores that data in LMDB and consults it while making decisions.
Before the generation work, trust database reloads were easier to reason about as a destructive replacement: drop the old contents and build the new contents. That is a poor fit for a daemon answering live permission events. (It did wait until it was between decisions to do this. But it had to stop all decision making just to replace the trust database.)
The 1.6 model is safer:
build a candidate trust database generation
validate it
publish metadata that makes it active
let old readers drain
reclaim retired generations later
If the candidate cannot be built, the active trust database remains active. If a decision is already reading an older generation, that reader can finish before the retired generation is reclaimed. If reloads arrive during an active rebuild, the daemon coalesces or queues them so that a configuration-changing reload is not silently lost.
A future detailed trust database sizing article will cover this in depth, including manual sizing, automatic sizing, high-water pages, and compaction. The main point for a release overview is that trust database publication is now generational, and failed reloads preserve the last good trust state.
Why the trust database file may need compaction
Most administrators do not need to know LMDB internals. The useful question is simpler:
Is the trust database content current, and is the storage file still a reasonable size?Those are related, but they are not the same thing.
The trust database generation tells you which trusted-file list is active. When packages change or an administrator updates trust files, fapolicyd builds and publishes a new trust database generation. That answers the content question: "which trust data are new decisions using?"
The storage file is the file that holds that list on disk. After package updates, repeated trust reloads, or test rebuilds, the current trusted-file list may be normal size while the storage file still reflects earlier growth. That does not mean the trust data has problems. It means the database file may have holes that can be repacked to save space.
That is what the status report is trying to show with active pages and allocated high-water pages. Active pages describe the trust data currently in use. Allocated high-water pages describe how large the database file has grown internally. If the high-water number is much larger than the active size, the status report may recommend compaction.
Compaction means "build a clean replacement storage file from the current trust sources and swap it in safely." It is similar in spirit to vacuuming or repacking a database.
In fapolicyd 1.6, compaction is controlled. The daemon builds a clean replacement database file in a temporary directory from the current trust sources, validates it, briefly stops new trust database readers, swaps in the replacement, and reopens the database. If that fails, the old database file is preserved or restored.
The status and metrics reports call the storage-file generation the LMDB environment generation. You do not need that term for daily administration. The practical meaning is: this number changes when fapolicyd replaces the database storage file, such as after controlled compaction. The trust database generation changes when the trusted-file contents change.
In 1.6, db_max_size = auto is the default setting. Auto sizing now accounts for reload headroom - not only the final active database size. Manual numeric values are still supported, but the status report can now tell the administrator when a manual value is too small for safe reloads. When in doubt, use auto.
The (future) dedicated trust database sizing article has the commands to investigate and compact this state, so we will not duplicate that full walkthrough here.
Reports show the active generations
The status and metrics reports now identify more than one kind of generation.
fapolicyd-cli --check-status is still the place to ask whether the daemon is healthy and configured as expected. It now reports the active config generation, ruleset generation, trust database generation, LMDB environment generation, and trust database entry count.
fapolicyd-cli --check-metrics is still the place to ask what happened during the current counter window. It includes the same generation headers so the counter window has context.
That matters during investigations. If a denial happened after a rule reload, you want to know which ruleset generation was active. If package updates triggered trust reloads, you want to know which trust database generation is currently used by new decisions. If a compaction ran, you want to know the physical LMDB environment generation changed.
The reader-slot counter is worth keeping. Trust lookups now use private LMDB read transactions and cursors instead of one global read cursor. That is one of the changes needed before future decision workers can look up trust concurrently.
Worker-ready decision state
The daemon still runs with one decision thread in 1.6. But the hot path
is now much closer to the shape needed for more than one.
Earlier
code kept several decision-path objects in shared global or file-static
state: subject and object caches, decision counters, syslog formatting
buffers, deferral state, libmagic handles, device cache state, and LMDB
read state.
That works only as long as one decision thread owns the world.
The 1.6 release introduces an internal decision_context object.
Today there is one active context, so behavior stays compatible. The
important change is ownership. Mutable decision state now has a clearer
owner, which makes it possible for a later worker design to give each
worker its own context instead of having all workers contend on hidden
shared state.
![]() |
| fapolicyd 1.6 moves mutable decision state behind a context so future workers can own their state. |
This does not mean all concurrency work is finished. It means the most obvious shared-state blockers are being removed before the worker pool exists.
Better policy diagnostics
The policy linter also became more useful.
The linter added in the previous release warned about policy rules that can accidentally let executable or programmatic content reach the default-allow path. In 1.6, those warnings became more actionable. The linter can now report rule numbers and source file-line locations when a specific rule is involved. It also warns when an old fapolicyd.rules file shadows compiled.rules during linting.
You can demonstrate this without changing the installed policy. Create a small temporary rules file that intentionally lacks a terminal execute deny:
tmp_rules="$(mktemp)" && \
printf 'allow perm=execute all : all\n' > "$tmp_rules" && \
fapolicyd-cli --check-rules "$tmp_rules" --lint; \
rc=$?; rm -f "$tmp_rules"; echo "exit status: $rc"
Rules file is valid (1 rules)
Policy lint warning: executable events can fall through; no terminal broad execute deny found after rule 1 at /tmp/tmp.SbkGW2TCQF:1
Policy lint hint: add a final "deny_audit perm=execute all : all" rule
Policy lint warning: %languages is not defined in /tmp/tmp.SbkGW2TCQF; programmatic ftype coverage cannot be checked
exit status: 1The exact warning text may differ as the linter evolves, but the point of the demo is that the warning names the policy issue and points at the rule context that caused it.
The release also starts warning about dir=untrusted. That compatibility macro is deprecated and should be replaced with explicit object trust rules. Existing policies still parse, but administrators get a warning so they can plan the migration before the compatibility path is removed in a later major release.
For a safe local check, use a temporary rule:
tmp_rules="$(mktemp)" && \
printf 'allow perm=any dir=untrusted : path=/tmp/payload\n' > "$tmp_rules" && \
fapolicyd-cli --check-rules "$tmp_rules"; \
rc=$?; rm -f "$tmp_rules"; echo "exit status: $rc"07/03/26 14:23:38 [ WARNING ]: rules: line:1: subject dir=untrusted is deprecated and will be removed in a future release
Rules file is valid (1 rules)
exit status: 0
Smaller operational improvements
Several smaller changes round out the release.
fapolicyd-cli --timer-stop now asks before replacing an existing timing report. Timing data can be expensive to collect because it is usually captured around a specific workload. Accidentally overwriting the report with a "timing not armed" result is frustrating, so the CLI now checks before it removes an existing report.
fapolicyd-cli --dump-db handles an empty active trust database correctly in the generation-based layout. fapolicyd-cli --check-trustdb now treats an empty database as success instead of a database walk failure. --check-path now reports trust database initialization failures instead of continuing with partial database state.
ignore_mounts got a broader risk report
The ignored-mount checker was expanded in 1.6. It no longer looks only for files matching the %languages macro. It now reports risk categories such as executable regular files, ELF/shared objects, archives and JARs, bytecode caches, plugin/runtime directories, and language files.
That will be a future article because ignored mounts are easy to misuse. The overview version is simple: ignore_mounts is still for data-only mounts, and the checker now gives administrators better evidence before they decide a mount is safe to ignore.
The practical takeaway
fapolicyd 1.6 is not a release where the most important feature is one new command. The important change is the publication model.
Config, rules, trust contents, and physical LMDB storage now have clearer generation boundaries. Reports expose those boundaries. Decisions pin the state they started with. Failed trust reloads preserve the last good database. Mutable decision state is moving behind an ownership boundary that can support future workers.
The result is a daemon that is easier to operate during reloads, easier to diagnose after package and policy changes, and better prepared for future multi-threaded decision handling.
The upstream project is here:
https://github.com/linux-application-whitelisting/fapolicyd
If you are new to fapolicyd itself, the Red Hat documentation on
blocking and allowing applications with fapolicyd is a useful starting point.

