pax_global_header00006660000000000000000000000064152407164400014515gustar00rootroot0000000000000052 comment=1cc645b0eae9923f360096d6b9a8354187e16dfd fapolicyd-2.0/000077500000000000000000000000001524071644000133305ustar00rootroot00000000000000fapolicyd-2.0/.editorconfig000066400000000000000000000007201524071644000160040ustar00rootroot00000000000000 root = true [{*.{c,h,spec},Makefile,Makefile.*}] charset = utf-8 end_of_line = lf indent_style = tab indent_size = 8 trim_trailing_whitespace = true insert_final_newline = true [*.py] charset = utf-8 end_of_line = lf indent_style = space indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true [*.{ac,m4,yml}] charset = utf-8 end_of_line = lf indent_style = space indent_size = 2 trim_trailing_whitespace = true insert_final_newline = true fapolicyd-2.0/.fmf/000077500000000000000000000000001524071644000141565ustar00rootroot00000000000000fapolicyd-2.0/.fmf/version000066400000000000000000000000021524071644000155560ustar00rootroot000000000000001 fapolicyd-2.0/.github/000077500000000000000000000000001524071644000146705ustar00rootroot00000000000000fapolicyd-2.0/.github/workflows/000077500000000000000000000000001524071644000167255ustar00rootroot00000000000000fapolicyd-2.0/.github/workflows/.rawhide-fedora-build000066400000000000000000000014261524071644000227070ustar00rootroot00000000000000name: rawhide-build on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest container: fedora:rawhide steps: - uses: actions/checkout@v2 - name: getting envinronment info run: uname -a - name: print fedora version run: cat /etc/fedora-release - name: installing dependecies run: dnf -y install dnf-plugins-core python3-dnf-plugins-core; dnf -y builddep ./fapolicyd.spec - name: generate config files run: ./autogen.sh - name: configure run: ./configure --with-perf-test --with-rpm --with-audit --disable-shared --disable-dependency-tracking - name: build run: make - name: check run: make check - name: dist run: make dist fapolicyd-2.0/.github/workflows/almalinux9.yml000066400000000000000000000016041524071644000215340ustar00rootroot00000000000000name: almalinux9-build on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest container: almalinux:9 steps: - uses: actions/checkout@v2 - name: getting envinronment info run: uname -a - name: print version run: cat /etc/*release - name: installing dependencies 1 run: dnf -y install epel-release dnf-plugins-core gawk python3-dnf-plugins-core util-linux --nogpgcheck - name: installing dependencies 2 run: dnf -y builddep ./fapolicyd.spec --enablerepo='crb' --nogpgcheck - name: generate config files run: ./autogen.sh - name: configure run: ./configure --with-perf-test --with-rpm --with-audit --disable-shared --disable-dependency-tracking - name: build run: make - name: check run: make check - name: dist run: make dist fapolicyd-2.0/.github/workflows/centosstream10.yml000066400000000000000000000016331524071644000223230ustar00rootroot00000000000000name: centosstream10-build on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest container: quay.io/centos/centos:stream10 steps: - uses: actions/checkout@v2 - name: getting envinronment info run: uname -a - name: print version run: cat /etc/*release - name: installing dependencies 1 run: dnf -y install epel-release dnf-plugins-core gawk python3-dnf-plugins-core util-linux --nogpgcheck - name: installing dependencies 2 run: dnf -y builddep ./fapolicyd.spec --enablerepo='crb' --nogpgcheck - name: generate config files run: ./autogen.sh - name: configure run: ./configure --with-perf-test --with-rpm --with-audit --disable-shared --disable-dependency-tracking - name: build run: make - name: check run: make check - name: dist run: make dist fapolicyd-2.0/.github/workflows/rockylinux9.yml000066400000000000000000000016211524071644000217500ustar00rootroot00000000000000name: rockylinux9-build on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest container: rockylinux/rockylinux:9 steps: - uses: actions/checkout@v2 - name: getting envinronment info run: uname -a - name: print version run: cat /etc/*release - name: installing dependencies 1 run: dnf -y install epel-release dnf-plugins-core gawk python3-dnf-plugins-core util-linux --nogpgcheck - name: installing dependencies 2 run: dnf -y builddep ./fapolicyd.spec --enablerepo='crb' --nogpgcheck - name: generate config files run: ./autogen.sh - name: configure run: ./configure --with-perf-test --with-rpm --with-audit --disable-shared --disable-dependency-tracking - name: build run: make - name: check run: make check - name: dist run: make dist fapolicyd-2.0/.github/workflows/stable-fedora-build.yml000066400000000000000000000014501524071644000232550ustar00rootroot00000000000000name: stable-fedora-build on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest container: fedora:latest steps: - uses: actions/checkout@v2 - name: getting envinronment info run: uname -a - name: print fedora version run: cat /etc/fedora-release - name: installing dependencies run: dnf -y install dnf-plugins-core gawk python3-dnf-plugins-core util-linux; dnf -y builddep ./fapolicyd.spec - name: generate config files run: ./autogen.sh - name: configure run: ./configure --with-perf-test --with-rpm --with-audit --disable-shared --disable-dependency-tracking - name: build run: make - name: check run: make check - name: dist run: make dist fapolicyd-2.0/.github/workflows/ubuntu22.yml000066400000000000000000000020761524071644000211430ustar00rootroot00000000000000name: ubuntu22-build on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest container: ubuntu:jammy steps: - uses: actions/checkout@v2 - name: getting envinronment info run: uname -a - name: print version run: cat /etc/*release - name: Ensure up to date package list run: apt update - name: installing dependencies 1 run: apt install -y autoconf automake libtool gawk gcc libdpkg-dev libmd-dev uthash-dev liblmdb-dev libudev-dev - name: installing dependencies 2 run: apt install -y libgcrypt-dev libssl-dev libmagic-dev libcap-ng-dev libseccomp-dev make debmake debhelper - name: generate config files run: ./autogen.sh - name: configure run: ./configure --with-perf-test --without-rpm --with-audit --disable-shared --disable-dependency-tracking --with-deb - name: build run: make - name: check run: make check - name: dist run: make dist - name: build deb run: cd deb && ./build_deb.sh fapolicyd-2.0/.gitignore000066400000000000000000000017451524071644000153270ustar00rootroot00000000000000# Based on https://gitignore.org # Autotools.gitignore # http://www.gnu.org/software/automake Makefile.in /test-driver .deps/ .dirstamp # http://www.gnu.org/software/autoconf autom4te.cache /aclocal.m4 /compile /config.cache /config.guess /config.h /config.h.in /config.log /config.status /config.sub /configure /configure.scan /depcomp /install-sh /missing /stamp-h1 # https://www.gnu.org/software/libtool/ /libtool /ltmain.sh .libs/ *~ # http://www.gnu.org/software/m4/ m4/libtool.m4 m4/ltoptions.m4 m4/ltsugar.m4 m4/ltversion.m4 m4/lt~obsolete.m4 # Generated Makefile # (meta build system like autotools, # can automatically generate from config.status script # (which is called by configure script)) Makefile # Based on https://gitignore.org # C.gitignore # Object files *.o # Libraries *.a *.la *.lo # Shared objects *.so *.so.* # fapolicyd specific init/fapolicyd-magic.mgc src/fapolicyd src/fapolicyd-cli src/fapolicyd-perf-test src/fapolicyd-rpm-loader *.log *.rpm *.tar.gz fapolicyd-2.0/.packit.yaml000066400000000000000000000033321524071644000155460ustar00rootroot00000000000000specfile_path: fapolicyd.spec upstream_package_name: fapolicyd downstream_package_name: fapolicyd upstream_tag_template: v{version} jobs: - job: copr_build trigger: pull_request identifier: build-normal actions: post-upstream-clone: - bash -c "sed -i 's/#ELN %//' fapolicyd.spec" - bash -c "curl -L -o $(grep Source1 fapolicyd.spec | cut -d ' ' -f 2 | sed -r \"s/%\{name\}/$(grep 'Name:' fapolicyd.spec | cut -f 2 -d ' ')/g;s/%\{semodule_version\}/$(grep '%define semodule_version' fapolicyd.spec | cut -f 3 -d ' ')/g\" | sed -r 's|(.*)#/(.*)|\2 \1|')" - bash -c "pwd" - bash -c "ls -la" targets: - fedora-all - epel-10 - epel-9 # - job: copr_build # trigger: pull_request # identifier: build-asan # actions: # post-upstream-clone: # - bash -c "sed -i 's/#ASAN %//' fapolicyd.spec" # - bash -c "sed -i 's/#ELN %//' fapolicyd.spec" # - bash -c "curl -L -o $(grep Source1 fapolicyd.spec | cut -d ' ' -f 2 | sed -r \"s/%\{name\}/$(grep 'Name:' fapolicyd.spec | cut -f 2 -d ' ')/g;s/%\{semodule_version\}/$(grep '%define semodule_version' fapolicyd.spec | cut -f 3 -d ' ')/g\" | sed -r 's|(.*)#/(.*)|\2 \1|')" # - bash -c "curl -L -o $(grep Source2 fapolicyd.spec | cut -d ' ' -f 2 | sed -r 's|(.*)#/(.*)|\2 \1|')" # - bash -c "cp uthash*.tar.gz /builddir/build/SOURCES/" # - bash -c "pwd" # - bash -c "ls -la" # targets: # - fedora-all # - epel-9 - job: tests trigger: pull_request identifier: build-normal targets: - fedora-all - epel-10 - epel-9 # - job: tests # trigger: pull_request # identifier: build-asan # targets: # - fedora-all # - epel-9 fapolicyd-2.0/AGENTS.md000066400000000000000000000067761524071644000146530ustar00rootroot00000000000000# Repository Guidelines ## Working Agreements - This is `fapolicyd`, an autotools C project for the daemon, CLI, policy rules, trust database backends, and package manager integration helpers. - Before changing files, inspect `git status --short --branch`. Keep `main` clean; work on a focused branch and leave unrelated local changes alone. - Prefer `rg`/`rg --files` for search. Make surgical changes that match the existing module and style. ## Build And Test - Normal local build: ```sh ./autogen.sh ./configure --with-audit --disable-shared make -j$(nproc) ``` - `--with-rpm` is the default when librpm is available. Use `--without-rpm` if rpm development libraries are missing. - On Fedora/RHEL hosts, skip Debian/libdpkg support unless `libdpkg` and `libmd` are confirmed installed. - Useful optional configure flags: `--with-asan`, `--with-perf-test`, and `--enable-stress`. - Run `make check` when feasible. For a targeted test after configuring, use the Automake form, for example `make -C src/tests check TESTS=rules_test`. - The stress helper is opt-in: configure with `--enable-stress`, then build or test under `src/tests/stress`. See `src/tests/stress/README.md` before using it against a running daemon. - If you created autotools build artifacts, run `make maintainer-clean` before finishing unless the user explicitly wants the build tree preserved. ## Project Map - `src/library`: shared policy, rules, trust DB, event, cache, metrics, and utility code used by the daemon and CLI. - `src/daemon`: daemon entry point, fanotify handling, mounts, and state reporting. - `src/cli`: `fapolicyd-cli`, rule linting, ignore-mount checks, and related user-facing commands. - `src/handler`: package-manager helper programs such as the RPM loader. - `src/perf-test`: optional installed performance test utility. - `src/tests`: unit and regression tests; `src/tests/stress` is a separate non-installed stress harness. - `init`: default config, magic database inputs, systemd unit, tmpfiles, and bash completion. - `doc`: man pages and manpage checks. - `rules.d`: policy rule units consumed by `fagenrules`. - `dnf`, `deb`, `CI`, `.fmf`: packaging, integration, and CI metadata. ## C Style - Follow Linux kernel style: tabs are 8 columns, keep lines near 80 columns, match existing brace style, and avoid unrelated whitespace churn. - New C functions need a short comment describing purpose, inputs, and return behavior. Prefer comments that explain why edge-case code exists. - Try hard to keep functions at 4 arguments or fewer. If more data is needed, consider a focused struct or smaller functions. - Keep `.c` files class-like and focused. Add small local helpers to the file they support; extract a new module only when a related helper cluster has its own state, lifecycle, naming prefix, or reusable purpose. - Preserve existing names and patterns unless the change requires otherwise. ## Rules And Policy Files - The authoritative rule syntax reference is `doc/fapolicyd.rules.5`. - Keep `rules.d` files in the numbered groups documented in `rules.d/README-rules`, and preserve natural sort order because first match wins. - Do not add new `dir=untrusted` policy. It is deprecated and should be replaced with explicit trust-based object matching. ## Commits - Commit only when asked. Make one logical commit per issue. - Do not commit on `main`. - Commit messages should have a concise subject plus body paragraphs explaining what was wrong, how it was fixed, and what tests were added or run. fapolicyd-2.0/AUTHORS000066400000000000000000000001131524071644000143730ustar00rootroot00000000000000This program was created and maintained by Steve Grubb fapolicyd-2.0/BUILD.md000066400000000000000000000106331524071644000145140ustar00rootroot00000000000000BUILDING ======== Building fapolicyd is reasonably straightforward on Fedora and RedHat-based Linux distributions. This document will guide in installing the build-time dependencies, configure and compile the code, and finally build the RPMs for distribution on compatible non-production systems. BUILD-TIME DEPENDENCIES (fedora and RHEL8) ------------------------------------------ * gcc * autoconf * automake * libtool * make * libudev-devel * kernel-headers * systemd-devel * libgcrypt-devel ( <= fapolicyd-1.1.3) * openssl ( >= fapolicyd-1.1.4) * rpm-devel (optional) * file * file-devel * libcap-ng-devel * libseccomp-devel * lmdb-devel * uthash-devel * python3-devel * kernel >= 4.20 (Must support FANOTIFY_OPEN_EXEC_PERM. See [1] below.) RHEL8: ENABLE CODEREADY AND INSTALL EPEL REPOS ---------------------------------------------- ```bash sudo subscription-manager repos --enable codeready-builder-for-rhel-8-$(arch)-rpms sudo dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm ``` INSTALL BUILD DEPENDENCIES (fedora and RHEL8) --------------------------------------------- ```bash sudo dnf install -y gcc autoconf automake libtool make libudev-devel kernel-headers systemd-devel libgcrypt-devel rpm-devel file file-devel libcap-ng-devel libseccomp-devel lmdb-devel uthash-devel python3-devel ``` CONFIGURING AND COMPILING ------------------------- To build from the repo after cloning and installing dependencies: ```bash cd fapolicyd ./autogen.sh ./configure --with-audit --disable-shared make make dist ``` This will create a tarball. You can use the new tarball with the spec file and create your own rpm. If you want to experiment without installing, just run make with no arguments. It should run fine from where it was built as long as you put the configuration files in /etc/fapolicyd (fapolicyd.rules, fapolicyd.trust, fapolicyd.conf). Note that the shipped policy expects that auditing is enabled. This is done by passing --with-audit to ./configure. The use of rpm as a trust source is now optional. You can run ./configure passing --without-rpm and it will not link against librpm. In this mode, it purely uses the file database in fapolicyd.trust. If rpm is used, then the file trust database can be used in addition to rpmdb. BUILDING THE RPMS ----------------- :exclamation: These unofficial RPMs should only be used for testing and experimentation purposes and not for production systems. :exclamation: To build the RPMs, first install the RPM development tools: ```bash sudo dnf install -y rpmdevtools ``` Then in the root of the repository where fapolicyd was built, use `rpmbuild` to build the RPMs: ```bash rpmbuild -ta fapolicyd-*.tar.gz ``` By default, the RPMs will appear in `~/rpmbuild/RPMS/$(arch)`. NOT BUILDING RPMS ----------------- If you chose to do it yourself, you need to do a couple prep steps: ``` 1) sed -i "s/%python2_path%/`readlink -f /bin/python2 | sed 's/\//\\\\\//g'`/g" rules.d/*.rules 2) sed -i "s/%python2_path%/`readlink -f /bin/python3 | sed 's/\//\\\\\//g'`/g" rules.d/*.rules 3) interpret=`readelf -e /usr/bin/bash \ | grep Requesting \ | sed 's/.$//' \ | rev | cut -d" " -f1 \ | rev` 4) sed -i "s|%ld_so_path%|`realpath $interpret`|g" rules.d/*.rules ``` This corrects the placeholders to match your current system. Then follow the rules listed above for compiling except run make install instead of make dist. CREATING RUNTIME ENVIRONMENT ---------------------------- If you are not using rpm's spec file and are doing it yourself, there are a few more steps. You need to create the necessary directories in the right spot: ``` mkdir -p /etc/fapolicyd/{rules.d,trust.d} mkdir -p /var/lib/fapolicyd/ mkdir --mode=0755 -p /usr/share/fapolicyd/ mkdir -p /usr/lib/tmpfiles.d/ mkdir --mode=0755 -p /run/fapolicyd/ cd init cp fapolicyd.bash_completion /etc/bash_completion.d/ cp fapolicyd.conf /etc/fapolicyd/ cp fapolicyd-magic /usr/share/fapolicyd/ cp fapolicyd-magic.mgc /usr/share/fapolicyd/ cp fapolicyd.service /usr/lib/systemd/system/ cp fapolicyd-tmpfiles.conf /usr/lib/tmpfiles.d/fapolicyd.conf cp fapolicyd.trust /etc/fapolicyd/trust.d/ useradd -r -M -d /var/lib/fapolicyd -s /sbin/nologin -c "Application Whitelisting Daemon" fapolicyd chown root:fapolicyd /etc/fapolicyd/ chown root:fapolicyd /etc/fapolicyd/rules.d/ chown root:fapolicyd /etc/fapolicyd/trust.d/ chown root:fapolicyd /var/lib/fapolicyd/ ``` fapolicyd-2.0/CI/000077500000000000000000000000001524071644000136235ustar00rootroot00000000000000fapolicyd-2.0/CI/ci-tests.fmf000066400000000000000000000002541524071644000160510ustar00rootroot00000000000000discover: how: fmf url: https://github.com/linux-application-whitelisting/fapolicyd-tests.git filter: component:fapolicyd & tag:CI-Tier-1 execute: how: tmt fapolicyd-2.0/COPYING000066400000000000000000001045131524071644000143670ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . fapolicyd-2.0/ChangeLog000066400000000000000000000411361524071644000151070ustar00rootroot000000000000002.0 - Add decision_threads configuration to fapolicyd.conf - Allow multiple decision threads - Adjust reports to include aggregate and per decision worker numbers - Convert fapolicyd service type to watchdog based - Make fagenrules follow the generational update pattern - Fix Debian backend to work again - Add filter support for Debian backend - Add support for globbing in rules using explicit glob demarcation - Fix logging to syslog/journald to not deadlock on log rotation - Allow rules to express file/dir names with spaces using double quotes - Allow packages to append MIME types to the %languages policy set - fapolicyd-cli now has --ftype-ext emits NUL-delimited path and MIME pairs - Move the pid file into /run/fapolicyd and restrict it to the daemon - Improve privilege error messages in fapolicyd-cli 1.6 - Many internal changes - Preserve the active trust database when reloads fail - Mark dir=untrusted as deprecated in rules - Improve status, metrics, and timing report details and formatting - Improve fapolicyd-cli --check-rules diagnostics with source line locations - Add fapolicyd-cli --compact-trustdb to rebuild and compact the trust database - Expand fapolicyd-cli --check-ignore_mounts risk reporting - Change the default value of db_max_size to auto - Prompt before replacing existing decision timing reports 1.5 - Increase the line length of config files (Renaud Métrich) - Add fapolicyd-cli --check-rules to validate rules - Fix fapolicyd-cli --check-rules --lint without an explicit path - Add fapolicyd-perf-test for QE purposes - Make fapolicyd rule reload transactional - Detect and handle FAN_Q_OVERFLOW in fapolicyd - Collect metrics around decision replies that fail (Reply Errors) - Collect metrics around default-allow decisions - Document metrics in fapolicyd.state - Add --reset-metrics to fapolicayd-cli and reset_strategy to fapolicyd.conf - Add a decision timing framework to fapolicyd - Add --timing-start/--timing-stop to fapolicyd-cli - fapolicyd, remove deprecated options: --boost, --group, --queue, and --user - Add stress test harness - fapolicyd, add deferal mechanism for inbound events that evict building events - Evict traced and stale subjects if there collision while building the event - Add per-rule hit counters state report to improve observability. - Add per-attribute cache hit and lookup counters to improve observability. - Split CLI state and metrics reports 1.4.5 - Inform the admin symlinks were skipped when adding paths - Avoid large mount options in the realtime mounts code - Fix cli --update causing fatal error in the damon (#399) 1.4.4 - Fix races and shutdown hazards in trust database updates - Fix races in rules reload and mount handling - Harden LMDB error handling to avoid corruption and stale state - Fix long-path trust hashing collisions - Improve parsing and validation of trust data inputs - Add support for SHA512 hashes in integrity data parsing - Improve shell script, R, and ELF MIME detection - Fix rule matching for set-based untrusted/execdirs checks - Improve verification of deleted paths and trust-file names - Fix fapolicyd-cli --check-ignore_mounts parsing with --verbose - Stop fapolicyd-cli --file from accepting special files 1.4.3 - Fix missing allow decision in fapolicyd-cli --test-filter - Update the trust-db filter rules to add and drop some things - Add new magic detections and drop a lot of old rules - Performance improvement looking up mime-types - Various code cleanups, hardening, and performance optimizing - Classify ELF files with an unknown interpreter as application/x-bad-elf - Consolidate the fapolicyd-cli return codes - Add integrity value to state report 1.4.2 - Correct identification of kworker threads - Fix various threading, initialization, and function attribute issues - Add support for SHA512, replace SHA256HASH with FILE_HASH - Add --filter option to fapolicyd-cli --file - Update bash completions for new options - Add an early subject cache evictions counter to the state report - Add CPU cores to the state report - Move mounts update to it's own thread to prevent deadlocks with LUKS - Add db_max_size = auto support so db size is managed by the daemon - In rpm backend, drop any file from trustdb that doesn't verify size or hash 1.4.1 - Fix deadlock on reconfigure - On reconfigure, update the trust list and reload the rpm filter 1.4 - Add ignore_mounts option to fapolicyd to not watch some mount points - add --check-ignore_mounts option to fapolicyd-cli - Fix overlay filesystem detection in fapolicyd-cli --check-watch_fs - Fix fapolicyd-cli --check-path so that it properly detects symlinked dirs - In fapolicyd-cli, add --verbose flag to --check-ignore_mounts - Watch the /run partition - Update some config items in SIGHUP - Clarify warnings for fapolicyd-cli --check-trustdb - All trust source backend checks/updates performance improvement - Fix line too long error when last line of conf file had no newline 1.3.7 - Refactor queue to unify enqueue and dequeue and make it lockless - Improve text/x-shellscript detection - Fix bug in fapolicyd-cli --ftype detection which was introduced in 1.3.6 - Add watched mount points to state report - important since they can change - Warn when object cache eviction ratios are high - Use uthash for trust file backend duplication detection - Update subject identity collection to gather all uid/gid components 1.3.6 - Increase the default subject cache size - Move fapolicyd-rpm-loader to bin directory - Suppress the subject cache eviction for scripts run by interpreters - Fix decriptor leak in previous release - only leaks on rpminstall/delete - Improve performance of filter code - Drop 'device' keyword for subject part of a rule - Add documentation to fapolicyd-filter.conf for better understanding - Fix build flags for Debian to include libmd - Add --test-filter to fapolicy-cli to help test filter rules - Fix bug in the filter that was allowing unexpected files though the filter - Drop 2 more kinds of files from the rpm filter: html and md 1.3.5 - Raise default value for db_max_size - Increase buffer size for reading process groups - Pid read buffer size is defined using define rather than const - Allow override of mounts file - Fix leak and problematic memory managament - Fix creation of RUN_DIR because it breaks rpm verify - Add Microsoft Windows PE MIME magic definition - Microsoft Windows PE rules - Optimize path allocation - Revert change to report interval logic (#325) - Ensure fagenrules handles incomplete lines - Describe how to handle nss-user-lookup correctly - Fix normal pattern handling - Improve AVL test - Install gawk in test workflows - Cleanup and document filesystem code - Add AGENTS.md - Revert "Fix for rpmdb with SQLite3 backend" - Fix rpmdb locking issues by loading via separate process - Fix an infinite loop (#345) - Use memfd instead of pipe (#346) - fapolicyd-cli --check-path doesn't exit - Fix null argv when spawning fapolicyd-rpm-loader (#343) - Fix some concurrency issues - Update comments in lru_evict header - Update fd_fgets code - On exit due to poll error, try to stop and save the database - Improve update thread synchronization in fapolicyd - Modify rpm_load_list for stop checks - Fix a TODO that notes NULL should be returned - Add some error cleanup in filter_load_file - Increase buffer size when reading the PT_INTERP string in gather_elf() - Add debug logs for STATE_LD_SO decisions - Remove volatile qualifier from atomic types - Add MEM_MMAP_FILE storage type for fd_fgets - Fix segfault when writing to readonly memory - Switch rpm-backend to mmap based list parsing - Add memory statistics report - To allow sealing, need to create with MFD_ALLOW_SEALING - Ease SHA strings allocated on each iteration - Skip to add non regular files to trustdb (#333) - Fix segfault when socket is inside of the directory (#355) - Subject cache eviction warning - No eviction warnings on shutdown 1.3.4 - Fix race on fanotify fd on termination - Improve efficiency loading the rpm database into trust db - Fix issue where lock from rpmdb(Sqlite3 backend) is dropped when it shouldn't 1.3.3 - Improve dpkg support (Stephen Tridgell) - Fix issue when no initial mount points (wjhunter3) - Add RuntimeDirectory to the systemd service file - Update code to limit the number group id's logged - Improve mount point detection code - Double the amount of groups a user could have (32) - Small performance improvement by not double rewinding descriptor - Improve logging: make stderr output more colourful; add timestamps (Kangie) - Identify ruleset being loaded by a sha hash of the rule file (John Wass) - Add 22-buildroot.rules to use if the machine builds software - Add --with-asan for configure 1.3.2 - Remove LimitNOFILE and instead setrlimit more carefully - Sync q_size to the documentation - Fix multiple memory leaks 1.3.1 - Fix not complete patch for filter file renaming 1.3 - Be consistent in updating and removing file system marks - Add escaping to /proc/mount entries - Revise escaping of trust files - Add LimitNOFILE to the service file - Add dpkg support (Stephen Tridgell) - Add support for runtime reloading of rules 1.2 - On shutdown when running reports, if trust db empty warn (Nobuhiro Iwamatsu) - Extend state machine to skip opens after exec until dyn linker found - Control filtering of unwanted files in rpm backend with config file - Add support for logging rule number of decision in the audit event 1.1.7 - Re-add dropped FAN_MARK_MOUNT for monitoring events (Steven Brzozowski) - Make some updates to allow running without an rpm back successful 1.1.6 - Correct the optional inclusion of code based on HAVE_DECL_FAN_MARK_FILESYSTEM 1.1.5 - If in debug mode, do not write audit events to audit system - Update filesystems we dont care about - Add --check-path to fapolicyd-cli to locate missed files - Detect trusted static apps running programs by ld.so - Add support for using FAN_MARK_FILESYSTEM to see bind mounted accesses 1.1.4 - Fix descriptor leak on enqueue failure (Steven Brzozowski) - Switch SHA256 hashing to openssl - Add --check-status to fapolicyd-cli - If fapolicyd is already running, exit - Do trust db size check on all fapolicyd updates - Add bash completions 1.1.3 - Replace snprintf integer to char conversion with uitoa function - Update the locking between the main and decision threads - Speedup sha256 hashing by mmap'ing the object - Add OOMScoreAdjust to fapolicyd.service 1.1.2 - Release the update lock if starting trust db read operations errors - CVE-2022-1117 fapolicyd incorrectly detects the run time linker - Add the btrfs to the watch_fs config option - Fix a problem tracking trusted static apps that launch other apps 1.1.1 - Reorder patterns and loopholes in rule.d - Add support for subject ppid rule matching - Add support for reloading the trust database from SIGHUP 1.1 - Add support for a rules.d directory - Add --check-config, --check-watch_fs, and --check-trustdb to fapolicyd-cli - Add libgcrypt initialization - Break up all the rules so they can be installed in rules.d - Add text/x-nftables magic - Add interpreter for s390x, ppc64le 1.0.4 - Tighten up ELF detection - Add support for multiple trust files in a trust.d directory - Add troubleshooting info for when the trust db is full - In permissive mode, allow audit events when rules say to log it - Add new rpm_sha256_only config option to the daemon - Escape whitespaces in file names put into the file trust database 1.0.3 - Add startup and shutdown syslog message - fapolicyd-cli open trustdb without locking to prevent daemon hang - If db migration fails due to unlinking problem, fail startup - Do not exit on fanotify_event read failure - Add application/javascript to Language macro 1.0.2 - Add Group ID support for rules - Add test cases for avl library - Update support for multiple copies of a trusted executable - Add support for dynamic trust updating 1.0.1 - If trust db is empty when fapolicyd-cli dumps it, say its empty - Make fapolicyd-cli buffer bigger for rule listing - Fix ignored db errors from check_trust_database - Adjust ELF x-object detection - Do device mime-type detection in-house instead of libmagic - Allow arbitrarily large group statements - Fix logging of object trust - Correct denial accounting - Add new form of LD_PRELOAD pattern detection - Fix mount reading routine to get it all - Update languages kept from /usr/share 1.0 - Add file size, IMA, and sha256 based integrity checking - Add ability to send decision results to syslog - Add ability to define the format of the syslog event - Add support for sets in rules - Add support for dumping the trustdb by fapolicyd-cli - Print a warning if rpm backend doesn't have a sha256 hash - In rpm backend, add back javascript from /usr/share 0.9.4 - Fix pattern detection in light of EXEC_PERM events - Conserve memory by dropping unneeded lists after startup - Do full reset of subject credentials when execve finishes - Drop files in /usr/share, /usr/src, and /usr/include to reduce memory use - Add error checking of the trust database - Fixed threading issue during rpm update - Add option to delete the trust database to cli, --delete-db - Add option to cli to add/delete/update the file trust database 0.9.3 - In fapolicyd-cli, add a --list option to list rules - Change lmdb to use writable mmap for startup performance improvment - Change the database to support duplicate keys - Provide a magic override file and use it during file inspection - Update rules to match new magic overrides - Add --ftype command to fapolicyd-cli - Add database statistics to usage report 0.9.2 - Split codebase into daemon, library and cli - Add Admin defined trust database - Make use of librpm optional - Updated the man pages - Setting boost, queue, user, and group on the command line are deprecated 0.9.1 - Make watched filesystems configurable - Improve ELF file classification - Expose file type in debug output - Update rules for ansible and dracut - Skip config files in database check - Expand definition of doc files - Create new rule format exposing Subj and Obj trust - Redesign the rules for trust based rules 0.9 - Convert hashes to lowercase like sha256sum outputs - Use FAN_OPEN_EXEC_PERM for subject cache management - Add static pattern detection - Performance improvements - Switch from static mounts to hotplug configuration of mount points - Dont collect documentation in trust database - When path is longer than lmdb can store, use a sha512 hash (Attila Lakatos) - Cache subject trustworthiness information after lookup (Radovan Sroka) 0.8.10 - Fix segfault for rules whose subject is number oriented - When database problem is found on startup, rebuild database - Don't flush empty caches on database rebuild - Revise default settings for better performance 0.8.9 - Systemd usage updates - File permission adjustments based on selinux policy review - Fix unterminated reads of auid & sessionid values - Deprecate ld_preload pattern until new method exists 0.8.8 - Add FAN_OPEN_EXEC_PERM Support (Matthew Bobrowski) - Man page updates - Add dnf plugin to sync database when rpms install 0.8.7 - If the path has a top level symlinked dir, retry db lookup without /usr - Fix parsing of command line options (Matthew Bobrowski) - Add more validation of mount types (Matthew Bobrowski) - Elf parser updates (Matthew Bobrowski) 0.8.6 - Update object hash calculation to better determine uniqueness - Override rpm's signal handling - Use private database as trust store - Update the rules for python 3.6 and remove systemd exclusion - Rename exec_dir rule option unpackaged to untrusted - Remove unneeded rpm code - Add support for daemon config file - Allow database size to be configurable - Add permissive setting, q_size, and q_depth to usage report 0.8.5 - Update spec file and license info 0.8.4 - Mask signals from deadman's switch - Reinstate strong umask before writing report - Use pw_gid to set the group when changing gid - Allow the use of account names for auid & uid in rules - Support group option on command line 0.8.3 - Add audit support for the linux-4.15 kernel - Don't close report descriptor in report - Fix busy loop to use poll as originally intended - Relax timing on deadman's switch 0.8.2 - Add seccomp filter support - Fix leaked descriptor in exe_type processing - Add LRU cache for subject and objects - Create fapolicyd user on install - Update systemd service file to run as user fapolicyd - Adjust inter-thread queue default size - Write statistics on shutdown - Change attribute access to hash table - Deny access to stale pid's or fd's - Add new pattern subject detection - Add executable report on shutdown - Add --no-details to suppress file/exe names on shutdown report 0.8.1 - Documentation updates - Update rules - Output how many rules are loaded in debug mode - Add user commandline option 0.8 - Initial public release fapolicyd-2.0/IMA-setup-RHEL8-example.md000066400000000000000000000107251524071644000176740ustar00rootroot00000000000000# How to Set Up IMA (Integrity Measurement Architecture) ## Step-by-Step Guide ### 1. Ensure `securityfs` is mounted: ```bash mount -l | grep securityfs ``` Example output: ``` # mount -l | grep securityfs securityfs on /sys/kernel/security type securityfs (rw,nosuid,nodev,noexec,relatime) ``` ### 2. Confirm IMA kernel options are configured: ```bash grep -e IMA -e INTEGRITY /boot/config-$(uname -r) ``` Example output: ``` $ grep -e IMA -e INTEGRITY /boot/config-4.18.0-80.11.2.e18_0.x86_64 CONFIG BLK _DEV_INTEGRIT’ CONFIG _KEXEC_BZIMAGE_VERIFY_SIG=-y # CONFIG WIMAX is not set CONFIG _DM_INTEGRITY=m CONFIG _MLXSW_MINIMAL=m CONFIG_FB_CFB_IMAGEBLIT=y CONFIG_FB_SYS_IMAGEBLIT=m CONFIG_FRAMEBUFFER_CONSOLE_DETECT_PRIMARY=y CONFIG_HID_PRIMAX=m CONFIG_INTEGRITY=y CONFIG_INTEGRITY_SIGNATURE=y CONFIG INTEGRITY ASYMMETRIC KEYS=y CONFIG_INTEGRITY_TRUSTED_KEYRING=y CONFIG INTEGRITY PLATFORM KEYRING=y CONFIG_INTEGRITY_AUDIT=y CONFIG_IMA=y CONFIG_IMA_MEASURE_PCR_IDX=10 CONFIG_IMA_LSM RULES-y_ # CONFIG INA TEMPLATE is not set CONFIG_IMA_NG_TEMPLATE=y # CONFIG INA SIG TEMPLATE is not set CONFIG_IMA_DEFAULT_TEMPLATE="ima-ng” CONFIG _IMA DEFAULT HASH SHAL=y # CONFIG_IMA_DEFAULT_HASH_SHA2S6 is not set CONFIG _IMA_DEFAULT_HASH="shal" # CONFIG_IMA WRITE POLICY is not set # CONFIG_IMA READ POLICY is not set CONFIG_IMA_APPRAISE=y CONFIG_IMA_APPRAISE_BOOTPARAM=y CONFIG_IMA_TRUSTED_KEYRING=y # CONFIG IMA BLACKLIST KEYRING is not set # CONFIG IMA LOAD _XS09 is not set ``` ### 3. Ensure the file system supports `i_version`: - `ext4` has `i_version` enabled by default. - Other filesystems like XFS and ext3 require it to be explicitly enabled in `/etc/fstab`. Example `/etc/fstab` entry: ``` [shearerd@awc-devel fapolicyd]$ cat /etc/fstab /etc/fstab Created by anaconda on Wed Apr 22 11:31:17 2020 See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info After editing this file, run ‘systemctl daemon-reload' to update systemd # # # # # Accessible filesystems, by reference, are maintained under '/dev/disk/' # # # # units generated from this file. # /dev/mapper/cl_awc--devel-root / ext4 defaults, iversion ai UUID=f26b7db8 -4935-4507-9722-79ad9eb5c55b /boot ext4 defaults 12 /dev/mapper/cl_awc--devel-swap swap swap defaults 00 ``` Check if `i_version` is enabled: ```bash mount -l | grep version ``` Example output: ``` [shearerd@awc-devel fapolicyd]$ mount -l | grep version /dev/mapper/cl_awc--devel-root on / type ext4 (rw,relatime,seclabel,i version) [shearerd@awc-devel fapolicyd]$ | ``` ### 4. Enable IMA in appraise fix mode: **a.** Backup the grub config: ```bash cp /etc/default/grub /etc/default/grub.orig ``` **b.** Edit `/etc/default/grub` to include IMA settings: ``` 3_TIMEOUT=5 GRUB_DISTRIBUTOR="5 (sed GRUB_DEFAULT=2aved GRUB_DISABLE_SUBMENU-true GRUB_TERMINAL_OUTPUT="console” GRUB_CMDLINE_EINUX="crashkernel=auto ima_policy=tcb ima_appraise_tcb ima_appraise=fix ima_hash=sha256 ima_audit=1 resume=/dev/mapper/cl-swap rd.ivm.lv=cl/root rd.ivm.iv=cl/swap rhgb quiet” GRUB_DISABLE_RECOVERY="true" GRUB_ENABLE_BLSCFG=true release .*5,,g" /etc/system-release)” ``` **c.** Rebuild grub: - BIOS-based machines: ```bash grub2-mkconfig -o /boot/grub2/grub.cfg ``` - UEFI-based machines: ```bash grub2-mkconfig -o /boot/efi/EFI/redhat/grub.cfg ``` **d.** Reboot machine: ```bash reboot ``` **e.** Confirm that the IMA directory structure is present: ```bash ls /sys/kernel/security/ima ``` **f.** Label files: ```bash find / -fstype ext4 -type f -uid 0 -exec dd if='{}' of=/dev/null count=0 status=none \; ``` **g.** [OPTIONAL] View measurements: ```bash tail -f /sys/kernel/security/ima/ascii_runtime_measurements ``` ### 5. View contents of security labels: ```bash # getfattr -m ^security --dump -e hex /bin/bash getfattr: Removing leading '/' from absolute path names # file: bin/bash security.ima=0x040420557151302622baSc281893436a62164538C77bd43452267fa2da6c3cf23ed8 security.selinux=0x73797374656d5£753a6£626a6563745£723a7368656CEcS£E657865635£743a733000 ``` ### 6. Install fapolicyd ```bash # dnf install fapolicyd ``` ### 7. Set IMA in fapolicyd.conf ```bash # vim /etc/fapolicyd/fapolicyd.conf ... integrity = ima ... ``` ### 7. Start fapolicyd ```bash # fapolicyd --debug-deny ``` If no errors, you are good to go. If following appears hashes are not present in extended attributes. ```bash 10/28/24 06:13:21 [ ERROR ]: IMA integrity checking selected, but the extended attributes can't be read 10/28/24 06:13:21 [ ERROR ]: Exiting due to bad configuration ``` fapolicyd-2.0/INSTALL.tmp000066400000000000000000000001411524071644000151540ustar00rootroot00000000000000Installation Instructions ************************* See README.md for build, install, testing. fapolicyd-2.0/Makefile.am000066400000000000000000000010541524071644000153640ustar00rootroot00000000000000 # src/tests/file_type_detect_test loads the generated init/fapolicyd-magic.mgc. SUBDIRS = init src doc rules.d TEST_FIXTURES = \ src/tests/fixtures/filter-minimal.conf \ src/tests/fixtures/filter-cases.txt \ src/tests/fixtures/broken-filter.conf \ init/fapolicyd-filter.conf \ src/tests/fixtures/rules-valid.rules EXTRA_DIST = ChangeLog AUTHORS NEWS README.md INSTALL fapolicyd.spec \ dnf/fapolicyd-dnf-plugin.py autogen.sh \ $(TEST_FIXTURES) dist_check_DATA = $(TEST_FIXTURES) clean-generic: rm -rf autom4te*.cache rm -f *.rej *.orig *.lang *.list fapolicyd-2.0/NEWS000066400000000000000000000000001524071644000140150ustar00rootroot00000000000000fapolicyd-2.0/README.md000066400000000000000000001136351524071644000146200ustar00rootroot00000000000000File Access Policy Daemon ========================= [![Build Status](https://travis-ci.com/linux-application-whitelisting/fapolicyd.svg?branch=master)](https://travis-ci.com/linux-application-whitelisting/fapolicyd) This is a simple application whitelisting daemon for Linux. RUNTIME DEPENDENCIES -------------------- * kernel >= 4.20 (Must support FANOTIFY_OPEN_EXEC_PERM. See [1] below.) BUILDING -------- See [BUILD.md](./BUILD.md) for build-time dependencies and instructions for building. DEPRECATION NOTICES ------------------- ### dir=untrusted rules The `dir=untrusted` rule macro is deprecated and is planned for removal in the 2.1 release. Existing rules continue to parse for compatibility, but they emit a deprecation warning. New policy should match trust explicitly. Use object trust rules for the file being controlled instead of relying on the legacy `dir=untrusted` path exception. POLICIES -------- The current design for policy is that it is split up into units of rules that are designed to work together. They are copied into /etc/fapolicyd/rules.d/ When the service starts, the systemd service file runs fagenrules which assembles the units of rules into a comprehensive policy. The policy is evaluated from top to bottom with the first match winning. You can see the assembled policy by running ``` fapolicyd-cli --list ``` Originally, there were 2 policies shipped, known-libs and restrictive. The restrictive policy was designed with these goals in mind: 1. No bypass of security by executing programs via ld.so. 2. Anything requesting execution must be trusted. 3. Elf binaries, python, and shell scripts are enabled for trusted applications/libraries. 4. Other languages are not allowed or must be enabled. It can be recreated by copying the following policy units into rules.d. The optional ones are not included unless they are needed: 20-dracut.rules 21-updaters.rules 30-patterns.rules 40-bad-elf.rules 41-shared-obj.rules 43-known-elf.rules 71-known-python.rules 72-shell.rules optional: 73-known-perl.rules optional: 74-known-ocaml.rules optional: 75-known-php.rules optional: 76-known-ruby.rules optional: 77-known-lua.rules 90-deny-execute.rules 95-allow-open.rules The known-libs policy (default) was designed with these goals in mind: 1. No bypass of security by executing programs via ld.so. 2. Anything requesting execution must be trusted. 3. Any library or interpreted application or module must be trusted. 4. Everything else is not allowed. It can be created by copying the following policy units into rules.d: 10-languages.rules 20-dracut.rules 21-updaters.rules 30-patterns.rules 40-bad-elf.rules 41-shared-obj.rules 42-trusted-elf.rules 70-trusted-lang.rules 72-shell.rules 90-deny-execute.rules 95-allow-open.rules EXPERIMENTING ------------- You can test by starting the daemon from the command line. Before starting the daemon, cp /usr/bin/ls /usr/bin/my-ls just to setup for testing. When testing new policy, its highly recommended to use the permissive mode to make sure nothing bad happens. It really is not too hard to deadlock your system. Continuing on with the tutorial, as root start the daemon as follows: ``` /usr/sbin/fapolicyd --permissive --debug ``` Then in another window do the following: 1. /usr/lib64/ld-2.29.so /usr/bin/ls 2. my-ls 3. run a python file in your home directory. 4. run a program from /tmp In permissive + debug mode you will see dec=deny which means "decision is to deny". But the program will actually be allowed to run. You can run the daemon from the command line with --debug-deny command line option. This culls the event notification to only print the denials. If this is running cleanly, then you can remove the --permissive option and get true denials. Now retest above steps and see the difference. DEBUG MODE ---------- In debug mode, you will see events such as this: ``` rule:9 dec=deny_audit perm=execute auid=1001 pid=14137 exe=/usr/bin/bash : file=/home/joe/my-ls ftype=application/x-executable ``` What this is saying is rule 9 made the ultimate Decision that was followed. The Decision is to deny access and create an audit event. The subject is the user that logged in as user id 1001. The subject's process id that is trying to perform an action is 14137. The current executable that the subject is using is bash. Bash wanted permission to execute /home/joe/my-ls which is the object. And the object is an ELF executable. Sometimes you want to list out the rules to see what rule 9 might be. You can easily do that by running: ``` fapolicyd-cli --list ``` Also, in fapolicyd.conf, there is a configuration option, syslog_format, which can be modified to output information the way you want to see it. So, if you think auid in uninteresting you can delete it. If you want to see the device information for the file being accessed, you can add it. You can also enable this information to go to syslog by changing the rules to not say audit, but instead have syslog or log appended to the allow or deny decision. OVERRIDE MOUNTS WHILE DEBUGGING ------------------------------- When debugging you can specify an alternative mounts file to the deamon to watch for event notifications. This allows for finer level of control than is achievable by filtering by filesystem type. The alternative mounts file will expect the same format as `/proc/mounts`, which allows us to select entries from `/proc/mounts` into a new file which fapolicyd will use as the mount source. For example, use grep to select a single mount point: ``` mount -t tmpfs tmpfs /tmp/my-test-dir grep my-test-dir /proc/mounts > /tmp/my-test-mounts fapolicyd --debug --mounts=/tmp/my-test-mounts ``` Here we mount a tmpfs for testing in `/tmp`, and grep it from `/proc/mounts` into the overriding mounts file, then run fapolicyd in debug mode while specifying the override file. The result is fapolicyd only receives events that occur in `/tmp/my-test-dir`. WRITING RULES ------------- The authoritative source is the fapolicyd.rules man page. It is suggested that people use the known-libs set of rules. This set of rules is designed to allow anything that is trusted to execute. It's design is to stay out of your way as much as possible. All that you need to do is add unpackaged but trusted files to the trust database. See the "Managing Trust" section for more. But if you had to write rules, they follow a simple "decision permission subject : object" recipe. The idea is to write a couple things that you want to allow, and then deny everything else. For example, this is how shared libraries are handled: ``` allow perm=open all : ftype=application/x-sharedlib trust=1 deny_audit perm=open all : ftype=application/x-sharedlib ``` What this says is let any program open shared libraries if the library being opened is trusted. Otherwise, deny with an audit event any open of untrusted libraries. First and foremost, fapolicyd rules are based on trust relationships. It is not meant to be an access control system of Mandatory Access Control Policy. But you can do that. It is not recommended to do this except when necessary. Every rule that is added has to potentially be evaluated - which delays the decision. If you needed to allow admins access to ping, but deny it to everyone else, you could do that with the following rules: ``` allow perm=any gid=wheel : trust=1 path=/usr/bin/ping deny_audit perm=execute all : trust=1 path=/usr/bin/ping ``` You can similarly do this for trusted users that have to execute things in the home dir. You can create a trusted_user group, add them the group, and then write a rule allowing them to execute from their home dir. When you want to use user or group name (as a string). You have to guarantee that these names were correctly resolved. In case of systemd, you need to add a new after target 'After=nss-user-lookup.target'. To achieve that you can use `systemctl edit --full fapolicyd`, uncomment the respective line and save the change. ``` allow perm=any gid=trusted_user : ftype=%languages dir=/home deny_audit perm=any all : ftype=%languages dir=/home ``` One thing to point out, if you have lists of things that you would like to allow, use the macro set support as illustrated in this last example. This puts the list into a sorted AVL tree so that searching is cut to a minimum number of compares. The `exe` and `path` rule attributes also accept `fnmatch` glob patterns for paths with a necessary variable component. Prefix each pattern with `glob:`, as in `glob:/opt/vendor/product-*/bin/tool`, to distinguish it from an exact path. Unprefixed values remain exact even when their file names contain `*`, `?`, or `[` characters. Wildcards do not cross `/` or implicitly match a leading period, and `**` recursive matching is not supported. Continue to use `dir` for a complete subtree under a fixed directory and exact values or sets when the paths are known. The `glob:` prefix is a rule error on `dir` and all attributes other than `exe` and `path`; `fapolicyd-cli --check-rules` reports these errors without expanding valid patterns. A glob selects matching paths but does not trust them. In particular, it is not advisable to allow execution from a user-writable path solely because its name matches a glob. It is strongly suggested to pair an object `path` allow with object `trust=1` or an explicit `FILE_HASH`; pair a subject `exe` glob in a user-writable location with subject `trust=1`. The trust database continues to contain concrete paths; see the `fapolicyd.rules(5)` man page for the complete globbing contract. One last note, the rule engine is a first match wins system. If you are adding rules to allow something but it gets denied by a rule higher up, then move your rule above the thing that denies it. But again, if you are writing rules to allow execution, you should reconsider if adding the programs to the trust database is better. RULE ORDERING ------------- Starting with 1.1, the rules should be placed in a rules.d directory under the fapolicyd configuration directory. There is a new utility, fagenrules, which will compile the rules into a single file, compiled.rules, and place the resulting file in the main config directory. Starting with 2.0, fagenrules validates the generated candidate with the daemon policy parser before installation. It syncs and atomically renames a validated candidate into `compiled.rules`, leaving the installed file in place until the replacement is ready. It prints a SHA-256 ruleset identity when the rules are unchanged or successfully updated. Use `fagenrules --check` to determine whether the assembled policy needs an update without replacing `compiled.rules`. If you want to migrate your existing rules, just move them as is to the rules.d directory. You cannot have both compiled.rules and fapolicyd.rules. The fagenrules will notice this and put a warning in syslog. If you use fapolicyd-cli --list, it will also notice and warn. If you do have both files, the old rules file will be used instead of the new one. This new rules.d directory is intended to make it easier to develop application specific rules that can be dropped off by automation / orchestration. This should make managing the configuration easier. The files in the rules.d directory are processed in a specific order. See the [rules.d README](rules.d/README-rules) file for more information. Rules can be validated before they are installed or reloaded by running: ``` fapolicyd-cli --check-rules [path] ``` If no path is given, the active rules file is checked. The `--lint` option adds policy-shape warnings for rules that may unintentionally allow executable or programmatic content to reach the default allow decision. Runtime rule reloads are transactional: a candidate ruleset is parsed and validated before it replaces the active policy, so a failed reload preserves the previous published ruleset. REPORT ------ On shutdown the daemon will write an object access report to /var/log/fapolicyd-access.log. The report is from oldest access to newest. Timestamps are not included because that would be a severe performance hit. The report gives some basic forensic information about what was being accessed. PERFORMANCE ----------- When a program opens a file or calls execve, that thread has to wait for fapolicyd to make a decision. To make a decision, fapolicyd has to lookup information about the process and the file being accessed. Each system call fapolicyd has to make slows down the system. To speed things up, fapolicyd caches everything it looks up so that subsequent access uses the cache rather than looking things up from scratch. But the cache is only so big. You are in control of it, though. You can make both subject and object caches bigger. When the program ends, it will output some performance statistic like this into /var/log/fapolicyd-access.log or the screen: ``` Permissive: false q_size: 640 Inter-thread max queue depth 7 Allowed accesses: 70397 Denied accesses: 4 Trust database max pages: 14848 Trust database pages in use: 10792 (72%) Subject cache size: 1549 Subject slots in use: 369 (23%) Subject hits: 70032 Subject misses: 455 Subject evictions: 86 (0%) Object cache size: 8191 Object slots in use: 6936 (84%) Object hits: 63465 Object misses: 17964 Object evictions: 11028 (17%) ``` In this report, you can see that the internal request queue maxed out at 7. This means that the daemon had at most 7 threads/processes waiting. This shows that it got a little backed up but was handling requests pretty quick. If this number were big, like more than 200, then increasing the q_size may be necessary. Another statistic worth looking at is the hits to evictions ratio. When a request has nowhere to put information, it has to evict something to make room. This is done by a LRU cache which naturally determines what's not getting used and makes it's memory available for re-use. In the above statistics, the subject hit ratio was 95%. The object cache was not quite as lucky. For it, we get a hit ration of 79%. This is still good, but could be better. This would suggest that for the workload on that system, the cache could be a little bigger. If the number used for the cache size is a prime number, you will get less cache churn due to collisions than if it had a common denominator. Some primes you might consider for cache size are: 2039, 4099, 6143, 8191, 10243, 12281, 16381, 20483, 24571, 28669, 32687, 40961, 49157, 57347, 65353, etc. ## Decision worker tuning Starting with 2.0, `decision_threads` controls the number of decision workers. The default is `1`. Each worker has its own event queue, subject cache, object cache, and subject defer array, so `q_size`, `subj_cache_size`, and `obj_cache_size` are per-worker values. This setting requires a daemon restart. Increase the worker count only after observing persistent queue pressure under a representative workload: a high inter-thread queue depth relative to `q_size`, a non-zero queue-full count, or a growing oldest queued age. Start with two workers and compare the per-worker queue, cache, and defer data before making another change. A busy single PID remains on one stable worker, so one hot worker and idle peers can indicate workload skew rather than a need to continually increase the count. For day to day monitoring, `fapolicyd-cli --check-status` reports daemon health and configuration state, while `fapolicyd-cli --check-metrics` reports runtime counters. The status report includes health indicators such as kernel queue overflows, filesystem health events, reply errors, and deferred-subject pressure. The metrics report includes rule hit counts, default-allow fallthrough decisions, cache effectiveness, and subject/object attribute lookup activity. Starting with 1.6, status and metrics reports also identify the active configuration, ruleset, trust database, and LMDB environment generations. This helps correlate a report with the live state that was active after a reload, trust database rebuild, or controlled compaction. State and metrics reports can be scheduled periodically by setting the configuration option `report_interval`. This option is set to `0` by default, which disables the reporting interval. A positive value for this option specifies the number of seconds to wait between reports. Runtime metric counters can be reset with `fapolicyd-cli --reset-metrics` when the daemon is configured with `reset_strategy=manual`. Rule hit counters naturally reset when a new ruleset is loaded, and `--reset-metrics` also clears them after reporting so the current rules can be tested from a fresh counter window. For bounded latency investigations, set `timing_collection=manual` and use `fapolicyd-cli --timing-start` followed by `fapolicyd-cli --timing-stop`. This produces a decision timing report with aggregate queue, decision, and helper latency information without storing one timing record per decision. Also, it should be mentioned that the more rules in the policy, the more rules it will have to iterate over to make a decision. As for the system performance impact, this is very workload dependent. For a typical desktop scenario, you won't notice it's running. A system that opens lots of random files for short periods of time will have more impact. Another configuration option that can affect performance is the integrity setting. If this is set to sha256, then every miss in the object cache will cause a hash to be calculated on the file being accessed. One trade-off would be to use size checking rather than sha256. This is not as secure, but it is an option if performance is problematic. ## Use ignore_mounts with great care Starting with fapolicyd-1.3.8, there is a new performance option, ignore_mounts. ignore_mounts removes selected mount points from fanotify monitoring to reduce overhead on very busy filesystems (for example, cache or logging partitions). This improves performance **at the cost of visibility**: when a mount is ignored, fapolicyd does not see opens/reads from that tree and cannot apply policy decisions there. ### When to consider it + High-churn **data-only** mounts where monitoring provides little value (e.g. cache directories, file/content serving directories, or dedicated logging partitions). + Workloads that are **well understood and controlled**, with correct ownership/permissions/SELinux labels and no expectation that content will be treated as code. ### Risks + **Interpreter and plugin gaps**: Even with noexec, trusted interpreters (shell, Python, Java, Node.js, etc.) and applications that load plugins/bytecode may read and act on files from the ignored mount. Those accesses bypass fapolicyd because the mount is not watched. + **Policy blind spots**: Content copied into the ignored tree won’t be evaluated while it resides there and may only be detected after it moves to a monitored location. + **Coverage assumptions**: The root filesystem / is always monitored. Do not use ignore_mounts to work around denials for native ELF binaries; that is not its purpose. ### Required guardrails + Each ignored mount **must** be mounted with noexec. This prevents direct ELF execve() from that mount. If noexec is missing, / is still monitored and the first observed event will be the runtime linker which will trigger the ld_so pattern detection which will deny access. Due to this certain denial, fapolicyd refuses to ignore mount points not mounted with noexec. Mounting with noexec does not mitigate interpreter/JIT/plugin scenarios. + **Advisory pre-check** before changing configuration: ``` fapolicyd-cli --check-ignore_mounts[=MOUNT] [--verbose] ``` This verifies the mount exists, confirms noexec, scans for executable regular files, ELF/shared objects, archives/JARs/ZIPs, bytecode caches, plugin/runtime directories, and files matching the %languages macro. It reports counts by risk category, and `--verbose` lists each matching file or directory with its categories so automation can gate configuration changes. + Add entries exactly as shown in the second column of /proc/mounts. Whitespace around comma-separated entries is ignored. ### Conflicts and notes + / is always monitored. + Do not combine this option with allow_filesystem_mark=1; the daemon will refuse the configuration. MEMORY USAGE ------------ Fapolicyd uses LMDB as its trust database. LMDB is fast because it uses the kernel virtual memory system to map the database. The `db_max_size` configuration option controls the maximum LMDB map size. Starting with the 1.6 release, the shipped default is: ``` db_max_size = auto ``` Users with an older numeric `db_max_size` setting should migrate to `db_max_size = auto` with this release unless they have a specific operational requirement for a fixed manual limit. Auto sizing starts from the historical 100 MiB baseline and lets the daemon grow or shrink the LMDB map as the trust database and package set change. Auto sizing is also aware of reload headroom. A trust database reload builds a candidate generation while the current generation remains available to active readers. That means safe sizing needs room for the active trust database, the candidate generation, and LMDB copy-on-write metadata. Use the status report to inspect the current trust database sizing state: ``` fapolicyd-cli --check-status ``` The most useful fields are: ``` Trust database max pages Trust database pages in use Trust database allocated high-water pages Retired trust database generations Trust database resize recommended Trust database resize target Trust database compaction recommended Trust database compaction target ``` `Trust database pages in use` is the active trust database footprint. `Trust database allocated high-water pages` is LMDB's physical allocation high-water mark. The high-water value can be larger than active use after reload churn. Manual numeric values are not changed by the daemon. If a system keeps a manual `db_max_size` and the status report says resize is recommended, either set `db_max_size` to at least the reported target or migrate the system to: ``` db_max_size = auto ``` If the status report says compaction is recommended, use the controlled maintenance command: ``` sudo fapolicyd-cli --compact-trustdb ``` This rebuilds and validates a replacement LMDB environment, then swaps it into place during a controlled handoff. Normal package and trust file updates should still use `fapolicyd-cli --update`. Starting with the 0.9.4 release, the rpm backend filters most files in the /usr/share directory. It keeps anything with a with a python extension or a libexec directory. It also keeps /usr/src/kernel so that Akmod can still build drivers on a kernel update. TROUBLESHOOTING --------------- Whatever you do, DO NOT TRY TO ATTACH WITH PTRACE. Ptrace attachment sends a SIGSTOP which cannot be blocked. Since your whole system depends on fapolicyd approving access to glibc and various critical libraries, that will not happen until SIGCONT is sent. The system can deadlock if the continue signal is not sent. Using gdb will have the same results. With that in mind, let's talk about troubleshooting steps... If you are using deny_audit and you are not getting any audit events, the fix is to add 1 audit rule. It can be a rule about anything. Watches tend to be the highest performance, so maybe just add a watch for writes to etc shadow and restart the audit daemon so the rule gets loaded. ``` -w /etc/shadow -p w ``` When fapolicyd blocks something, it will generate an audit event if the Decision is deny_audit and it has been compiled with the auditing option. The audit system must have at least 1 audit rule loaded to create the full FANOTIFY event. It doesn't matter what rule. To see if you have any denials, you can run: ``` ausearch --start today -m fanotify --raw | aureport --file --summary File Summary Report =========================== total file =========================== 16 /sbin/ldconfig 1 /home/joe/./my-ls ``` You can also see which executables are involved like this: ``` ausearch --start today -m fanotify -f /sbin/ldconfig --raw | aureport -x --summary Executable Summary Report ================================= total file ================================= 16 /usr/bin/python3.7 ``` However, you probably want to know the rule that is blocking it. Unfortunately the audit system cannot tell you this unless you are using the 6.3 kernel or later. What you can do is change the decisions to deny_log. This will write the event to syslog as well as the audit log. In syslog, you will have the same output as the debug mode. The shipped rules expect that everything installed is in the trust database. If you have installed anything by unzipping it or untarring it, then you need to add the executables, libraries, and modules to the trust database. See the MANAGING THE FILE TRUST SOURCE section for instructions on how to do this. You can ask fapolicyd to include the trust information by adding trust to the end of the syslog_format configuration option. The things that you need to know to debug the policy is: * The rule triggering * The executable accessing the file * The object file type * The trust value Look at the rule that triggered and see if it makes sense that it triggered. If the rule is a catch all denial, then check if the file is in the trust db. To see the rule that is being triggered, either reproduce the problem with the daemon running in debug-deny mode or change the rules from deny_audit to deny_syslog. If you choose this method, the denials will go into syslog. To see them run: ``` journalctl -b -u fapolicyd.service ``` to list out any events since boot by the fapolicyd service. On systemd builds with watchdog support, the installed service uses `Type=notify`, `WatchdogSec=10s`, and `Restart=on-failure`. The health monitor refreshes the watchdog while decision workers make progress. Adjust the watchdog with a systemd drop-in rather than editing the packaged unit; see [the systemd watchdog documentation](doc/systemd-watchdog.md). Starting with 1.1, fapolicyd-cli includes diagnostic and maintenance capabilities. | Option | Added in | What it does | |------------------------|----------|--------------| | --check-config | 1.1 | Parse fapolicyd.conf for syntax errors. | | --check-trustdb | 1.1 | Check the trustdb against files on disk for mismatches that can cause run time problems. | | --check-watch_fs | 1.1 | Compare mounted file systems with the watch_fs daemon configuration. | | --check-status | 1.1.4 | Output daemon health and configuration state. Starting in 1.6, active generations and trust DB sizing guidance. | | --check-path | 1.1.5 | Check that every file in $PATH is in the trustdb. | | --test-filter | 1.3.6 | Test a path against filter rules to see whether it will be trusted. | | --check-ignore_mounts | 1.4 | Check ignored mounts for noexec and suspicious files. | | --check-metrics | 1.5 | Output runtime counters, rule hits, cache effectiveness, and attribute lookup metrics. Starting in 1.6, generation context. | | --check-rules [path] | 1.5 | Validate rule syntax without loading the rules; use --lint for policy-shape warnings. | | --reset-metrics | 1.5 | Output metrics and request a counter reset when the daemon configuration allows it. | | --timing-start | 1.5 | Start a manual decision timing collection run. | | --timing-stop | 1.5 | Stop manual decision timing collection and output the timing report. | | --timer-start | 1.5 | Alias for --timing-start. | | --timer-stop | 1.5 | Alias for --timing-stop. | | --compact-trustdb | 1.6 | Rebuild and compact the daemon trustdb LMDB environment. | | --check-status and --check-metrics | 2.0 | Include aggregate and per-decision-worker health, queue, cache, and defer data. | MANAGING TRUST -------------- Fapolicyd use lmdb as a backend database for its trusted software list. You can find this database in /var/lib/fapolicyd/. This list gets updated whenever packages are installed by dnf or rpm by a rpm plugin. The files that go into the trust database from rpm go through a filter to eliminate as many unimportant files as possible so that the trust database is concise. To see what kinds of files are in the trust database, you can try this: ``` fapolicyd-cli -D | awk '{print $2}' | awk -F/ '{ base=$NF ext="*" if (base !~ /^\./ && base ~ /\./) { n=split(base,a,"."); ext="*."a[n] } path="" for(i=1;i "/sys/kernel/security/ipe/Ex Policy/active" ``` and so on. Since they can all be disabled, the fact that an admin can issue a service stop command is not a unique weakness. 6) How do you prevent race conditions on startup? Can something execute before the daemon takes control? One of the design goals is to take control before users can login. Users are the main problem being addressed. They can pip install apps to the home dir or do other things an admin may wish to prevent. Only root can install things that run before login. And again, root can change the rules or turn off the daemon. Another design goal is to prevent malicious apps from running. Suppose someone guesses your password and they login to your account. Perhaps they wish to ransomware your home dir. The app they try to run is not known to the system and will be stopped. Or suppose there is an exploitable service on your system. The attacker is lucky enough to pop a shell. Now they want to download privilege escalation tools or perhaps an LD_PRELOAD key logger. Since neither of these are in the trust database, they won't be allowed to run. This is really about stopping escalation or exploitation before the attacker can gain any advantage to install root kits. If we can do that, UEFI secure boot can make sure no other problems exist during boot. Wrt to the second question being asked, fapolicyd starts very early in the boot process and startup is very fast. It's running well before other login daemons. NOTES ----- * It's highly recommended to run in permissive mode while you are testing the daemon's policy. * Stracing the fapolicyd daemon WILL DEADLOCK THE SYSTEM. * About shell script restrictions...there's not much difference between running a script or someone typing things in by hand. The aim at this point is to check that any program it calls meets the policy. * Some interpreters do not immediately read all lines of input. Rather, they read content as needed until they get to end of file. This means that if they do stuff like networking or sleeping or anything that takes time, someone with the privileges to modify the file can add to it after the file's integrity has been checked. This is not unique to fapolicyd, it's simply how things work. Make sure that trusted file permissions are not excessive so that no unexpected file content modifications can occur. * If for some reason rpm database errors are detected, you may need to do the following: ``` 1. db_verify /var/lib/rpm/Packages if OK, then 2. rm -f /var/lib/rpm/__db* 3. rpm --rebuilddb ``` [1] - https://git.kernel.org/pub/scm/linux/kernel/git/jack/linux-fs.git/commit/?id=66917a3130f218dcef9eeab4fd11a71cd00cd7c9 fapolicyd-2.0/RELEASE_PROCESS.md000066400000000000000000000027051524071644000161340ustar00rootroot00000000000000# fapolicyd Release Process 1. **Clean the repository** ```bash git clean -xfd ``` 2. **Bootstrap and build** ```bash ./autogen.sh ./configure --with-audit --with-rpm --disable-shared make ``` 3. **Run the test suite** ```bash make check ``` 4. **Build with Address Sanitizer** Reconfigure with `--with-asan`, rebuild, and run both the daemon and command-line client to ensure there are no ASAN failures. ```bash ./configure --with-audit --with-rpm --disable-shared --with-asan make sudo ./src/fapolicyd --debug-deny sudo ./src/fapolicyd-cli --dump-db ``` 5. **Update version numbers** - `configure.ac` line 2 - `fapolicyd.spec` line 12 - Document the changes in `ChangeLog`. 6. **Create the source tarball** ```bash ./autogen.sh ./configure --with-audit --with-rpm --disable-shared make dist ``` 7. **Tag the release** ```bash git tag -s -m "fapolicyd-X.Y.Z" vX.Y.Z git push origin vX.Y.Z ``` 8. **Sign the tarball** ```bash sha256sum fapolicyd-X.Y.Z.tar.gz > fapolicyd-X.Y.Z.tar.gz.sum gpg --armor --detach-sign fapolicyd-X.Y.Z.tar.gz gpg --clearsign fapolicyd-X.Y.Z.tar.gz.sum ``` 9. **Publish on GitHub** Create a new release with the tag, include notes from `ChangeLog`, and upload the following files: - `fapolicyd-X.Y.Z.tar.gz` - `fapolicyd-X.Y.Z.tar.gz.asc` - `fapolicyd-X.Y.Z.tar.gz.sum` - `fapolicyd-X.Y.Z.tar.gz.sum.asc` fapolicyd-2.0/TODO000066400000000000000000000003541524071644000140220ustar00rootroot00000000000000Userspace ========= * Don't overwrite existing timer reports if the timer is not armed * Improve reconfigure via SIGHUP to update configuration * Consider adding rule testing to cli (uid, pgm, file) * Support other packaging manifests fapolicyd-2.0/autogen.sh000077500000000000000000000002141524071644000153260ustar00rootroot00000000000000#! /bin/sh set -x -e # --no-recursive is available only in recent autoconf versions autoreconf -fv --install cp INSTALL.tmp INSTALL || true fapolicyd-2.0/configure.ac000066400000000000000000000216021524071644000156170ustar00rootroot00000000000000AC_REVISION($Revision: 1.3 $)dnl AC_INIT([fapolicyd],[2.0]) AC_PREREQ([2.60])dnl AC_CONFIG_HEADERS([config.h]) AC_CONFIG_MACRO_DIR([m4]) AC_USE_SYSTEM_EXTENSIONS AC_CANONICAL_TARGET AM_INIT_AUTOMAKE(foreign subdir-objects) LT_INIT AC_SUBST(LIBTOOL_DEPS) AC_MSG_CHECKING([whether build host is RHEL 8]) fapolicyd_rhel8=no if test -r /etc/os-release; then fapolicyd_rhel8=`( . /etc/os-release if test "x$ID" = xrhel; then case "$VERSION_ID" in 8|8.*) echo yes ;; *) echo no ;; esac else echo no fi )` fi AC_MSG_RESULT([$fapolicyd_rhel8]) if test "x$fapolicyd_rhel8" = xyes; then AC_DEFINE([FAPOLICYD_RHEL8], [1], [Define if the build host is Red Hat Enterprise Linux 8]) fi echo . echo Checking for programs AC_PROG_CC AM_PROG_CC_C_O AC_PROG_INSTALL AC_PROG_AWK PKG_PROG_PKG_CONFIG AC_CHECK_PROG([FILE_COMM], "file", "yes", "no") if test "$FILE_COMM" = "no"; then AC_MSG_ERROR([Unable to find the file program need to build magic databases]) fi AC_CHECK_MEMBER([struct fanotify_response_info_audit_rule.rule_number], [perm=yes], [perm=no], [[#include ]]) if test $perm = "yes"; then AC_DEFINE(FAN_AUDIT_RULE_NUM, 1,[Define if kernel supports audit rule numbers]) fi echo . echo Checking compiler options AC_C_CONST AC_C_INLINE withval="" AC_ARG_WITH(debug, AS_HELP_STRING([--with-debug],[turn on debugging (default=no)]), AC_DEFINE(DEBUG,1,[Define if you want to enable runtime debug checking.]), []) AC_MSG_CHECKING(__attr_access support) AC_COMPILE_IFELSE( [AC_LANG_SOURCE( [[ #include int audit_fgets(char *buf, size_t blen, int fd) __attr_access ((__write_only__, 1, 2)); int main(void) { return 0; }]])], [ACCESS="yes"], [ACCESS="no"] ) AC_MSG_RESULT($ACCESS) AC_MSG_CHECKING(__attr_dealloc_free support) AC_COMPILE_IFELSE( [AC_LANG_SOURCE( [[ #include const char *strdup(const char *buf) __attr_dealloc_free; int main(void) { return 0; }]])], [DEALLOC="yes"], [DEALLOC="no"] ) AC_MSG_RESULT($DEALLOC) withval="" AC_ARG_WITH(asan, AS_HELP_STRING([--with-asan],[build with asan sanitizer (default=no)]), use_asan=yes,use_asan=$withval) if test x$use_asan = xyes ; then AC_LANG_PUSH([C]) CCFLAGS="-fno-omit-frame-pointer" ASAN_CFLAGS="" for CFLAG in $CCFLAGS; do echo -n "checking for $CFLAG... " TMPFLAGS="$CFLAGS" CFLAGS="$CFLAGS $CFLAG" AC_LINK_IFELSE([AC_LANG_PROGRAM([[]], [[]])],[ASAN_CFLAGS="$ASAN_CFLAGS $CFLAG" AC_MSG_RESULT(yes)], [AC_MSG_RESULT(no)]) CFLAGS="$TMPFLAGS" done LLDFLAGS="-faddress-sanitizer -fsanitize=address" ASAN_LDFLAGS="" for LDFLAG in $LLDFLAGS; do echo -n "checking for $LDFLAG... " TMPLDFLAGS="$LDFLAGS" LDFLAGS="$LDFLAGS $LDFLAG" AC_LINK_IFELSE([AC_LANG_PROGRAM([[]], [[]])],[ASAN_LDFLAGS="$ASAN_LDFLAGS $LDFLAG" AC_MSG_RESULT(yes)], [AC_MSG_RESULT(no)]) LDFLAGS="$TMPLDFLAGS" done # how many flags should pass? just one "-fno-omit-frame-pointer" if [[ -z "$ASAN_CFLAGS" ]] || [[ "`echo "$ASAN_CFLAGS" | wc -w`" -ne 1 ]]; then AC_MSG_ERROR([ Compiler does not support asan sanitizer cflags ]) fi # how many flags should pass? just one of "-faddress-sanitizer -fsanitize=address" if [[ -z "$ASAN_LDFLAGS" ]] || [[ "`echo "$ASAN_LDFLAGS" | wc -w`" -ne 1 ]]; then AC_MSG_ERROR([ Compiler does not support asan sanitizer ldflags]) fi CFLAGS="$CFLAGS $ASAN_CFLAGS -O0" LDFLAGS="$LDFLAGS $ASAN_LDFLAGS" AC_DEFINE([USE_ASAN], [1], [Address Sanitizer is enabled]) fi AC_ARG_WITH(audit, AS_HELP_STRING([--with-audit],[turn on decision auditing (default=no)]), AC_DEFINE(USE_AUDIT,1,[Define if you want to enable decision auditing.]), []) AC_CHECK_DECLS([FAN_AUDIT], [], [], [[#include ]]) AC_CHECK_DECLS([FAN_OPEN_EXEC_PERM], [perm=yes], [perm=no], [[#include ]]) if test $perm = "no"; then AC_MSG_ERROR([FAN_OPEN_EXEC_PERM is not defined in linux/fanotify.h. It is required for the kernel to support it]) fi AC_CHECK_DECLS([FAN_MARK_FILESYSTEM], [], [], [[#include ]]) enable_fanotify_fs_error=no dnl FAN_FS_ERROR shutdown currently exposes a kernel close(2) hang. Keep the dnl monitor compiled out until the kernel problem is understood. dnl AC_ARG_ENABLE(fanotify-fs-error, dnl AS_HELP_STRING([--disable-fanotify-fs-error], dnl [disable FAN_FS_ERROR health monitoring even when supported]), dnl [enable_fanotify_fs_error=$enableval], dnl [enable_fanotify_fs_error=yes]) dnl case x$enable_fanotify_fs_error in dnl xyes) dnl AC_DEFINE([FAPOLICYD_ENABLE_FANOTIFY_FS_ERROR], [1], dnl [Define to enable FAN_FS_ERROR health monitoring]) dnl ;; dnl xno) dnl ;; dnl *) dnl AC_MSG_ERROR([bad value $enableval for --enable-fanotify-fs-error]) dnl ;; dnl esac withval="" AC_ARG_WITH(rpm, AS_HELP_STRING([--with-rpm],[Use the rpm database as a trust source]), use_rpm=$withval,use_rpm=yes) if test x$use_rpm = xyes ; then AC_CHECK_LIB(rpm, rpmtsInitIterator, , [AC_MSG_ERROR([librpm not found])], -lrpm) AC_CHECK_LIB(rpmio, rpmFreeCrypto, , [AC_MSG_ERROR([librpmio not found])], -lrpmio) AC_DEFINE(USE_RPM,1,[Define if you want to use the rpm database as trust source.]) fi AM_CONDITIONAL(WITH_RPM, test x$use_rpm = xyes) withval="" AC_ARG_WITH(deb, AS_HELP_STRING([--with-deb],[Use the deb database as a trust source]), use_deb=$withval,use_deb=no) if test x$use_deb = xyes ; then AC_CHECK_LIB(dpkg, pkg_array_init_from_hash, , [AC_MSG_ERROR([libdpkg not found])], -ldpkg) AC_DEFINE(USE_DEB,1,[Define if you want to use the deb database as trust source.]) AC_CHECK_LIB(md, MD5Final, , [AC_MSG_ERROR([libmd is missing])], -lmd) fi AM_CONDITIONAL(WITH_DEB, test x$use_deb = xyes) AM_CONDITIONAL(NEED_MD5, test x$use_deb = xyes) withval="" AC_ARG_WITH(perf-test, AS_HELP_STRING([--with-perf-test],[Build and install a performance test utility]), use_perf_test=$withval,use_perf_test=no) AM_CONDITIONAL(WITH_PERF_TEST, test x$use_perf_test = xyes) AC_ARG_ENABLE(stress, AS_HELP_STRING([--enable-stress], [Build non-installed stress test helper (default=no)]), use_stress=$enableval,use_stress=no) AM_CONDITIONAL(WITH_STRESS, test x$use_stress = xyes) AC_ARG_ENABLE(systemd-watchdog, AS_HELP_STRING([--disable-systemd-watchdog], [disable systemd Type=notify and watchdog support]), enable_systemd_watchdog=$enableval,enable_systemd_watchdog=yes) case x$enable_systemd_watchdog in xyes) AC_DEFINE([FAPOLICYD_ENABLE_SYSTEMD_WATCHDOG], [1], [Define to enable systemd notify/watchdog integration]) FAPOLICYD_SERVICE_TYPE=notify FAPOLICYD_SERVICE_NOTIFY_ACCESS=NotifyAccess=main FAPOLICYD_SERVICE_PIDFILE= FAPOLICYD_SERVICE_EXEC='/usr/sbin/fapolicyd --foreground' FAPOLICYD_SERVICE_WATCHDOG=WatchdogSec=10s ;; xno) FAPOLICYD_SERVICE_TYPE=forking FAPOLICYD_SERVICE_NOTIFY_ACCESS= FAPOLICYD_SERVICE_PIDFILE=PIDFile=/run/fapolicyd/fapolicyd.pid FAPOLICYD_SERVICE_EXEC=/usr/sbin/fapolicyd FAPOLICYD_SERVICE_WATCHDOG= ;; *) AC_MSG_ERROR([bad value $enableval for --enable-systemd-watchdog]) ;; esac AC_SUBST([FAPOLICYD_SERVICE_TYPE]) AC_SUBST([FAPOLICYD_SERVICE_NOTIFY_ACCESS]) AC_SUBST([FAPOLICYD_SERVICE_PIDFILE]) AC_SUBST([FAPOLICYD_SERVICE_EXEC]) AC_SUBST([FAPOLICYD_SERVICE_WATCHDOG]) dnl FIXME some day pass this on the command line def_systemdsystemunitdir=${prefix}/lib/systemd/system AC_SUBST([systemdsystemunitdir], [$def_systemdsystemunitdir]) echo . echo Checking for required header files AC_CHECK_HEADER(sys/fanotify.h, , [AC_MSG_ERROR( ["Couldn't find sys/fanotify.h...your kernel might not be new enough"] )]) AC_CHECK_FUNCS(fexecve, [], []) AC_CHECK_FUNCS([mallinfo2]) AC_CHECK_HEADER(uthash.h, , [AC_MSG_ERROR( ["Couldn't find uthash.h...uthash-devel is missing"] )]) echo . echo Checking for required libraries AC_CHECK_LIB(udev, udev_device_get_devnode, , [AC_MSG_ERROR([libudev not found])], -ludev) AC_CHECK_LIB(crypto, SHA256, , [AC_MSG_ERROR([openssl libcrypto not found])], -lcrypto) AC_CHECK_LIB(magic, magic_descriptor, , [AC_MSG_ERROR([libmagic not found])], -lmagic) AC_CHECK_LIB(cap-ng, capng_change_id, , [AC_MSG_ERROR([libcap-ng not found])], -lcap-ng) AC_CHECK_LIB(seccomp, seccomp_rule_add, , [AC_MSG_ERROR([libseccomp not found])], -lseccomp) AC_CHECK_LIB(lmdb, mdb_env_create, , [AC_MSG_ERROR([liblmdb not found])], -llmdb) LD_SO_PATH AC_CONFIG_FILES([Makefile src/Makefile src/tests/Makefile src/tests/stress/Makefile init/Makefile init/fapolicyd.service doc/Makefile rules.d/Makefile]) AC_OUTPUT echo . echo " fapolicyd Version: $VERSION Target: $target Installation prefix: $prefix Compiler: $CC Compiler flags: `echo $CFLAGS | fmt -w 50 | sed 's,^, ,'` Linker flags: `echo $LDFLAGS | fmt -w 50 | sed 's,^, ,'` __attr_access support: $ACCESS __attr_dealloc_free support: $DEALLOC FAN_FS_ERROR monitoring: $enable_fanotify_fs_error systemd watchdog: $enable_systemd_watchdog " fapolicyd-2.0/deb/000077500000000000000000000000001524071644000140625ustar00rootroot00000000000000fapolicyd-2.0/deb/README.Debian000066400000000000000000000005411524071644000161230ustar00rootroot00000000000000fapolicyd for Debian This is a simple application whitelisting daemon for Linux. RUNTIME DEPENDENCIES -------------------- * kernel >= 4.20 (Must support FANOTIFY_OPEN_EXEC_PERM. See [1] below.) After configuring fapolicyd with /etc/fapolicyd/fapolicyd.conf and adding the desired set of rules to /etc/fapolicyd/rules.d/ start the fapolicyd service. fapolicyd-2.0/deb/build_deb.sh000077500000000000000000000027031524071644000163340ustar00rootroot00000000000000#! /bin/bash set -euo pipefail script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) top_dir=$(cd -- "${script_dir}/.." && pwd) version=$(sed -n 's/^AC_INIT(\[fapolicyd\],\[\([^]]*\)\].*/\1/p' \ "${top_dir}/configure.ac") if [ "${version}" = "" ] ; then echo "Unable to determine fapolicyd version" >&2 exit 1 fi dist_dir="fapolicyd-${version}" source_archive="${top_dir}/${dist_dir}.tar.gz" build_archive="${script_dir}/${dist_dir}.tar.gz" source_dir="${script_dir}/${dist_dir}" # make dist otherwise reuses an existing archive, which may not match the # current source tree. rm -f "${source_archive}" make -C "${top_dir}" dist if [ ! -f "${source_archive}" ] ; then echo "Expected distribution archive ${source_archive} was not created" >&2 exit 1 fi # Recreate only this version's packaging tree so repeated runs cannot select # stale archives or unpack over the previous debmake output. rm -rf "${source_dir}" cp "${source_archive}" "${build_archive}" tar -xzf "${build_archive}" -C "${script_dir}" if [ ! -d "${source_dir}" ] ; then echo "Expected distribution directory ${source_dir} was not extracted" >&2 exit 1 fi ( cd "${source_dir}" # Ugly work around for INSTALL.tmp # Need to figure out proper fix. mv INSTALL INSTALL.tmp ) tar -C "${script_dir}" -czf "${build_archive}" "${dist_dir}" cd "${source_dir}" debmake cp "${script_dir}/rules" debian/ cp "${script_dir}/postinst" debian/ cp "${script_dir}/README.Debian" debian/ debuild fapolicyd-2.0/deb/postinst000077500000000000000000000005501524071644000156730ustar00rootroot00000000000000#! /bin/sh set -e adduser --system --group fapolicyd mkdir -p /etc/fapolicyd/rules.d mkdir -p /etc/fapolicyd/trust.d mkdir -p /var/lib/fapolicyd mkdir -p /usr/share/fapolicyd/ mkdir -p /run/fapolicyd/ chown -R fapolicyd:fapolicyd /etc/fapolicyd/ chown fapolicyd:fapolicyd /var/lib/fapolicyd/ chown root:fapolicyd /run/fapolicyd/ chmod 0770 /run/fapolicyd/ fapolicyd-2.0/deb/rules000077500000000000000000000003571524071644000151470ustar00rootroot00000000000000#!/usr/bin/make -f %: dh $@ --with autoreconf override_dh_auto_configure: dh_auto_configure -- \ --with-audit \ --disable-shared \ --without-rpm \ --with-deb \ --prefix=/usr override_dh_autoreconf: dh_autoreconf -- ./autogen.sh fapolicyd-2.0/dnf/000077500000000000000000000000001524071644000140775ustar00rootroot00000000000000fapolicyd-2.0/dnf/fapolicyd-dnf-plugin.py000066400000000000000000000016731524071644000204730ustar00rootroot00000000000000#!/usr/bin/python3 import dnf import os import stat import sys class Fapolicyd(dnf.Plugin): name = "fapolicyd" pipe = "/run/fapolicyd/fapolicyd.fifo" file = None def __init__(self, base, cli): pass def transaction(self): if not os.path.exists(self.pipe): sys.stderr.write("Pipe does not exist (" + self.pipe + ")\n") sys.stderr.write("Perhaps fapolicy-plugin does not have enough permissions\n") sys.stderr.write("or fapolicyd is not running...\n") return if not stat.S_ISFIFO(os.stat(self.pipe).st_mode): sys.stderr.write(self.pipe + ": is not a pipe!\n") return try: self.file = open(self.pipe, "w") except PermissionError: sys.stderr.write("fapolicy-plugin does not have write permission: " + self.pipe + "\n") return self.file.write("1\n") self.file.close() fapolicyd-2.0/doc/000077500000000000000000000000001524071644000140755ustar00rootroot00000000000000fapolicyd-2.0/doc/Makefile.am000066400000000000000000000023341524071644000161330ustar00rootroot00000000000000# Makefile.am -- # Copyright 2016,2018-21,2023,2026 Red Hat Inc. # All Rights Reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Authors: # Steve Grubb # EXTRA_DIST = $(man_MANS) fapolicyd-perf-test.8 check-manpages.sh \ systemd-watchdog.md TEST_EXTENSIONS = .sh SH_LOG_COMPILER = $(SHELL) TESTS = check-manpages.sh man_MANS = \ fapolicyd.8 \ fagenrules.8 \ fapolicyd-cli.8 \ fapolicyd.rules.5 \ fapolicyd.trust.5 \ fapolicyd.state.5 \ fapolicyd.metrics.5 \ fapolicyd.timing.5 \ fapolicyd.conf.5 \ rpm-filter.conf.5 \ fapolicyd-filter.conf.5 fapolicyd-2.0/doc/check-manpages.sh000077500000000000000000000023311524071644000173010ustar00rootroot00000000000000#!/bin/sh # # Check every manual page source in this directory with groff warnings enabled. # man exits successfully for formatter warnings, so stderr is treated as a # test failure. set -u : "${MAN:=man}" : "${srcdir:=.}" if test "$srcdir" = "."; then case "$0" in */*) srcdir=${0%/*} ;; esac fi if ! command -v "$MAN" >/dev/null 2>&1; then echo "SKIP: man command not found" exit 77 fi if command -v locale >/dev/null 2>&1; then if ! LC_ALL=C.UTF-8 locale charmap >/dev/null 2>&1; then echo "SKIP: C.UTF-8 locale not available" exit 77 fi fi if ! "$MAN" --help 2>&1 | grep -- "--warnings" >/dev/null 2>&1; then echo "SKIP: man command does not support --warnings" exit 77 fi failed=0 found=0 for page in "$srcdir"/*.[0-9]; do test -e "$page" || continue found=1 output=$( LC_ALL=C.UTF-8 MANROFFSEQ='' MANWIDTH=80 \ "$MAN" --warnings -E UTF-8 -l -Tutf8 -Z "$page" \ 2>&1 >/dev/null ) status=$? if test $status -ne 0 || test -n "$output"; then failed=1 echo "$page:" if test -n "$output"; then printf '%s\n' "$output" fi if test $status -ne 0; then echo "man exited with status $status" fi fi done if test $found -eq 0; then echo "No manual pages found" exit 1 fi exit $failed fapolicyd-2.0/doc/fagenrules.8000066400000000000000000000026761524071644000163340ustar00rootroot00000000000000.TH FAGENRULES "8" "Nov 2021" "Red Hat" "System Administration Utilities" .SH NAME fagenrules \- a script that merges component fapolicyd rule files .SH SYNOPSIS .B fagenrules .RI [ \-\-check ]\ [ \-\-load ] .SH DESCRIPTION \fBfagenrules\fP is a script that merges all component fapolicyd rules files, found in the fapolicyd rules directory, \fI/etc/fapolicyd/rules.d\fP, placing the merged file in \fI/etc/fapolicyd/compiled.rules\fP. Component fapolicyd rule files, must end in \fI.rules\fP in order to be processed. All other files in \fI/etc/fapolicyd/rules.d\fP are ignored. .P The files are concatenated in order, based on their natural sort (see -v option of ls(1)) and stripped of empty and comment (#) lines. .P The generated file is validated with the daemon policy parser before installation. If it differs from the existing compiled rules, it is synced and atomically renamed into \fI/etc/fapolicyd/compiled.rules\fP so the previous compiled rules remain in place on validation or installation failure. .P \fBfagenrules\fP prints the final SHA-256 ruleset identity when the installed compiled rules are unchanged or successfully updated. .SH OPTIONS .TP .B \-\-check test if rules have changed and need updating without overwriting compiled.rules. .TP .B \-\-load load old or newly built rules into the daemon. .SH FILES /etc/fapolicyd/rules.d/ /etc/fapolicyd/compiled.rules .SH "SEE ALSO" .BR fapolicyd.rules (5), .BR fapolicyd-cli (8), .BR fapolicyd (8). fapolicyd-2.0/doc/fapolicyd-cli.8000066400000000000000000000353411524071644000167130ustar00rootroot00000000000000.TH "FAPOLICYD-CLI" "8" "March 2026" "Red Hat" "System Administration Utilities" .SH NAME fapolicyd-cli \- Fapolicyd CLI Tool .SH SYNOPSIS \fBfapolicyd-cli\fP [\fIoptions\fP] .SH DESCRIPTION The fapolicyd command line utility performs maintenance and troubleshooting tasks for fapolicyd. It can ask the daemon to reload state, inspect the trust database, validate configuration, test filter decisions, and manage administrator-defined trust entries. Normally the daemon learns about package changes through integration such as the dnf plugin, but manual package installation or direct edits to trust files still require explicit administrator action. .SH PRIVILEGES The CLI checks command privilege requirements before accessing protected files or contacting the daemon. The following commands require root because they modify administrator-owned trust files or use daemon controls restricted to a privileged signal sender: .BR \-\-file , .BR \-\-check-status , .BR \-\-check-metrics , .BR \-\-reset-metrics , .BR \-\-timing-start , and .BR \-\-timing-stop . .PP Commands that read the installed configuration, rules, or trust database, and commands that use the daemon update FIFO, accept either root or a caller in the group that owns .IR /etc/fapolicyd . These include .BR \-\-check-config , .BR \-\-check-path , .BR \-\-check-trustdb , .BR \-\-check-watch_fs , .BR \-\-check-ignore_mounts , .BR \-\-list , .BR \-\-dump-db , .BR \-\-delete-db , .BR \-\-update , .BR \-\-reload-rules , .BR \-\-compact-trustdb , and .BR \-\-test-filter . Checking the active rules with .B \-\-check-rules has the same requirement. An unprivileged caller may still check an explicitly named readable rules file, classify a readable file with .BR \-\-ftype , or display help. .SH OPTIONS .TP .B \-h, \-\-help Prints a list of command line options. .TP .B \-\-check-config Opens fapolicyd.conf and parses it to see if there are any syntax errors in the file. .TP .B \-\-check-path Check the PATH environmental variable against the trustdb to look for file not in the trustdb which could cause problems at run time. .TP .B \-\-check-status Dump the daemon's health and configuration state. This report is for checking whether the daemon is healthy and configured as expected. It includes operating mode, startup-sized resources, current resource utilization, headline allow and deny activity, health indicators, and watched mounts. Runtime counters, attribute lookup tables, cache effectiveness metrics, and rule hit counts are reported by .BR \-\-check-metrics . See .BR fapolicyd.state (5) for the state report fields. This command requests a plain state report and does not ask the daemon to reset runtime metric counters. .TP .B \-\-check-metrics Dump the daemon's runtime metrics report from \fI/run/fapolicyd/fapolicyd.metrics\fP. The report starts with .B Last metrics reset , .B Config generation , .B Ruleset generation , .B Trust database generation , and .B LMDB environment generation headers so the counter window, active decision configuration generation, active policy generation, trust content generation, and physical LMDB storage generation are explicit. It then prints decision outcome counters, subject and object cache effectiveness, rule hit counts, subject and object attribute lookup tables, and queue and defer activity. See .BR fapolicyd.metrics (5) for the metrics report fields. .TP .B \-\-check-trustdb Check the trustdb against the files on disk to look for mismatches that will cause problems at run time. .TP .B \-\-compact-trustdb Request controlled trust database LMDB compaction. The daemon refreshes backend snapshots, builds a replacement LMDB environment in a temporary directory, validates it, then briefly blocks new trust database readers, closes the live environment, and swaps in the replacement. The previous environment is kept or restored on failure. This command is intended for administrator maintenance when .BR fapolicyd-cli\ --check-status reports that trust database compaction is recommended; ordinary trust database updates should use .BR \-\-update . .TP .B \-\-check-watch_fs Check the mounted file systems against the watch_fs daemon config entry to determine if any file systems need to be added to the configuration. .TP .B \-\-check-ignore_mounts[=mount-point] Inspect the ignore_mounts list or the provided mount point for potentially executable content. The command verifies that each mount exists, confirms the presence of the \fBnoexec\fP option, and scans the directory tree for files matching risk categories that fapolicyd cannot see on an ignored mount. The summary reports counts for executable regular files, ELF/shared objects, archives/JARs/ZIPs, bytecode caches, plugin/runtime directories, and files matching the \fB%languages\fP macro. A non-zero status is returned when risky entries are found so automated workflows can gate changes. .TP .B \-\-check-rules [path] Parse the rules file with the daemon policy parser without loading it into the daemon. If \fIpath\fP is omitted, the command checks the active rules file, using \fI/etc/fapolicyd/fapolicyd.rules\fP when present and \fI/etc/fapolicyd/compiled.rules\fP otherwise. If both installed rules files exist, the command reports a path configuration error unless \fB\-\-lint\fP is also used. A zero exit status means the file is valid. Values with the explicit \fBglob:\fP prefix are accepted only for \fBexe\fP and \fBpath\fP, and the pattern after the prefix must be an absolute path. This command reports violations of those requirements, including patterns supplied through named sets. It does not expand valid patterns or simulate which concrete paths they match. .TP .B \-\-lint When used with \fB\-\-check-rules\fP, also emit policy-shape warnings for executable or programmatic content that can reach the default-allow path. The option may be placed before or after \fB\-\-check-rules\fP. A warning causes a non-zero exit status. .RS .P Examples: .nf fapolicyd-cli --check-rules --lint fapolicyd-cli --check-rules /tmp/proposed.rules --lint .fi .P Warnings include rule numbers and file-line locations where a specific rule is involved. For example: .nf Policy lint warning: executable events can fall through; no terminal broad execute deny found after rule 12 at /etc/fapolicyd/compiled.rules:13 Policy lint hint: add a final "deny_audit perm=execute all : all" rule Policy lint warning: broad open allow rule 17 at /etc/fapolicyd/compiled.rules:18 appears before programmatic-content deny rule 18 at /etc/fapolicyd/compiled.rules:19 .fi .P When both \fI/etc/fapolicyd/fapolicyd.rules\fP and \fI/etc/fapolicyd/compiled.rules\fP exist, linting warns that the old rules file shadows \fIcompiled.rules\fP and recommends removing or migrating the old file. .RE .TP .B \-\-reset-metrics Request the metrics to be reset. Runtime metric counters reset after a metrics report is written when the daemon is configured with \fBreset_strategy=manual\fP. Rule hit counters normally reset when a new ruleset generation is loaded, but this command also clears them after reporting so the current rules can be tested from a fresh counter window. The report contains the counter values being reset. A successful reset also updates the .B Last metrics reset timestamp shown by later metrics reports. If metrics have never been reset since daemon start, the header says .BR never . The CLI checks the on-disk \fIfapolicyd.conf\fP before requesting the metrics to be reset. If the on-disk \fBreset_strategy\fP is not \fBmanual\fP, or the setting cannot be verified, the command tells the user and sends a plain \fB\-\-check-metrics\fP request instead. The daemon's active in-memory setting may differ from the file until configuration is reloaded. The command asks for confirmation before sending the reset request unless \fB-y\fP or \fB--yes\fP is used. .TP .B \-\-timing-start, \-\-timer-start Request a manual decision timing run. The daemon honors the request only when the active configuration has \fBtiming_collection=manual\fP and the signal sender is privileged. Starting resets the fixed per-worker timing aggregates and starts collecting timing for subsequent fanotify decisions. .TP .B \-\-timing-stop, \-\-timer-stop Stop a manual decision timing run and ask the daemon to write \fI/run/fapolicyd/fapolicyd.timing\fP. The command waits for that report and writes it to stdout. If a timing report already exists, the command asks before replacing it. If timing is not armed, the report says so instead of printing timing data. See .BR fapolicyd.timing (5) for the report fields, including the .B TL;DR summary printed before the detailed latency sections. The daemon honors the request only when the active configuration has \fBtiming_collection=manual\fP and the signal sender is privileged. .TP .B \-y, \-\-yes Do not prompt before \fB--reset-metrics\fP sends a reset request. .TP .B \-d, \-\-delete-db Deletes the trust database. Normally this never needs to be done. But if for some reason the trust database becomes corrupted, then the only method of recovery is to run this command. .TP .B \-D, \-\-dump-db Dumps the trust db contents for inspection. This will print the original trust source, path, file size, and recorded hash of the file as known by the trust source the entry came from. The rpm backend may provide SHA256 or SHA512 digests depending on how the package was built. .TP .B \-f, \-\-file add|delete|update [path] Manage the file trust database. .PP The path is a command-line argument, not a rule-file value. Use the shell's quoting to pass a path containing a space, for example: .nf .B fapolicyd-cli --file add "/tmp/first last/" .B fapolicyd-cli --file delete "/tmp/first last/" .fi The CLI receives the unquoted path. A literal \fB%20\fP in a command-line path is not decoded to a space. .RS .TP 12 .B add This command adds the file given by path to the trust database. It gets the size and calculates the required hash (SHA256 by default). If the path is a directory, it will walk the directory tree to the bottom and add every regular file that it finds. By default, the path is appended to the end of the \fBfapolicyd.trust\fP file. .TP 12 .B delete This command deletes all entries that match from the trust database. It will try to match multiple entries so that entire directories can be deleted in one command. To ensure that you only match a directory and not a partial name, be sure to end with '/'. .TP 12 .B update This command updates the size and hash of any matching paths in the file trust database. If no path is given, then all files are updated. If an argument is passed, then only matching paths get updated. If the intent is to match against a directory, ensure that it ends with '/'. .TP 12 .B --filter When used with \fBadd\fP or \fBupdate\fP, evaluate the selected files and directories through the filter configuration (\fIfapolicyd-filter.conf\fP). Paths excluded by the filter are skipped so only allowed entries are added or refreshed. .RE .TP .B \-\-trust-file trust-file-name Use after \fBfile\fP option. Makes every command of \fBfile\fP option operate on a single trust file named \fBtrust-file-name\fP that is located inside trust.d directory. If a trust file with such a name does not exist inside trust.d directory, it is created. .TP .B \-\-test-filter /path/to/file Evaluate the filter configuration against the given path and emit a rule-by-rule trace ending with "decision include" or "decision exclude". Use this to confirm whether a file would be eligible for inclusion in the trust database. .TP .B \-t, \-\-ftype /path/to/file Prints the mime type of the file given. A full path must be specified. This command is intended to help get the ftype parameter of rules correct by seeing how fapolicyd will classify it. Fapolicyd may differ from the \fBfile\fP command. .TP .B \-T, \-\-ftype-ext /path/to/file Writes two NUL-terminated fields to standard output: the supplied path followed by the fapolicyd MIME type. Each invocation writes one path/MIME pair. This binary output is intended for scripts; use \fB\-\-ftype\fP for interactive, MIME-only output. .PP The following Bash example reads one or more path/MIME pairs safely: .RS .nf 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 .fi .RE .PP A pathname component may contain every byte except NUL and slash, including spaces, tabs, newlines, quotes, backslashes, glob characters, leading hyphens, colons, and terminal control sequences. NUL field framing prevents those bytes from creating additional output records, but does not make them safe for shell evaluation or terminal display. Do not use command substitution, unquoted expansions, \fBeval\fP, or newline-delimited file lists for these values. Use quoted variables when passing paths to commands, and use \fBfind -print0\fP with the same NUL-aware read pattern when discovering paths. The Bash \fB%q\fP conversion in the example renders values safely for a terminal. .TP .B \-l, \-\-list Prints a listing of the fapolicyd rules file with a rule number to aid in troubleshooting or understanding of the debug messages. .TP .B \-u, \-\-update Notifies fapolicyd to perform an update of the trust database. .TP .B \-r, \-\-reload-rules Notifies fapolicyd to perform a reload of the rules. .TP .B \-\-verbose Enable additional output for commands that support it. At present this affects \fB\-\-check-ignore_mounts\fP by printing each file or directory that matched a risk category, the category names, and the detected MIME type when available. .SH RETURN CODES The following exit status values are used for error reporting and scripting: .TP .B 0 Success Normal completion. .TP .B 1 Generic/unspecified failure Fallback for errors that do not map to a more specific category. .TP .B 2 CLI/usage error Incorrect options or argument counts. .TP .B 3 Path/configuration error Failed \fBrealpath\fP lookups, malformed configuration, or invalid mount overrides. .TP .B 4 Database/LMDB error Trust database operations failed (creation, open, cursor traversal, or deletion). .TP .B 5 Rule/filter error Filter initialization or parsing problems, including \fB%languages\fP handling. .TP .B 6 Daemon/IPC error Communication with the daemon failed (FIFO permissions/shape issues, missing PID, or status report timeouts). .TP .B 7 Filesystem/I\-O/permission error Problems opening, stat'ing, or writing files (including rule files or the trust database on disk). .TP .B 8 Internal software/OOM Allocation failures or other unexpected internal errors. .TP .B 9 No\-op/Not\-found/Nothing to do Operations that completed without making changes, such as attempting to update, delete, or add entries that were not present. .SH "SEE ALSO" .BR fapolicyd (8), .BR fapolicyd.rules (5), .BR fapolicyd.state (5), .BR fapolicyd.metrics (5), .BR fapolicyd.timing (5), .BR fapolicyd.trust (5), and .BR fapolicyd.conf (5) .SH AUTHOR Zoltan Fridrich fapolicyd-2.0/doc/fapolicyd-filter.conf.5000066400000000000000000000076341524071644000203560ustar00rootroot00000000000000.TH FAPOLICYD_FILTER.CONF: "15" "June 2023" "Red Hat" "System Administration Utilities" .SH NAME fapolicyd-filter.conf \- fapolicyd filter configuration file .SH DESCRIPTION The filter controls which files from a trust source are added to the TrustDB. Rules are processed from top to bottom, with indentation narrowing the scope of the parent rule. A matching plus (+) rule causes a file to be included; a minus (-) rule excludes it. If no rule matches, the file is excluded by default. The filter is consulted only when trust data is imported: during system updates and when the daemon starts and rebuilds its database. Runtime policy decisions use the TrustDB itself, not the filter, so changes to the filter affect new or rebuilt trust entries rather than live access checks. Valid line starts with character '+', '-' or '#' for comments. The rest of the line contains a path specification. Space can be used as indentation to add more specific filters to the previous one. Note, that only one space is required for one level of an indent. If there are multiple specifications on the same indentation level they extend the previous line with lower indentation, usually a directory. The path may be specified using the glob pattern. A directory specification has to end with a slash ‘/’; without it the rule is treated as a file glob and the directory decision from the parent rule remains in effect. If the result was a plus (+), the respective file from a trust source is imported to the TrustDB. Vice versa, if the result was a minus (-), the respective file is not imported. From a performance point of view it is better to design an indented filter because in the ideal situation each component of the path is compared only once. In contrast to it, a filter without any indentation has to contain a full path which makes the pattern more complicated and thus slower to process. The motivation behind this is to have a flexible configuration and keep the TrustDB as small as possible to make the look-ups faster. .nf .B # this is simple allow list .B - /usr/bin/some_binary1 .B - /usr/bin/some_binary2 .B + / .fi .nf .B # this is the same .B + / .B \ + usr/bin/ .B \ \ - some_binary1 .B \ \ - some_binary2 .fi .nf .B # this is similar allow list with a wildcard .B - /usr/bin/some_binary? .B + / .fi .nf .B # this is similar with another wildcard .B + / .B \ - usr/bin/some_binary* .fi .nf .B # keeps everything except usr/share except python and perl files .B # /usr/bin/ls - result is '+' .B # /usr/share/something - result is '-' .B # /usr/share/abcd.py - result is '+' .B + / .B \ - usr/share/ .B \ \ + *.py .B \ \ + *.pl .fi .SH THEORY OF OPERATION .PP The filter configuration is parsed into a tree where each node represents a path fragment and whether it is included or excluded. Each level of indentation in the configuration file becomes another depth level in that tree. During evaluation the daemon iteratively walks the tree with an explicit stack rather than recursion, advancing through the path as fragments match. This approach keeps evaluation deterministic and prevents deep call stacks, but it also means filter nesting cannot exceed 64 levels; longer hierarchies are rejected and reported as depth errors. .SH TESTING FILTERS .PP Administrators can validate how a change to \fIfapolicyd-filter.conf\fP behaves before rebuilding the trust database. Use \fBfapolicyd-cli --test-filter /path/to/file\fP to trace how the configuration is applied to a specific path and see the final include/exclude decision. When adding or updating trust entries, combine \fB--file add\fP or \fB--file update\fP with \fB--filter\fP so only paths that survive the filter are processed; this is useful when pointing the tool at directories to ensure the filter omits unwanted content while you test. .SH FILES .B /etc/fapolicyd/fapolicyd-filter.conf .SH "SEE ALSO" .BR fapolicyd (8), .BR fapolicyd-cli (1) .BR fapolicy.rules (5) and .BR glob (7) .SH AUTHOR Radovan Sroka fapolicyd-2.0/doc/fapolicyd-perf-test.8000066400000000000000000000022131524071644000200450ustar00rootroot00000000000000.TH "FAPOLICYD-PERF-TEST" "26" "Mar 2026" "Red Hat" "System Administration Utilities" .SH NAME fapolicyd-perf-test \- Fapolicyd Performance Test Utility .SH SYNOPSIS \fBfapolicyd-perf-test\fP [\fIINPUT_FILE\fP] .SH DESCRIPTION Runs a dummy fapolicyd policy decision on each file from newline-separated list read from INPUT_FILE (or stdin if not specified) and prints the total time it took. .SH RETURN CODES The following exit status values are used for error reporting and scripting: .TP .B 0 Success Normal completion. .TP .B 1 Generic/unspecified failure Fallback for errors that do not map to a more specific category. .TP .B 2 CLI/usage error Incorrect options or argument counts. .SH EXAMPLE USAGE .nf \fB# touch file-list.txt\fP \fB# find /usr/lib64 -type f >>file-list.txt\fP \fB# find /usr/bin -type f >>file-list.txt\fP \fB# find /usr/share -type f >>file-list.txt\fP \fB# wc -l file-list.txt\fP 15080 file-list.txt \fB# fapolicyd-perf-test file-list.txt 2>/dev/null\fP Starting scan... Elapsed: 0 seconds, 58 milliseconds \fB# \fP .fi .SH "SEE ALSO" .BR fapolicyd (8), .BR fapolicyd-cli (8), and .BR fapolicyd.conf (5) .SH AUTHOR Ondrej Mosnacek fapolicyd-2.0/doc/fapolicyd.8000066400000000000000000000123171524071644000161440ustar00rootroot00000000000000.TH "FAPOLICYD" "8" "March 2026" "Red Hat" "System Administration Utilities" .SH NAME fapolicyd \- File Access Policy Daemon .SH SYNOPSIS .B fapolicyd .RI [ --debug | --debug-deny | --foreground ] .RI [ --permissive ] .RI [ --no-details ] .RI [ --mounts=PATH ] .RI [ --version ] .SH DESCRIPTION \fBfapolicyd\fP is a userspace daemon that determines access rights to files based on a trust database and file or process attributes. It can be used to either blacklist or whitelist file access and execution. Configuring \fBfapolicyd\fP is done with the files in the \fI/etc/fapolicyd/\fP directory. There are three files: .B compiled.rules , .B fapolicyd.conf , and .B fapolicyd.trust. The first one contains the access policy, the second determines the daemon's configuration, and the last allows admin defined trusted files. The default rules will generate audit events whenever there is a denial. NOTE: you must have at least 1 audit rule loaded for the audit system to create the full FANOTIFY event. It doesn't matter which rule is loaded. To see if you have any denials, you can run the following command: .RS .TP 2 .B ausearch \-\-start today \-m fanotify \-i .RE or instead of \-i, you can add \-\-format text to get an easier to read audit event. .SH OPTIONS .TP .B \-\-debug Run the daemon in the foreground and write event information to stderr so policy decisions can be observed. .TP .B \-\-debug\-deny Run the daemon in the foreground and write only deny decisions to stderr. .TP .B \-\-foreground Run the daemon in the foreground without enabling debug output. This is intended for service managers using readiness or watchdog supervision. .TP .B \-\-permissive Allow file access even when policy would deny it. This is useful when validating rules before enforcing them. .TP .B \-\-mounts=PATH In debug mode only, read mount information from the regular file at .I PATH instead of .I /proc/mounts. The file must use the same format as .IR /proc/mounts . This is useful when reproducing mount-related issues with a captured or filtered mount list. .TP .B \-\-no-details Suppress process and file names in the shutdown usage report while keeping the aggregate statistics. .TP .B \-\-version Display version information and exit. .SH SIGNALS .TP .B SIGTERM causes fapolicyd to discontinue processing events, write it's performance report, and exit. .TP .B SIGHUP causes fapolicyd to reload the trust database. .TP .B SIGUSR1 causes fapolicyd to dump the requested report to .IR /run/fapolicyd/fapolicyd.state or .IR /run/fapolicyd/fapolicyd.metrics . See .BR fapolicyd.state (5) and .BR fapolicyd.metrics (5). When .B reset_strategy=manual is configured, a SIGUSR1 report that carries reset intent (only possible programmatically) also resets runtime metric counters after the report is written. Plain SIGUSR1 reports do not reset counters. SIGUSR1 can also carry timing start and stop intents. Those intents are honored only for privileged senders when .B timing_collection=manual is active; stopped timing runs are written to .IR /run/fapolicyd/fapolicyd.timing . .SH NOTES Whatever you do, DO NOT TRY TO ATTACH WITH PTRACE. Ptrace attachment sends a SIGSTOP which cannot be blocked. Since your whole system depends on fapolicyd approving access to glibc and various critical libraries, that will not happen until SIGCONT is sent. The system can deadlock if the continue signal is not sent. To get audit events, you must have auditing enabled and at least one systemcall rule loaded. Otherwise you will not get any events. If the rpmdb is set as a trust source, you should minimize the number of 32 bit packages on the system. In such cases, there may be a 32 bit and 64 file with the same pathname. Obviously only one can exist on the disk. So, this will always cause database miscompares and cause a delay in the daemon being operational. The .B compiled.rules file is the resulting merge of component rules in /etc/fapolicyd/rules.d/ See the .B fagenrules man page for more information. When built with systemd watchdog support, the packaged service uses .B Type=notify and starts fapolicyd with .BR --foreground . See .I systemd-watchdog.md in the documentation directory for watchdog timeout and drop-in examples. If you are running in the debug mode and wish to compare rule numbers reported in the output with which rule is actually triggering, you can see the rules with the corresponding number by running the following command: .nf .B fapolicyd-cli \-\-list .fi .SH FILES .B /etc/fapolicyd/fapolicyd.conf - daemon configuration .P .B /etc/fapolicyd/compiled.rules - access control rules .P .B /etc/fapolicyd/fapolicyd.trust - admin defined trusted files .P .B /var/log/fapolicyd-access.log - information about what was being accessed. .P .B /run/fapolicyd/fapolicyd.state - daemon state report. See .BR fapolicyd.state (5). .P .B /run/fapolicyd/fapolicyd.metrics - runtime metrics report. See .BR fapolicyd.metrics (5). .P .B /run/fapolicyd/fapolicyd.timing - manual decision timing report. See .BR fapolicyd.timing (5). .SH "SEE ALSO" .BR fapolicyd-cli (8), .BR fapolicyd.rules (5), .BR fapolicyd.state (5), .BR fapolicyd.metrics (5), .BR fapolicyd.timing (5), .BR fapolicyd.trust (5), .BR fapolicyd-filter.conf (5), .BR fagenrules (8), and .BR fapolicyd.conf (5) .SH AUTHOR Steve Grubb fapolicyd-2.0/doc/fapolicyd.conf.5000066400000000000000000000471761524071644000171000ustar00rootroot00000000000000.TH FAPOLICYD.CONF: "5" "September 2022" "Red Hat" "System Administration Utilities" .SH NAME fapolicyd.conf \- fapolicyd configuration file .SH DESCRIPTION The file .I /etc/fapolicyd/fapolicyd.conf contains configuration information for the application whitelisting daemon configuration. This file allows the admin to tune the performance and actions of the fapolicyd during runtime. This file contains one configuration keyword per line, an equal sign, and then followed by appropriate configuration information. All option names and values are case insensitive. The keywords recognized are listed and described below. Each line should be limited to 160 characters or the line will be skipped. You may add comments to the file by starting the line with a '#' character. .TP .B permissive This option is either a 0 to mean send policy decisions to the kernel for enforcement. Or it can be a 1 to mean always allow the access even if policy would block it. This should only be used for policy testing and debug. The default value is 0. .TP .B nice_val This option gives fapolicyd a scheduler boost. The number can be from 0 to 20. The default value is 10. .TP .B q_size This option is used to control how big of an internal queue that fapolicyd will use. If requests come in faster than fapolicyd can answer, the queue holds the pending requests. If the do_stat_report is enabled, when fapolicyd shutsdown it will provide some statistics which includes maximum queue depth used. This information can be used to help tune performance. The default value is 800. Also note, this value means that fapolicyd gets a file descriptor for that entry. There is an rlimit cap controlled by systemd's LimitNOFILE setting for the service. You may also need to adjust it if the q_size exceeds its value. When .B decision_threads is greater than 1, this value is applied to each decision worker. The total userspace queue capacity and approximate fanotify permission file descriptor budget are therefore .B decision_threads multiplied by .BR q_size , plus the daemon's other descriptors. .TP .B decision_threads This option controls how many decision worker threads fapolicyd is configured to use. The default value is 1. The value must be at least 1, no larger than the number of online CPU cores, and no larger than the daemon's supported maximum. Startup validation also checks that the configured worker count fits the LMDB reader-slot reservation, estimated fixed worker memory budget, and file descriptor budget implied by .BR q_size . Each decision worker owns its own event queue, subject cache, object cache, and subject-slot defer array. Increasing this value can reduce queueing when decision processing is CPU-bound, but it also increases the fixed memory and file descriptor footprint of those per-worker resources. The trust database map is shared, but each worker still needs an LMDB reader slot. This setting is applied when the daemon starts; restart fapolicyd after changing it. .TP .B uid This can be a number or an account name which fapolicyd should switch to during startup. The default value is 0 because it is guaranteed to exist. But it is recommended to use the fapolicyd account if that exists. .TP .B gid This can be a number or an group name which fapolicyd should switch to during startup. The default value is 0 because it is guaranteed to exist. But it is recommended to use the fapolicyd group if that exists. .TP .B do_stat_report This option controls whether (1) or not (0) fapolicyd should create a usage statistics report on shutdown. The report is written to /var/log/fapolicyd-access.log. This report gives information about number of allowed accesses and denials. Then for both the subject and object cache, it dumps information about size, hits, misses, and evictions. The default value is 1 which means create the report. .TP .B detailed_report This option controls whether (1) or not (0) fapolicyd should add subject and object information to the usage statistics report. This would be information about the exact process or file path in the cache from most recently used to last recently used. This can be useful for forensics if an incident had occurred. But if the file names are sensitive then you may want to turn this off. The default value is 1 meaning add the details. .TP .B db_max_size This option controls how many megabytes to allow the trust database to grow to. If you have lots of packages installed, then you want to make it bigger. The default value is "auto", which tells the daemon to size the trust database based on current utilization whenever it starts or rebuilds the database. Auto sizing starts from a 100 megabyte baseline, targets roughly 75% active database usage, keeps lower-utilization reload headroom for the current trust database generation and the candidate generation being built, grows when needed, and shrinks conservatively when utilization is low. Manual numeric values are never changed by the daemon. If a manual value is too small for a safe reload, fapolicyd logs a warning with the current size and a recommended minimum. Set \fBdb_max_size\fP to at least that recommendation, or use \fBdb_max_size = auto\fP so the daemon can resize the LMDB map as package sets change. .TP .B subj_cache_size This option controls how many entries the subject cache holds. You want the size to be big enough that you are not getting too many evictions compared to hits. But you don't want to waste memory. Whenever there is an eviction, fapolicyd has to regenerate information about the subject and this slows performance. There are only 64k processes allowed at any time, so this would be the upper limit. The default value is 4099. With multiple .B decision_threads , this value is per worker. Leaving it unchanged increases total subject cache capacity and memory. Reducing it to keep the memory footprint stable can increase subject collisions, early subject cache evictions, and subject defer fallbacks. .TP .B obj_cache_size This option controls how many entries the object cache holds. You want the size to be big enough that you are not getting too many evictions compared to hits. But you don't want to waste memory. Whenever there is an eviction, fapolicyd has to regenerate information about the object and this slows performance. The default value is 8191. With multiple .B decision_threads , this value is per worker. Leaving it unchanged increases total object cache capacity and memory. Reducing it to keep the memory footprint stable can increase object cache misses, collisions, and evictions. .TP .B watch_fs This is a comma separated list of file systems that should be watched for access permission. No attempt is made to validate the file systems names. They should exactly match the name presented in the first column of /proc/mounts. If this is not configured, it will default to watching ext4, xfs, and tmpfs. .TP .B ignore_mounts .B ignore_mounts A comma\-separated list of mount points that fapolicyd must not watch, even when their filesystem type matches .BR watch_fs . Entries must be absolute paths exactly as shown in the second column of .BR /proc/mounts ; whitespace around commas is ignored. Each listed mount .B must be mounted with the .BR noexec option; otherwise the daemon warns and monitors the mount point instead. The root filesystem .B / is always monitored. This option cannot be combined with .BR allow_filesystem_mark =1 . See the discussion in .BR "SECURITY CONSIDERATIONS FOR ignore_mounts" . .TP .B trust This is a comma separated list of trust back-ends. If this is not configured, 'rpmdb,file' is default. Fapolicyd supports \fBfile\fP back-end that reads content of /etc/fapolicyd/fapolicyd.trust and use it as a list of trusted files. The second option is \fBrpmdb\fP backend that generates list of trusted files from rpmdb. .TP .B integrity This option tells fapolicyd which integrity strategy it should use. It can be one of 4 values: .RS .TP 12 .B none This is the .IR default and does no integrity checking. .TP .B size Selecting this option will compare the size of the file with what it was knows to be. This is better than nothing and very fast since fapolicyd already collects size information during normal processing. However, an attacker could replace the file and as long as the size matches, it will not be detected. .TP .B ima Selecting this option will use a hash that the IMA subsystem places in a file's extended attributes in addition to the size check. IMA measurements can be SHA256 or SHA512 depending on kernel policy. When the IMA digest and trust metadata disagree, fapolicyd recomputes the IMA hash for a single retry before logging rate-limited warnings. The recomputation adds hashing overhead on the first mismatch and there is currently no configuration knob to disable these warnings. This means that all file systems holding executable code must support extended attributes. .TP .B sha256 Selecting this option will calculate a SHA256 hash by cryptographic means. A size check will also be performed. .RE .TP .B syslog_format This option controls how the output from the access decision is formatted. The format is a comma separated list of subject and object names from the rules. It does not allow the keyword "all". It also allows for rule, dec, and perm. The format must include a semi-colon to delineate subject from object keywords. The typical use is to place information about the access decision, then subject information, a colon, and the object information. Also note that the more things being logged, the more it will impact system performance. Also, the event written is limited to 512 bytes. Example: .nf .B syslog_format = rule,dec,perm,auid,pid,exe,:,path,ftype,trust .fi .TP .B rpm_sha256_only When this option is set to 1, it will force RPM trust entries to use SHA256 or larger hashes. This is useful on systems where the integrity is set to SHA256 or IMA and some rpms were originally built with e.g. SHA1. The daemon will ignore these SHA1 entries when the RPM backend imports trust data. Hash integrity lookups also reject SHA1/MD5 records that are already present in the active trust database, so stale weak RPM entries do not remain trusted after a configuration reload enables this option. If set to 0 the daemon stores and uses SHA1/MD5 from RPM trust data as well. This is compatible with older behavior which works with the integrity set to NONE and SIZE. The NONE or SIZE integrity setting considers the files installed via rpm as trusted and it does not care about their hashes at all. The default value is 0. .TP .B allow_filesystem_mark When this option is set to 1, it allows fapolicyd to monitor file access events on the underlying file system when they are bind mounted or are overlayed (e.g. the overlayfs). Normally they block fapolicyd from seeing events on the underlying file systems. This may or may not be desirable. For example, you might start seeing containers accessing things outside of the container but there is no source of trust for the container. In that case you probably do not want to see access from the container. Or maybe you do not use containers but want to control anything run by systemd-run when dynamic users are allowed. In that case you probably want to turn it on. Not all kernel's support this option. Therefore the default value is 0. This option cannot be used when \fBignore_mounts\fP lists one or more paths. Filesystem marks extend monitoring beneath bind or overlay mounts in a way that prevents individual mount points from being ignored. When both options appear in the configuration the daemon terminates with an error so the conflict can be corrected before startup. .TP .B report_interval This option specifies a reporting interval, measured in seconds, which fapolicyd uses to schedule recurring dumps of daemon state to \fBfapolicyd.state\fP and runtime metrics to \fBfapolicyd.metrics\fP. See \fBfapolicyd.state\fP(5) and \fBfapolicyd.metrics\fP(5) for the report fields. The default value of 0 disables interval reporting. .TP .B reset_strategy This option controls whether runtime metric counters are reset as part of metrics report generation. The default value is \fBnever\fP, which preserves the historical behavior where metrics grow for the lifetime of the daemon. Use \fBauto\fP when interval reports should describe only the activity since the previous timer-based report. Use \fBmanual\fP when counters should reset only after a signal-based report carries reset intent, such as \fBfapolicyd-cli --reset-metrics\fP. Plain \fBfapolicyd-cli --check-status\fP and \fBfapolicyd-cli --check-metrics\fP reports do not reset counters. Signal-based reports do not reset counters when this option is set to \fBauto\fP, and interval timer reports do not reset counters when it is set to \fBmanual\fP. Set this option to \fBnever\fP when continuously growing counters are required, and to \fBauto\fP or \fBmanual\fP when reports should atomically snapshot the current metrics and start the next reporting interval with fresh counters. Rule hit counters are naturally scoped to the active ruleset generation and reset when a new ruleset is loaded; metric resets also clear them after reporting so the existing rules can be tested from a fresh counter window. In all reset modes, configuration and state identity values such as cache sizes, queue size, trust database size, integrity mode, permissive mode, watched mounts, and ruleset generation are not reset. .TP .B timing_collection This option controls whether privileged manual decision timing windows are allowed. The default value is \fBoff\fP, which ignores timing start and stop requests. Set it to \fBmanual\fP to allow root to use \fBfapolicyd-cli --timing-start\fP and \fBfapolicyd-cli --timing-stop\fP for bounded diagnostic timing runs. While a run is active, the daemon records fixed aggregate latency metrics for each decision worker and stage. When the run is stopped, the daemon writes \fI/run/fapolicyd/fapolicyd.timing\fP. Normal state reports include only timing control state, not the timing histograms. See \fBfapolicyd.timing\fP(5) for the timing report fields. .SH "DECISION WORKER RESOURCE TUNING" Do not treat .B decision_threads as only a CPU setting. A higher value creates more decision workers, and each worker preallocates its own queue, subject cache, object cache, and subject-slot defer array. The subject defer array is sized from .B subj_cache_size with a fixed minimum, so increasing workers also increases total defer capacity and memory. .PP Increase .B decision_threads when metrics show that decision work is backing up, such as high queue wait, high .B "Inter-thread max queue depth" relative to .BR q_size , or non-zero .BR "Inter-thread queue full count" . After changing it, compare .BR fapolicyd-cli (8) status or metrics reports before and after the restart. Watch queue depth, queue full count, subject and object cache collisions and evictions, subject defer fallbacks, and the reported glibc heap usage. .PP On systems with enough memory, keep .BR q_size , .BR subj_cache_size , and .B obj_cache_size unchanged when adding workers so total queue and cache capacity grows with the available decision parallelism. On memory-constrained systems, reduce those per-worker values only after estimating the total footprint. Reducing cache sizes can keep memory more stable, but it may increase cache misses, collisions, evictions, and subject defer fallbacks. Any sustained non-zero subject defer fallback count means the subject cache and derived defer array are too small for the workload. .SH "RELOAD BEHAVIOR" On .BR SIGHUP , fapolicyd parses .IR fapolicyd.conf , publishes a new immutable decision configuration generation, reloads the filter configuration, and asks the normal rule and trust database owners to reload their state. A decision that has already started keeps using the decision configuration generation it pinned at the beginning of the decision. New decisions use the most recently published generation. .PP The following .IR fapolicyd.conf settings are live after a successful reload: .RS .IP \[bu] 2 \fBpermissive\fP, \fBintegrity\fP, and the lookup-side \fBrpm_sha256_only\fP digest floor are decision-used fields and are published together as one immutable generation. .IP \[bu] \fBnice_val\fP, \fBdo_stat_report\fP, \fBdetailed_report\fP, \fBreset_strategy\fP, and \fBtiming_collection\fP are applied directly by the reload path. .IP \[bu] \fBsyslog_format\fP is used by the next successfully published ruleset. .IP \[bu] \fBtrust\fP and the \fBrpm_sha256_only\fP RPM ingestion filter are used by the next trust database reload. .RE .PP The .IR fapolicyd-filter.conf file is also reloaded on .BR SIGHUP before the trust database reload is requested. The filter is consulted when trust source entries are imported or rebuilt; it is not consulted directly by normal access decisions. .PP The following settings require a daemon restart because they are consumed when long-lived runtime objects are created: .RS .IP \[bu] 2 \fBq_size\fP, \fBdecision_threads\fP, \fBsubj_cache_size\fP, and \fBobj_cache_size\fP size the per-worker event queues, worker count, caches, and subject defer arrays. .IP \[bu] \fBuid\fP and \fBgid\fP control the daemon identity selected during startup. .IP \[bu] \fBwatch_fs\fP, \fBignore_mounts\fP, and \fBallow_filesystem_mark\fP determine the fanotify mark set installed during startup. .IP \[bu] \fBdb_max_size\fP and the special \fBauto\fP database sizing mode determine the LMDB map size policy used when the trust database is opened or rebuilt. Changing this setting in the file does not replace the active database sizing policy until restart. .IP \[bu] \fBreport_interval\fP starts the interval report timer used by the decision thread. .RE .SS SECURITY CONSIDERATIONS FOR ignore_mounts Ignoring a mount removes fanotify visibility for that tree. fapolicyd will .B not evaluate reads/opens that occur on the ignored mount, which reduces load but creates blind spots in policy enforcement. .IP \[bu] 2 \fBInterpreters and plugins:\fR Even with \fBnoexec\fR, trusted interpreters (shell, Python, Java, Node.js, etc.) and applications that load plugins, bytecode, or data\-driven modules may read and act on files from the ignored mount. Those accesses bypass fapolicyd because no fanotify mark is placed there. .IP \[bu] \fBPolicy blind spots:\fR Content copied into the ignored tree is not evaluated while it resides there. Risk may surface only after the content moves to a monitored location. .IP \[bu] \fBCoverage of system paths:\fR The root filesystem \fB/\fR is always monitored so core paths (e.g., \fI/usr\fR) remain protected. Do not rely on \fBignore_mounts\fR to work around denials for native ELF binaries; it is a performance control, not a permissive toggle. .PP Before adding entries to \fBignore_mounts\fR, administrators should: .RS 4 .IP \[bu] 2 Ensure each mount is truly \fIdata\-only\fR and is mounted with \fBnoexec\fR. .IP \[bu] Run the advisory check: .nf .B fapolicyd-cli \-\-check-ignore_mounts[=MOUNT] .fi to verify the mount exists, confirm \fBnoexec\fR, and scan for executable regular files, ELF/shared objects, archives/JARs/ZIPs, bytecode caches, plugin/runtime directories, and files matching the \fB%languages\fR macro. The command reports category counts and returns a non\-zero status when potentially executable content is detected. .IP \[bu] Reevaluate after workload changes; caches and logging trees evolve over time. .RE .PP Matching is by mount point path as shown in .BR /proc/mounts ; trailing slashes are normalized. Bind/overlay/NFS/FUSE mounts are matched by their mount point path (not device identifiers). When \fBallow_filesystem_mark=1\fR is set together with \fBignore_mounts\fR, the daemon refuses the configuration to avoid conflicting semantics. .SH "SEE ALSO" .BR fapolicyd (8), .BR fapolicyd-cli (8), .BR fapolicyd.state (5), .BR fapolicyd.metrics (5), .BR fapolicyd.timing (5), and .BR fapolicyd.rules (5). .SH AUTHOR Steve Grubb fapolicyd-2.0/doc/fapolicyd.metrics.5000066400000000000000000000241141524071644000176040ustar00rootroot00000000000000.TH "FAPOLICYD.METRICS" "5" "May 2026" "Red Hat" "File Formats" .SH NAME fapolicyd.metrics \- fapolicyd runtime metrics report file .SH DESCRIPTION The .I /run/fapolicyd/fapolicyd.metrics file contains the most recent daemon metrics report requested through .BR fapolicyd-cli\ --check-metrics , .BR fapolicyd-cli\ --reset-metrics , or periodic interval reporting. .PP The metrics report answers where runtime hot paths and cache effects are. The state report, written to .IR /run/fapolicyd/fapolicyd.state , answers whether the daemon is healthy and configured as expected. Each field is printed as a .I name: value line. Section headers end in a colon. Worker-specific drill-down fields are indented by two spaces under the related aggregate section. .SH HEADER .TP .B Last metrics reset The wall-clock time of the last successful metrics reset, or .B never when metrics have not been reset since daemon start. Reset reports show the counter window that is about to be reset; later metrics reports show the new reset time. .TP .B Config generation The active decision configuration generation at report time. The counters may span earlier generations when configuration was reloaded after the last metrics reset. .TP .B Ruleset generation The active ruleset generation that the counters and rule hit table apply to. .TP .B Trust database generation The active trust database generation used by new decision reads at report time. This is a daemon-runtime generation that starts at 1 during startup. The counters may span earlier trust database generations when reloads occurred after the last metrics reset. .TP .B LMDB environment generation The active physical LMDB environment generation at report time. This increments only when fapolicyd closes the live environment and publishes a validated replacement environment for compaction or high-water reset. It complements the trust database generation: trust generations publish logical content inside an environment, while LMDB environment generations identify the storage file currently backing those logical generations. .TP .B Trust database entries The number of records in the active trust database generation at report time. .SH Decision outcomes .TP .B Allowed accesses The number of policy decisions that allowed access. .TP .B Denied accesses The number of policy decisions that denied access. .TP .B Allowed by rule The number of allow decisions produced by a matching rule. .TP .B Allowed by fallthrough The number of allow decisions produced when no rule had an opinion and the daemon used the default allow behavior. Detailed fallthrough dimensions are printed when this value is non-zero. .SH Inter-thread queue & defer activity .TP .B Inter-thread current queue depth The number of fanotify permission events currently waiting in internal worker queues. .TP .B Inter-thread max queue depth The highest per-worker internal event queue depth observed since the last metrics reset. .TP .B Inter-thread queue full count The number of failed enqueue attempts across all worker queues since the last metrics reset. .TP .B Inter-thread oldest queued age The oldest currently queued event age across all worker queues. .TP .B Decision worker N current queue depth The number of fanotify permission events currently waiting in that worker's queue. .TP .B Decision worker N max queue depth The highest queue depth observed for that worker since the last metrics reset. .TP .B Decision worker N queue full count The number of failed enqueue attempts for that worker since the last metrics reset. .TP .B Decision worker N oldest queued age The age of the oldest event currently waiting in that worker's queue. The value is formatted with a human-readable unit such as .B ms or .BR s . .TP .B Subject deferred events The cumulative number of fanotify permission events deferred since the last metrics reset because another process was still building subject pattern state in the same cache slot. .TP .B Subject defer max depth The highest number of concurrently deferred subject events observed since the last metrics reset. .TP .B Subject defer fallbacks The cumulative number of defer-array-full fallbacks since the last metrics reset. This field also appears in the state report because non-zero values are health indicators. .TP .B Decision worker N Subject deferred events The number of subject-slot deferrals owned by one decision worker since the last metrics reset. .TP .B Decision worker N Subject defer max depth The highest number of concurrently deferred subject events observed in one decision worker since the last metrics reset. .TP .B Decision worker N Subject defer fallbacks The number of defer-array-full fallbacks in one decision worker since the last metrics reset. .TP .B Decision worker N Subject defer current depth The number of subject events currently parked in one decision worker's defer array. .TP .B Decision worker N Subject defer oldest age The age of the oldest currently deferred event in one decision worker's defer array. .SH Subject cache effectiveness .TP .B Subject hits The number of subject cache hits. .TP .B Subject misses The number of subject cache misses. .TP .B Subject collisions The number of populated subject cache slots whose full process identity did not match the current event and therefore had to be evicted before reuse. .TP .B Subject evictions The number of subject cache evictions and the eviction percentage relative to subject cache hits. .TP .B Early subject cache evictions The number of subject cache entries evicted before process startup state was complete. This field also appears in the state report because non-zero values are health indicators. .TP .B Subject BUILDING tracer evictions The number of BUILDING subject cache entries evicted because the owning process was traced and could hold the slot indefinitely. This field also appears in the state report because non-zero values are health indicators. .TP .B Subject BUILDING stale evictions The number of BUILDING subject cache entries evicted because their startup state stayed incomplete past the bounded stale window. This field also appears in the state report because non-zero values are health indicators. .TP .B Decision worker N Subject hits The number of subject cache hits in one decision worker. .TP .B Decision worker N Subject misses The number of subject cache misses in one decision worker. .TP .B Decision worker N Subject collisions The number of subject cache collisions in one decision worker. .TP .B Decision worker N Subject evictions The number of subject cache evictions in one decision worker. .TP .B Decision worker N Early subject cache evictions The number of early subject cache evictions in one decision worker. .TP .B Decision worker N Subject BUILDING tracer evictions The number of traced BUILDING subject evictions in one decision worker. .TP .B Decision worker N Subject BUILDING stale evictions The number of stale BUILDING subject evictions in one decision worker. .SH Object cache effectiveness .TP .B Object hits The number of object cache hits. .TP .B Object misses The number of object cache misses. .TP .B Object collisions The number of populated object cache slots whose full file identity did not match the current event and therefore had to be evicted before reuse. .TP .B Object evictions The number of object cache evictions and the eviction percentage relative to object cache hits. .TP .B Decision worker N Object hits The number of object cache hits in one decision worker. .TP .B Decision worker N Object misses The number of object cache misses in one decision worker. .TP .B Decision worker N Object collisions The number of object cache collisions in one decision worker. .TP .B Decision worker N Object evictions The number of object cache evictions in one decision worker. .SH Trust database lookups .TP .B Trust DB lookups The number of trust database lookup attempts since the last metrics reset. .TP .B Trust DB reader slots full The number of lookup attempts that could not open an LMDB read transaction because all LMDB reader slots were in use. .PP Trust database lookup latency is reported by .BR fapolicyd.timing (5) only while manual timing collection is armed, so normal metrics collection does not add per-lookup clock reads. .SH Rule hit counts .TP .B Hits/rule One line per configured rule in rule order. The line includes the one-based rule number, hit count, and rule text. Rule hit counters are naturally scoped to the active ruleset generation and start at zero when a new ruleset is loaded. A metrics reset also clears the counters after reporting them, which allows focused tests against the currently loaded rules without forcing a rule reload. .SH Subject attribute lookups .TP .B Subject attr One line is printed for each counted subject attribute in the form .IR name " requests=" count " lookups=" count . The .B requests count increments when policy evaluation or syslog formatting asks for the attribute. The .B lookups count increments only when that attribute was absent from the event subject cache and fapolicyd had to compute or fetch it. .SH Object attribute lookups .TP .B Object attr One line is printed for each counted object attribute in the form .IR name " requests=" count " lookups=" count . The .B requests count increments when policy evaluation or syslog formatting asks for the attribute. The .B lookups count increments only when that attribute was absent from the event object cache and fapolicyd had to compute or fetch it. .SH NOTES Metrics resets affect counters in this report. Static configuration, current utilization snapshots, watched mounts, and health indicators are reported in .BR fapolicyd.state (5) and are not reset by .BR fapolicyd-cli\ --reset-metrics . .SH FILES .TP .I /run/fapolicyd/fapolicyd.metrics Runtime metrics report file. .TP .I /run/fapolicyd/fapolicyd.state Runtime state report file. .TP .I /run/fapolicyd/fapolicyd.timing Manual decision timing report written when a privileged stop request ends an armed timing run, when a stop request finds timing unarmed, or when timing collection stops to avoid counter overflow. .SH SEE ALSO .BR fapolicyd (8), .BR fapolicyd-cli (8), .BR fapolicyd.conf (5), .BR fapolicyd.rules (5), .BR fapolicyd.state (5), .BR fapolicyd.timing (5), and .BR fapolicyd.trust (5). fapolicyd-2.0/doc/fapolicyd.rules.5000066400000000000000000000436401524071644000172750ustar00rootroot00000000000000.TH FAPOLICYD.RULES: "5" "June 2022" "Red Hat" "System Administration Utilities" .SH NAME compiled.rules \- compiled fapolicyd rules to determine access rights fapolicyd.rules \- deprecated fapolicyd rules to determine access rights .SH DESCRIPTION \fBcompiled.rules\fP is a file that is compiled by .B fagenrules which contains the rules that \fBfapolicyd\fP uses to make decisions about access rights. The rules follow a simple format of: .nf .B decision perm subject : object .fi They are evaluated from top to bottom with the first rule to match being used for the access control decision. The colon is mandatory to separate subject and object since they share keywords. .SS Decision The decision is either .IR allow ", " deny ", " allow_audit ", " deny_audit ", " allow_syslog ", "deny_syslog ", " allow_log ", or " deny_log ". If the rule triggers, this is the access decision that fapolicyd will tell the kernel. If the decision is one of the audit variety, then the decision will trigger a FANOTIFY audit event with all relevant information. .B You must have at least one audit rule loaded to generate an audit event. If the decision is one of the syslog variety, then the decision will trigger writing an event into syslog. If the decision is of one the log variety, then it will create an audit event and a syslog event. Regardless of the notification, any rule with a deny in the keyword will deny access and any with an allow in the keyword will allow access. .SS Perm Perm describes what kind permission is being asked for. The permission is either .IR open ", " execute ", or " any ". If none are given, then open is assumed. .SS Subject The subject is the process that is performing actions on system resources. The fields in the rule that describe the subject are written in a name=value format. There can be one or more subject fields. Each field is and'ed with others to decide if a rule triggers. The name values can be any of the following: .RS .TP 12 .B all This matches against any subject. When used, this must be the only subject in the rule. .TP .B auid This is the login uid that the audit system assigns users when they log in to the system. When the audit login uid is unset, as is common for system daemons and kernel threads, the kernel value is -1 and fapolicyd reports it as 4294967295. The auid attribute is unsigned, so rules must use \fBauid=4294967295\fR; \fBauid=-1\fR is invalid. The given value may otherwise be numeric or the account name. .TP .B uid This is the user id that the program is running under. The given value may be numeric or the account name. .TP .B gid This is the group id that the program is running under. The given value may be numeric or the group name. .TP .B sessionid This is the numeric session id that the audit system assigns to users when they log in. Daemons have a value of -1. .TP .B pid This is the numeric process id that a program has. .TP .B ppid This is the numeric process id of the program's parent. Note that programs that are orphaned or started directly from systemd have a ppid value of 1. Kernel threads have a ppid value of 2. .TP .B trust This is a boolean describing whether it is required for the subject to be in the trust database or not. A value of 1 means its required while 0 means its not. Trust checking is extended by the integrity setting in fapolicyd.conf. When trust is used on the subject, it could be a daemon. If that daemon gets updated on disk, the trustdb will be updated to the new SHA256 hash. If the integrity setting is not none, the running daemon is not likely to be trusted unless it gets restarted. The default rules are not written in a way that this would happen. But this needs to be highlighted as it may not be obvious when writing a new rule. .TP .B comm This is the shortened command name. When an interpreter starts a program, it usually renames the program to the script rather than the interpreter. .TP .B exe This is the full path to the executable. Values are exact paths unless they use the explicit .B glob: pattern prefix. See .B PATH GLOBBING below. You may also use the special keyword \fBuntrusted\fP to match on the subject not being listed in the rpm database. .TP .B dir If you wish to match a directory, then use this by giving the full path to the directory. Its recommended to end with the / to ensure it matches a directory. There are 3 keywords that \fIdir\fP supports: \fBexecdirs\fP, \fBsystemdirs\fP, \fBuntrusted\fP. .RS .TP 12 .B execdirs The \fIexecdirs\fP option will match against the following list of directories: .RS .TP 12 /usr/ /bin/ /sbin/ /lib/ /lib64/ /usr/libexec/ .RE .TP 12 .B systemdirs The \fIsystemdirs\fP option will match against the same list as \fIexecdirs\fP but also includes /etc/. .TP 12 .B untrusted The \fIuntrusted\fP option will look up the current executable's full path in the rpm database to see if the executable is known to the system. The rule will trigger if the file in question is not in the trust database. This option is .B deprecated in favor of using object \fBtrust\fP with execute permission when writing rules. Rules that use \fBdir=untrusted\fP emit a deprecation warning when parsed, and this compatibility option will be removed in a future release. For compatibility with older policies, a subject \fBdir=untrusted\fP rule paired with a path object at the same rule field position will not match a trusted object. This subject/object cross-reference is obsolete; use explicit object trust rules for new policy. .RE .TP .B ftype This option takes the mime type of a file as an argument. Mime is determined based on magic patterns files. Fapolicyd uses two precompiled magic files /usr/share/fapolicyd/fapolicyd-magic.mgc and /usr/share/misc/magic.mgc. To compile a magic file run the command \fBfile -C -m \fP. If you wish to check the mime type of a file while writing rules, run the following command: .nf .B fapolicyd-cli \-\-ftype /path-to-file .fi .TP .B pattern There are various ways that an attacker may try to execute code that may reveal itself in the pattern of file accesses made during program startup. This rule can take one of several options depending on which access patterns is wished to be blocked. Fapolicyd is able to detect these different access patterns and provide the access decision as soon as it identifies the pattern. The pattern type can be any of: .RS .TP 12 .B normal This matches against any ELF program that is dynamically linked. .TP .B ld_so This matches against access patterns that indicate that the program is being started directly by the runtime linker. .TP .B ld_preload This matches against access patterns that indicate that the program is being started with either LD_PRELOAD or LD_AUDIT present in the environment. Note that even without this rule, you have protection against LD_PRELOAD of unknown binaries when the rules are written such that trust is used to determine if a library should be opened. In that case, the preloaded library would be denied but the application will still execute. This rule makes it so that even trusted libraries can be denied and the application will not execute. .TP .B static This matches against ELF files that are statically linked. .RE .RE .SS Object The object is the file that the subject is interacting with. The fields in the rule that describe the object are written in a name=value format. There can be one or more object fields. Each field is and'ed with others to decide if a rule triggers. The name values can be any of the following: .RS .TP 12 .B all This matches against any object. When used, this must be the only object in the rule. .TP .B path This is the full path to the file that will be accessed. Values are exact paths unless they use the explicit .B glob: pattern prefix. See .B PATH GLOBBING below. You may also use the special keyword \fBuntrusted\fP to match on the object not being listed in the rpm database. .TP .B dir If you wish to match on access to any file in a directory, then use this by giving the full path to the directory. Its recommended to end with the / to ensure it matches a directory. There are 3 keywords that \fIdir\fP supports: \fBexecdirs\fP, \fBsystemdirs\fP, \fBuntrusted\fP. See the \fBdir\fP option under Subject for an explanation of these keywords. .TP .B device This option will match against the device that the file being accessed resides on. To use it, start with /dev/ and add the target device name. .TP .B ftype This option matches against the mime type of the file being accessed. See \fBftype\fP under Subject for more information on determining the mime type. .TP .B trust This is a boolean describing whether it is required for the object to be in the trust database or not. A value of 1 means its required while 0 means its not. Trust checking is extended by the integrity setting in fapolicyd.conf. .TP .B FILE_HASH (the legacy keyword .B SHA256HASH is still accepted) This option matches against the hash of the file being accessed. The accepted hash length follows the trust source, so RPM entries built with SHA512 store SHA512 digests while other sources may continue to provide SHA256. The hash in the rules should be all lowercase letters and do NOT start with 0x. Lowercase is the default output of sha256sum and sha512sum. The SHA256HASH keyword remains for compatibility but is deprecated; prefer FILE_HASH for new rules. .RE .SH QUOTED VALUES Rule fields are separated by spaces. Enclose a complete string value in double quotes when it contains a space. The surrounding quotes are syntax and are not part of the value used for matching. This applies consistently to subject .B exe , subject and object .B dir , and object .BR path : .nf .B allow perm=execute all : dir="/tmp/first last/" .B allow perm=execute exe="/tmp/first last/tool" : all .B allow perm=execute all : path="/tmp/first last/tool" .fi Double quotes can also be used in named string-set definitions and in each comma-separated string value. Within a quoted value, a backslash before a double quote or another backslash represents that character. Other backslashes are preserved, so glob patterns retain their normal backslash behavior. Percent escapes are not interpreted in rules. For example, .B dir="/tmp/first%20last/" matches a literal percent sign followed by \fB20\fP, not a space. A backslash before a space and shell-style single quotes are not rule-file quoting forms; double quotes are the only supported way to include a literal space in a rule value. .SH PATH GLOBBING Rule values use exact matching by default. Prefix a value with .B glob: to request pattern matching. This explicit marker distinguishes exact paths from patterns, so path names containing literal .BR * , .BR ? , or .B [ characters keep their historical exact-match meaning. For example, .B path=/tmp/name[1] matches only that literal path, while .B path=glob:/tmp/name[1] uses .B [1] as a bracket expression and matches .BR /tmp/name1 . The .B glob: prefix is supported only by the subject .B exe and object .B path attributes, and the pattern after the prefix must be an absolute path. The .B dir attribute remains a literal directory prefix; use it instead of a glob when a rule applies to a complete subtree rooted at a fixed directory. An unmarked .B dir value containing metacharacters remains literal. A .B glob: value assigned to .BR dir , or to any attribute other than .B exe and .BR path , is a rule error, including when the value comes from a named set. Use subject .B exe or object .B path when glob matching is required. The pattern following .B glob: uses .BR fnmatch (3) syntax. A .B * matches zero or more characters, .B ? matches one character, and bracket expressions such as .B [0-9] match one character from the expression. Matching is case-sensitive and must cover the complete path. Wildcards do not match a slash, so each variable path component must be written explicitly. A wildcard does not match a leading period in a path component unless the period appears explicitly in the pattern. Backslash quotes a metacharacter. Recursive .B ** globstar matching is not supported. When a glob value contains a space, quote the complete value, including the .B glob: prefix. For example: .nf .B allow perm=execute all : path="glob:/tmp/first last/*" .B allow perm=execute exe="glob:/tmp/first last/*" : all .fi The quotes are removed before glob validation and matching. The .B dir attribute remains a literal prefix and does not accept a quoted or unquoted .B glob: value. Bracket ranges such as .B [a-z] and named character classes such as .B [[:alpha:]] use the locale active in the daemon. Avoid ranges and named classes when policy must produce identical matches in every locale; use exact alternatives or enumerate the intended characters instead. For example, .B glob:/home/*/bin/tool matches .B /home/alice/bin/tool but not .B /home/alice/project/bin/tool or .BR /home/.admin/bin/tool . Each pattern in a comma-separated attribute value or named string set needs its own .B glob: prefix. Exact paths and patterns may be mixed in the same set. Rule order is unchanged: rules are evaluated from top to bottom, and the first complete rule match determines the decision. The concrete object .B path is obtained from the event file descriptor through .IR /proc/self/fd/ , and the subject .B exe path is obtained through .IR /proc/PID/exe . Symbolic links have therefore already been followed, so a rule normally matches the reported target path rather than the symbolic link name used by the application. Hard links and mount aliases can expose different concrete paths for the same inode; policy that must cover those aliases needs an exact value or pattern for each reported path. Globbing selects paths; it does not expand the pattern, alter the path reported in logs, or make matching files trusted. Trust database entries continue to use concrete paths. An allow rule for a user-writable location should normally pair an object .B path glob with object .B trust=1 or an explicit .B FILE_HASH constraint. A subject .B exe glob in a user-writable location should normally require subject .BR trust=1 . Otherwise, any file whose name fits the pattern can receive the allow decision. Prefer exact paths, named sets of exact paths, or .B dir when they express the policy because exact and prefix matching are less expensive than glob evaluation. .nf .B deny_audit perm=execute all : path=glob:/opt/vendor/product-*/bin/tool .B allow perm=execute all : path=glob:/home/*/bin/tool trust=1 .fi .SH SETS Set is a named group of values of the same type. Fapolicyd internally distinguishes between INT and STRING set types. You can define your own set and use it as a value for a specific rule attribute. The definition is in key=value syntax and starts with a set name. The set name has to start with '%' and the rest is alphanumeric or '_'. The value is a comma separated list. The set type is inherited from the first item in the list. If that can be turned into number then whole list is expected to carry numbers. One can use these sets as a value for subject and object attributes. It is also possible to use a plain list as an attribute value without previous definition. The assigned set has to match the attribute type. It is not possible set groups for TRUST and PATTERN attributes. .SS SETS EXAMPLES .nf .B # definition .B # string set .B %python=/usr/bin/python2.7,/usr/bin/python3.6 .B allow exe=%python : all trust=1 .B # .B # definition .B # number set .B %uuids=0,1000 .B allow uid=%uuids : all .fi .SS LANGUAGE SET EXTENSIONS The installed \fB%languages\fR set identifies MIME types that policy treats as language or interpreter content. A package may add a MIME type without modifying the distribution-owned definition by placing a naturally ordered macro fragment after \fI10-languages.rules\fR and before policy rules. For example, \fI11-vendor-languages.rules\fR can contain: .nf .B %languages+=application/x-vendor-script .fi The \fB+=\fR form is available only for \fB%languages\fR. It requires the base set to have been defined already, does not replace it, and treats an existing MIME type as a no-op. Run \fBfagenrules\fR after adding or removing a fragment. The added value must also be a MIME type emitted by fapolicyd's file type detector or the system magic database; adding it to the set alone does not add file type detection. .SH NOTES When writing rules, you should keep them focused to one goal and store them in one file. These rule files are kept in the /etc/fapolicyd/rules.d directory. During daemon startup, .B fagenrules will run and compile all these component files into one master file, compiled.rules. See the .B fagenrules man page for more information. When you are writing a rule for the execute permission, remember that the file to be executed is an .B object. For example, you type ssh into the shell. The shell calls execve on /usr/bin/ssh. At that instant in time, ssh is the object that bash is working on. However, if you are blocking execution .I from a specific program, then you would normally state the program on the subject side and use .I all for the object side. If you are writing rules that use patterns, just select .I any as the permission to be clear that this applies to anything. In reality, pattern matching ignores the permission but the suggestion is for documentation purposes. Some interpreters do not immediately read all lines of input. Rather, they read content as needed until they get to end of file. This means that if they do stuff like networking or sleeping or anything that takes time, someone with the privileges to modify the file can add to it after the file's integrity has been checked. This is not unique to fapolicyd, it's simply how things work. Make sure that trusted file permissions are not excessive so that no unexpected file content modifications can occur. .SH EXAMPLES The following rules illustrate the rule syntax. .nf .B deny_audit perm=open exe=/usr/bin/wget : dir=/tmp .B allow perm=open exe=/usr/bin/python3.7 : ftype=text/x-python trust=1 .B deny_audit perm=any pattern ld_so : all .B deny perm=any all : all .fi .SH "SEE ALSO" .BR fapolicyd (8), .B fagenrules (8), .BR fapolicyd-cli (8), .BR fnmatch (3), and .BR fapolicyd.conf (5) .SH AUTHOR Steve Grubb fapolicyd-2.0/doc/fapolicyd.state.5000066400000000000000000000311141524071644000172540ustar00rootroot00000000000000.TH "FAPOLICYD.STATE" "5" "May 2026" "Red Hat" "File Formats" .SH NAME fapolicyd.state \- fapolicyd runtime state report file .SH DESCRIPTION The .I /run/fapolicyd/fapolicyd.state file contains the most recent daemon state report requested through .BR fapolicyd-cli\ --check-status or periodic interval reporting. .PP The state report answers whether the daemon is healthy and configured as expected. Runtime counters, rule hit counts, cache effectiveness metrics, attribute lookup tables, and queue/defer activity are reported in .IR /run/fapolicyd/fapolicyd.metrics . Each field is printed as a .I name: value line. Section headers end in a colon. Worker-specific health and queue drill-down fields are indented by two spaces under the related section. .SH STATE REPORT .SS Operating mode .TP .B Permissive Whether the daemon is running in permissive mode. In permissive mode, policy denials are reported as denied decisions, but the response sent to the kernel allows access. .TP .B Integrity The configured file integrity mode used for trust checks. .TP .B reset_strategy The active metrics reset strategy: .B never keeps metrics growing for the daemon lifetime, .B auto resets timer-generated metrics reports, and .B manual allows privileged signal-generated reset requests such as .BR fapolicyd-cli\ --reset-metrics . .TP .B Config generation The immutable decision configuration generation currently published by the daemon. Each successful reload publishes a new generation for decision-used fields such as permissive mode and integrity mode, so one decision does not mix values from different reloads. .TP .B Ruleset generation The current in-memory ruleset generation. This value increments each time a fully validated ruleset is published by the daemon. .TP .B Trust database generation The currently published trust database generation used by new decision reads. This is a daemon-runtime generation that starts at 1 during startup. The effective-since timestamp is the time the generation was published. .TP .B LMDB environment generation The physical LMDB environment generation currently opened by the daemon. This starts at 1 during startup and increments when fapolicyd publishes a rebuilt LMDB environment during controlled compaction. A trust database generation is a logical content epoch inside an environment; an LMDB environment generation is the storage epoch containing the LMDB page map, free list, and high-water state. .TP .B Trust database entries The number of records in the currently published trust database generation. .TP .B Timing collection mode The configured timing collection control mode. .TP .B Timing collection armed Whether a manual timing run is currently active. .TP .B Timing collection last start time The wall-clock time of the last successful timing start request, or .B never when timing has not been started. .TP .B Timing collection last stop time The wall-clock time of the last successful timing stop request, or .B never when timing has not been stopped. .SS Headline activity .TP .B Allowed accesses The number of policy decisions that allowed access. .TP .B Denied accesses The number of policy decisions that denied access. In permissive mode these decisions are still counted as denials even though the daemon permits kernel access. .SS Resource configuration .TP .B Daemon PID The process ID of the daemon process that wrote the report. .TP .B CPU cores The number of online processor cores reported by the system. .TP .B decision_threads The configured decision worker count from .BR fapolicyd.conf . .TP .B Per worker q_size The configured size of each decision worker's internal event queue. .TP .B Per worker Subject defer array size The number of preallocated entries available in each decision worker's subject-slot defer array. .TP .B Per worker Subject cache size The configured number of entries in each decision worker's subject cache. .TP .B Per worker Object cache size The configured number of entries in each decision worker's object cache. .TP .B Trust database max pages The current maximum LMDB map page count for the trust database environment. This reflects the map size currently opened by the daemon, including any automatic resize applied for reload headroom. .TP .B Trust database max readers The configured LMDB reader-slot limit. This is sized for .B decision_threads plus maintenance readers. .SS Resource utilization .TP .B Trust database pages in use The number and percentage of LMDB pages currently used by the active trust database contents. This is the active named-database footprint, not the LMDB environment high-water mark. .TP .B Trust database allocated high-water pages The number and percentage of LMDB map pages that have been allocated in the environment. This follows LMDB's file high-water mark and can be higher than the active trust database footprint after drop/rebuild reloads, because LMDB cannot reuse pages until transactions and freelist metadata allow it. When .B db_max_size is set to .B auto , fapolicyd uses this value as reload-headroom pressure while active pages show the steady-state trust database size. .TP .B Retired trust database generations The number of old trust generations waiting for readers to drain before their named databases can be reclaimed. .TP .B Oldest retired trust database generation age The age in seconds of the oldest retired trust generation still waiting for safe reclamation. .TP .B Max trust database generation reclaim delay The longest observed delay in seconds between retiring an old trust generation and reclaiming its named database. .TP .B Trust database resize recommended Whether the current LMDB sizing state suggests increasing a manually configured .B db_max_size . This field is printed only when .B db_max_size is a numeric value. It is omitted when .B db_max_size is set to .B auto because auto mode grows the map itself before rebuilds. .TP .B Trust database resize reason Short explanation for the manual resize recommendation. .TP .B Trust database resize target The recommended minimum .B db_max_size in MiB. This field is printed only when manual resize is recommended. .TP .B Trust database compaction recommended Whether the current LMDB sizing state suggests running controlled trust database compaction. Compaction is recommended when the allocated LMDB high-water mark is much larger than the active trust database footprint and no retired trust generations are pinned. .TP .B Trust database compaction reason Short explanation for the recommendation. If retired generations are pinned, the recommendation is .B no because readers must drain before the environment can be safely replaced. .TP .B Trust database compaction target The current map-size target in MiB for a compact rebuilt environment. This field is printed when compaction is recommended. .TP .B Subject slots in use The number and percentage of subject cache slots currently occupied. .TP .B Object slots in use The number and percentage of object cache slots currently occupied. .TP .B glibc arena (total memory) is The current total glibc heap arena size in KiB, followed by the value from the previous report. This field is printed only when the daemon is built with .BR mallinfo2 (3) support. .TP .B glibc uordblks (in use memory) is The current allocated heap memory in KiB, followed by the value from the previous report. This field is printed only when .BR mallinfo2 (3) support is available. .TP .B glibc fordblks (total free space) is The current free heap memory in KiB, followed by the value from the previous report. This field is printed only when .BR mallinfo2 (3) support is available. .SS Health indicators Any non-zero counter in this section warrants investigation. .TP .B Kernel queue overflow The number of .B FAN_Q_OVERFLOW events reported by the kernel. A non-zero value means kernel fanotify events were lost before the daemon could process them. .TP .B Filesystem errors The number of .B FAN_FS_ERROR events reported by the kernel. These are filesystem health events, not policy decisions. .TP .B Filesystem error last status Parser status for the most recent .B FAN_FS_ERROR event: .B none , .B ok , .B missing_error_record , or .B malformed . .TP .B Filesystem error last seen The wall-clock time of the most recent .B FAN_FS_ERROR event, or .B never when no filesystem error has been reported. .TP .B Filesystem error last errno The errno-style error code from the most recent parseable filesystem error event. .TP .B Filesystem error last suppressed count The kernel-reported count of additional filesystem errors suppressed behind the most recent error notification. .TP .B Reply errors The number of failed or short writes when sending fanotify permission responses back to the kernel. .TP .B Decision worker N health Compact health line for one decision worker. It includes a derived .BR state , heartbeat age, and current event age. The state is .BR idle , .BR busy , or .BR stalled . An idle worker reports .BR current_event=idle . Queue depth and oldest queued event age are reported by the per-worker queue entries that follow. .TP .B Decision worker N current queue depth The number of fanotify permission events currently waiting in that worker's queue. .TP .B Decision worker N max queue depth The highest queue depth observed for that worker since the last metrics reset. .TP .B Decision worker N queue full count The number of failed enqueue attempts for that worker since the last metrics reset. .TP .B Decision worker N oldest queued age The age of the oldest event currently waiting in that worker's queue. The value is formatted with a human-readable unit such as .B ms or .BR s . .TP .B Subject defer fallbacks The number of times the defer array was full and fapolicyd fell back to the historical subject cache eviction behavior. .TP .B Early subject cache evictions The number of subject cache entries evicted before process startup state was complete. .TP .B Subject BUILDING tracer evictions The number of BUILDING subject cache entries evicted because the owning process was traced and could hold the slot indefinitely. .TP .B Subject BUILDING stale evictions The number of BUILDING subject cache entries evicted because their startup state stayed incomplete past the bounded stale window. .TP .B Subject defer oldest age The age of the oldest currently deferred subject event, formatted with a human-readable unit such as .B ms or .BR s . .TP .B Failure action queue_full (observe) Number of times the internal userspace event queue was full. .TP .B Failure action kernel_queue_overflow (observe) Number of kernel fanotify queue overflow events. .TP .B Failure action worker_stall (terminate) Number of decision worker stall detections. .TP .B Failure action rule_reload_failure (observe) Number of rule reload failures. A failed transactional reload preserves the previous published policy when one exists. .TP .B Failure action trust_reload_failure (observe) Number of trust database reload failures. .TP .B Failure action response_write_failure (observe) Number of failed or incomplete fanotify response writes to the kernel. .TP .B Failure action fanotify_filesystem_error (observe) Number of .B FAN_FS_ERROR filesystem health events reported by the kernel. .SS Watched mounts .TP .B watching mount One line is printed for each mount point currently marked for fanotify monitoring. .SH NOTES .B Allowed accesses , .B Denied accesses , .B Config generation , .B Ruleset generation , .B Trust database generation , .B LMDB environment generation , .B Trust database entries , .B Decision worker N health , .B Decision worker N current queue depth , .B Decision worker N max queue depth , .B Decision worker N queue full count , .B Decision worker N oldest queued age , .B Subject defer fallbacks , .B Early subject cache evictions , .B Subject BUILDING tracer evictions , and .B Subject BUILDING stale evictions intentionally appear in both state and metrics reports. In the state report they provide health and activity context; in the metrics report they describe the current report context and counter window. .PP Metrics resets affect counters in the metrics report. Static configuration, current utilization snapshots, watched mounts, and health indicators are not reset by .BR fapolicyd-cli\ --reset-metrics . .SH FILES .TP .I /run/fapolicyd/fapolicyd.state Runtime state report file. .TP .I /run/fapolicyd/fapolicyd.metrics Runtime metrics report file. .TP .I /run/fapolicyd/fapolicyd.timing Manual decision timing report written when a privileged stop request ends an armed timing run, when a stop request finds timing unarmed, or when timing collection stops to avoid counter overflow. .SH SEE ALSO .BR fapolicyd (8), .BR fapolicyd-cli (8), .BR fapolicyd.conf (5), .BR fapolicyd.metrics (5), .BR fapolicyd.rules (5), .BR fapolicyd.timing (5), and .BR fapolicyd.trust (5). fapolicyd-2.0/doc/fapolicyd.timing.5000066400000000000000000000425631524071644000174350ustar00rootroot00000000000000.TH "FAPOLICYD.TIMING" "5" "May 2026" "Red Hat" "File Formats" .SH NAME fapolicyd.timing \- fapolicyd manual decision timing report .SH DESCRIPTION The .I /run/fapolicyd/fapolicyd.timing file contains one manual decision timing run from .BR fapolicyd (8). The daemon writes this file when a privileged manual timing stop request ends an armed timing run. A stop request while timing is not armed writes a short status report instead of timing data. The daemon may also stop collection and write the report when a timing counter would overflow. .PP Timing collection is disabled by default. Manual timing requests are honored only when the active daemon configuration has .B timing_collection=manual in .BR fapolicyd.conf (5). The command .B fapolicyd-cli --timing-start arms a run, and .B fapolicyd-cli --timing-stop stops the run and prints this report. .B --timer-start and .B --timer-stop are accepted aliases. .PP The daemon stores aggregate counters only. It does not store one timing record per decision. Each decision worker owns local aggregate blocks containing a call count, total time, maximum time, and fixed latency buckets for each stage. The report snapshots and aggregates those blocks. .SH RUN SUMMARY The first lines summarize the run: .TP .B Mode Active timing collection mode. .TP .B Timing run Wall-clock start and stop time. .TP .B Duration Elapsed wall-clock run time in .B H:MM:SS format. .TP .B Workers Number of decision worker timing blocks included in the snapshot. .TP .B Max queue depth Highest internal event queue depth observed during the timing run. The daemon temporarily resets only the max queue depth counter when timing starts and restores the previous high-water mark after timing stops if the previous value was larger. The .B Queueing section repeats this value next to queue wait timing. .TP .B Decisions Number of completed fanotify decisions timed during the run. .TP .B Throughput Timed decisions per second over the wall-clock run duration. This includes time when fapolicyd was armed but no events were waiting to be decided. .TP .B Active decision rate Timed decisions per second using the sum of .B decision:total latency instead of wall-clock duration. With more than one decision worker, this is worker-time, not elapsed wall-clock time. .TP .B Stop reason Printed only when the run stopped automatically because a timing counter would overflow. .SH TL;DR The .B TL;DR section appears immediately after the run summary and before .B Overall decision latency . It surfaces two or three dominant findings drawn from the same data used by the later .B Derived observations section, such as helper dominance, response formatting cost, or queueing health. The detailed observations remain in their original location later in the report. .SH OVERALL DECISION LATENCY The .B Overall decision latency section describes end-to-end decision latency from the point a decision worker starts processing a dequeued fanotify event until the decision path returns after response handling. .PP The section prints average and maximum latency using compact latency units, approximate percentile buckets, and cumulative percentages for useful thresholds such as .B <=50us , .B <=100us , .B <=500us , .B <=1ms , and .B >10ms . Percentiles are bucket approximations, not exact per-decision percentiles. The 95th percentile is a useful tail-latency marker: roughly 95% of observed calls were at or below that bucket, so a high p95 points to a recurring slow path rather than a single worst-case outlier. .PP When high-end tail buckets are observed, the section also prints a compact .B tail line for .B >10ms , .B >25ms , .B >50ms , .B >100ms , and .B >250ms . The line includes both count and percentage so long outliers do not disappear inside a single .B >10ms bucket. .SH QUEUEING The .B Queueing section describes work waiting before a decision worker starts processing an event. It is based on .B time_in_queue:total when enqueue timestamps were observed during the timing run. .TP .B avg wait Average observed time in the internal userspace event queue. .TP .B max wait Longest observed wait in the internal userspace event queue. .TP .B p95 bucket Approximate bucket containing the 95th percentile queue wait. .TP .B total queued time Sum of observed queue wait time. This is useful for spotting producer/consumer imbalance, but it is not decision CPU time. .TP .B max queue depth Highest internal event queue depth observed during the timing run. .PP If no enqueue timestamps were observed, the section prints .B not observed and still reports max queue depth. .SH PER-WORKER TIMING SKEW When more than one decision worker is active, the .B Per-worker timing skew section summarizes routing and latency balance by worker. The aggregate latency sections remain authoritative; this section exists to make uneven worker buckets visible. .TP .B decisions min/max/spread Smallest and largest number of timed decisions handled by any worker, plus the difference between them. .TP .B Worker Decision worker index. .TP .B Decisions Timed decisions handled by that worker. .TP .B Total Sum of that worker's .B decision:total latency. .TP .B Avg Average .B decision:total latency for that worker. .TP .B Max Slowest .B decision:total latency for that worker. .TP .B Queue p95 Approximate 95th percentile queue-wait bucket for that worker. .SH DECISION PHASE TIMING The .B Decision phase timing section shows the main decision-thread phases when they were observed: .PP .RS .nf event_build:total evaluation:total response:total .fi .RE .TP .B Phase Displayed phase name. .TP .B Calls Number of times the phase was measured. .TP .B Calls/Dec Average phase calls per timed decision. .TP .B Total Total measured phase time. .TP .B Avg Average phase call time. .TP .B Max Slowest observed phase call. .TP .B p95 bucket Approximate bucket containing the 95th percentile phase call. .TP .B Notes Short deterministic notes. For example, a response phase dominated by syslog or debug formatting is marked .B syslog/debug-heavy . .PP These phase rows are the best first view for explaining where decision-thread time is being spent. Helper rows are separate because some lazy helper operations can be requested from more than one phase. .SH LAZY HELPER ATTRIBUTION The report introduces helper attribution with: .PP .RS .nf Lazy helper attribution: Helper timings are attributed to the active logical driver: evaluation or response. Combined totals are evaluation + response. .fi .RE .PP The driver table and combined helper table follow this note. .SH LAZY HELPER ATTRIBUTION BY DRIVER The .B Lazy helper attribution by driver section shows helper time split by the logical context that caused it: evaluation or response. .TP .B Helper Short helper path. .TP .B Eval total Total helper time caused while the rule evaluation driver was active. .TP .B Response total Total helper time caused while response, audit metadata, or syslog/debug formatting was active. .TP .B Combined Total helper time from all driver contexts. .TP .B Response % Percentage of combined helper time caused by response-side work. This is useful for deciding whether manual/debug reporting is driving MIME or trust lookup cost. .SH COMBINED LAZY HELPER ATTRIBUTION The .B Combined lazy helper attribution section groups helper-related stages into stable combined paths. For helpers that are split by driver, the combined row is calculated by adding the phase-specific evaluation and response rows together. These rows answer how often a helper was needed and how expensive that helper was when amortized over all decisions. .TP .B Helper path Short combined helper path. .TP .B Calls Number of helper calls measured. .TP .B Calls/Dec Average helper calls per timed decision. .TP .B Total Total measured helper time. .TP .B Avg/call Average measured helper call time. .TP .B Amort/Dec Total helper time divided by all timed decisions. This shows how much a lazy or rare helper contributes to a typical decision in the measured workload. .TP .B Max Slowest observed helper call. .TP .B p95 bucket Approximate bucket containing the 95th percentile helper call. .PP The current helper groups are: .PP .RS .nf mime_detection:total mime_detection:fast_classification mime_detection:gather_elf mime_detection:libmagic_fallback trust_db_lookup:total trust_db_lookup:read trust_db_lookup:lock_wait hash_ima:total hash_sha:total proc_detail_lookup .fi .RE .SH DERIVED OBSERVATIONS The .B Derived observations section prints short deterministic observations when the required data was present in the run. Examples include: .IP \(bu 2 Wall-clock throughput compared with active decision rate, which can show that the workload was mostly idle while timing was armed. .IP \(bu 2 Queue wait and maximum queue depth compared with .B q_size when queue timing and configuration data are available. .IP \(bu 2 Response formatting share when syslog/debug formatting dominates .B response:total . .IP \(bu 2 MIME helper cost and libmagic fallback share. .IP \(bu 2 Rare but expensive integrity work from .B hash_ima or .B hash_sha , reported as percent of decisions, average time when called, and amortized decision cost. .IP \(bu 2 Trust database lock wait compared with trust database read time. .SH STAGE TAIL SUMMARY When hot detailed stage rows have observations above 10ms, the report prints a .B Stage tail summary after the detailed table. Rows are sorted by .B >10ms count, limited to the top five rows, and extended with any additional row that has a nonzero .B >50ms count. Near-identical parent/child tail rows may be suppressed to keep the section focused. .PP .RS .nf stage:name: >10ms count/percent, >25ms count/percent, >50ms count/percent .fi .RE .PP The detailed table remains the canonical total-time ranking; the tail summary only adds high-end resolution for rows that crossed one of the tail thresholds. Thresholds with zero observations are omitted. .SH DETAILED STAGE TABLE The .B Detailed stage timing table is sorted by total measured time by default. It answers which measured operations consumed the most total time during the run. This is the detailed view and is intentionally kept below the summary and helper attribution sections. .PP The .B decision:total row is included when observed and participates in the same total-time sort as the other rows. .TP .B Stage Measured operation name. The current stage set is listed in .BR "DECISION FLOW" . .TP .B Calls Number of times the operation was measured. .TP .B Calls/Dec Average measured calls per timed decision. .TP .B Total Total measured time for the stage. .TP .B Avg Average measured call time for the stage. .TP .B Max Slowest observed call for the stage. .TP .B p95 bucket Approximate bucket containing the 95th percentile call for the stage. .PP Stages with zero calls are hidden from the table and listed under .B Not observed . If a stage is not observed, no measured call to that operation occurred during the timing window. For example, an absent .B evaluation:trust_db_lookup:total and .B response:trust_db_lookup:total line means no measured trust database lookup occurred from those drivers in that run. .SH DECISION FLOW Stage names use a colon-separated hierarchy: .PP .RS .nf phase:operation:child .fi .RE .PP Rows ending in .B :total are parent scopes. Child operation timings are useful contributors, but they can be nested, lazy, or shared by more than one code path, so child rows are not expected to add up to the parent total. .PP The main decision-thread view is: .PP .RS .nf decision:total time_in_queue:total event_build:total event_build:cache_flush event_build:proc_fingerprint event_build:fd_stat evaluation:lock_wait evaluation:total evaluation:mime_detection:total evaluation:mime_detection:fast_classification evaluation:mime_detection:gather_elf evaluation:mime_detection:libmagic_fallback evaluation:fd_path_resolution evaluation:proc_detail_lookup evaluation:hash_ima:total evaluation:hash_sha:total evaluation:trust_db_lookup:total evaluation:trust_db_lookup:lock_wait evaluation:trust_db_lookup:read response:total response:syslog_debug_format:total response:mime_detection:total response:mime_detection:fast_classification response:mime_detection:gather_elf response:mime_detection:libmagic_fallback response:trust_db_lookup:total response:trust_db_lookup:lock_wait response:trust_db_lookup:read response:audit_metadata:total response:fanotify_write .fi .RE .PP Lazy helper operations are attributed to the active logical driver when the helper is called. Rule evaluation uses .B evaluation: , response logging and audit response work use .B response: . Helper timing outside those logical drivers is treated as an instrumentation bug and is not reported as a separate phase. .SH STAGES .TP .B decision:total Decision worker latency after a fanotify event has been dequeued, including event build, policy evaluation, optional logging or audit preparation, and response handling. Queue wait time is reported separately. .TP .B time_in_queue:total Time a fanotify event spent in the internal userspace queue between enqueue by the fanotify reader thread and dequeue by a decision worker. Events already in the queue when timing is armed do not have an enqueue timestamp and are not included in this stage. .TP .B event_build:total Construction of the internal event from fanotify metadata. This includes subject and object cache lookups, initial subject and object fingerprints, and some pattern-state work. .TP .B event_build:cache_flush Object cache flush performed on the decision path when the global flush flag is set. .TP .B event_build:proc_fingerprint Initial .I /proc/ stat used to identify the subject process and detect stale subject cache entries. .TP .B event_build:fd_stat Stat of the fanotify file descriptor used to fingerprint the object. .TP .B evaluation:lock_wait Time spent waiting for the rule lock before policy evaluation. .TP .B evaluation:total Rule list evaluation loop. Lazy subject, object, trust, hash, or type lookups caused by rule fields can appear as child operation timings and also contribute to this total. .TP .B evaluation:proc_detail_lookup On-demand subject procfs detail lookups, including .I /proc//status , .I /proc//exe , subject executable type, session id, and auid lookups. These can be driven lazily by rules, syslog/debug formatting, or audit metadata. .TP .B evaluation:fd_path_resolution Path resolution for the object file descriptor. .PP For MIME and trust DB helper rows below, .B * means the driver prefix .B evaluation , .B response . .TP .B *:mime_detection:total Object type and ELF/script detection. .TP .B *:mime_detection:fast_classification In-house classification before libmagic fallback. This includes empty-file, ELF/script, shebang, common magic number, common text format, and device checks. .TP .B *:mime_detection:gather_elf ELF/script header scan performed by .BR gather_elf (). .TP .B *:mime_detection:libmagic_fallback Full libmagic fallback used when the faster type checks cannot classify the object. .TP .B evaluation:hash_ima:total IMA digest collection from the .I security.ima xattr through .BR get_ima_hash (). .TP .B evaluation:hash_sha:total SHA-style file digest collection through .BR get_hash_from_fd2 (). .TP .B *:trust_db_lookup:total Total trust database lookup through .BR check_trust_database (). This includes update-thread lock wait, per-lookup LMDB read setup, the primary LMDB lookup, optional hash work requested by trust lookup, and the top-level .I /usr symlink retry path. .TP .B *:trust_db_lookup:lock_wait Time spent waiting for the trust database update-thread lock before LMDB read setup. This is a future multi-decision-thread contention signal. .TP .B *:trust_db_lookup:read LMDB read setup and trust database read work after the update-thread lock is held. This currently includes per-lookup LMDB read transaction and cursor setup, the primary lookup, optional hash work requested by trust lookup, the top-level .I /usr symlink retry path, and read cleanup. .TP .B response:total Work after the rule loop has completed. This includes syslog/debug formatting, rule/source bookkeeping, decision metric updates, response selection, audit metadata preparation, fanotify response writes, and descriptor close work. .TP .B response:syslog_debug_format:total Formatting of syslog or debug output after a decision. Lazy field lookups needed only for logging may appear as nested stage timings. .TP .B response:audit_metadata:total Preparation of fanotify audit response metadata before the response write. When audit metadata needs object trust, trust lookup time can be nested here. .TP .B response:fanotify_write The actual .BR write (2) of the fanotify response to the kernel. .SH NOTES The notes footer reports the largest queued-time contributor when queue wait was observed, the largest helper contributor, the largest decision phase contributor, and the slowest observed row by maximum time. .PP For cache hit, miss, collision, eviction, subject deferral, and early subject eviction counters, see .BR fapolicyd.metrics (5). .PP If adding a timing count, total, or bucket counter would overflow, fapolicyd disarms timing, records an overflow stop reason, and writes the timing report instead of allowing counters to wrap and skew the data. .SH FILES .TP .I /run/fapolicyd/fapolicyd.timing Manual decision timing report. .TP .I /run/fapolicyd/fapolicyd.state Runtime state report containing timing control state. .TP .I /run/fapolicyd/fapolicyd.metrics Runtime metrics report containing cache counters. .SH SEE ALSO .BR fapolicyd (8), .BR fapolicyd-cli (8), .BR fapolicyd.conf (5), .BR fapolicyd.state (5), .BR fapolicyd.metrics (5), and .BR fapolicyd.trust (5). fapolicyd-2.0/doc/fapolicyd.trust.5000066400000000000000000000050331524071644000173160ustar00rootroot00000000000000.TH FAPOLICYD.TRUST: "5" "January 2020" "Red Hat" "System Administration Utilities" .SH NAME fapolicyd.trust \- fapolicyd's file of trust .SH DESCRIPTION The file .I /etc/fapolicyd/fapolicyd.trust contains list of trusted files/binaries for the application whitelisting daemon. You may add comments to the file by starting the line with a '#' character. Each non-comment line contains a full path, size in bytes, and a valid SHA256 hash. Version 3 trust files write paths without escaping and find the size and hash by splitting at the final two spaces. A path may therefore contain spaces. The \fBfapolicyd-cli --file\fP commands receive ordinary shell arguments; quote a path containing a space in the shell, for example \fBfapolicyd-cli --file add "/tmp/first last/"\fP. In a version 3 trust file, \fB%20\fP is a literal percent sign followed by \fB20\fP, not a space. .sp The size field is an unsigned 64-bit byte count written in decimal. This fixed representation is intentionally independent of the native C \fBoff_t\fP and \fBsize_t\fP types, so trust records retain the same syntax on 32-bit and 64-bit builds. A record whose size cannot be represented by the native file-size type used for a filesystem comparison is rejected. .sp Version 2 trust files are accepted for compatibility and use their legacy percent escaping, including \fB%20\fP for a space. Newly rewritten trust files use version 3, so do not use version-2 percent escapes in new entries. .sp The directory \fI/etc/fapolicyd/trust\&.d\fR can be used to store multiple trust files\&. This way a privileged user can split the trust database into multiple files and manage them separately through \fBfapolicyd\-cli\fR\&. Functionally, the fapolicy daemon will behave the same way as if the whole trust database has been defined inside \fBfapolicyd\&.trust\fR file\&. Syntax and semantics of trust files inside \fBtrust\&.d\fR directory are the same as for \fBfapolicyd\&.trust\fR file (described above)\&. Trust files can either be created manually inside \fBtrust\&.d\fR directory or via \fBfapolicyd\-cli\fR\& (the latter option is recommended). .SH EXAMPLE .PP .EX [root@Desktop ~]# cat /etc/fapolicyd/fapolicyd.trust /home/user/my-ls 157984 61a9960bf7d255a85811f4afcac51067b8f2e4c75e21cf4f2af95319d4ed1b87 /home/user/my-ls2 5555 61a9960bf7d255a85811f4afcac51067b8f2e4c75e21cf4f2af95319d4ed1b87 .EE .SH FILES .B /etc/fapolicyd/fapolicyd.trust - list of trusted files/binaries .SH "SEE ALSO" .BR fapolicyd (8), .BR fapolicyd-cli (8) .BR fapolicy.rules (5) and .BR fapolicy.conf (5). .SH AUTHOR Radovan Sroka fapolicyd-2.0/doc/rpm-filter.conf.5000066400000000000000000000006571524071644000172000ustar00rootroot00000000000000.TH FAPOLICYD.FILTER: "26" "April 2023" "Red Hat" "System Administration Utilities" .SH NAME fapolicyd-filter.conf \- fapolicyd filter configuration file .SH DESCRIPTION The file .I /etc/fapolicyd/rpm-filter.conf was migrated to .I /etc/fapolicyd/fapolicyd-filter.conf or see .BR fapolicyd-filter.conf(5). .SH "SEE ALSO" .BR fapolicyd (8), .BR fapolicyd-cli (1) .BR fapolicy.rules (5) and .BR glob (7) .SH AUTHOR Radovan Sroka fapolicyd-2.0/doc/systemd-watchdog.md000066400000000000000000000036161524071644000177130ustar00rootroot00000000000000# fapolicyd systemd watchdog When fapolicyd is built with systemd watchdog support, the installed service uses `Type=notify` and starts the daemon with `--foreground`. This lets systemd track the real daemon process directly. The daemon sends `READY=1` after startup has completed and sends `WATCHDOG=1` only while the health monitor sees decision workers making progress. The `--debug` and `--debug-deny` modes disable systemd notification at runtime, even if `NOTIFY_SOCKET` or `WATCHDOG_*` variables are inherited from the environment. The production `--foreground` mode used by the service remains notify-capable. The default service sets: ```ini [Service] Type=notify NotifyAccess=main ExecStart=/usr/sbin/fapolicyd --foreground WatchdogSec=10s Restart=on-failure RestartSec=5s ``` `WatchdogSec=` is the worker stall deadline. The health monitor checks workers at half that interval and uses the same deadline when deciding whether queued work has stopped making progress. If a worker stalls, fapolicyd records the `worker_stall` failure action and terminates the daemon. Systemd then applies the service restart policy. Slow external storage can make a decision worker look stalled while the kernel waits for a device to respond. Increase the watchdog timeout with a drop-in rather than editing the packaged unit: ```ini # /etc/systemd/system/fapolicyd.service.d/watchdog-timeout.conf [Service] WatchdogSec=30s ``` To disable watchdog restarts while keeping `Type=notify` readiness reporting: ```ini # /etc/systemd/system/fapolicyd.service.d/no-watchdog.conf [Service] WatchdogSec=0 ``` After adding or changing a drop-in, run: ```sh systemctl daemon-reload systemctl restart fapolicyd ``` Builds configured with `--disable-systemd-watchdog` install a legacy forking unit instead: ```ini [Service] Type=forking PIDFile=/run/fapolicyd/fapolicyd.pid ExecStart=/usr/sbin/fapolicyd Restart=on-failure RestartSec=1s ``` fapolicyd-2.0/fapolicyd.spec000066400000000000000000000243511524071644000161630ustar00rootroot00000000000000#ASAN %%global asan_build 1 #ELN %%global eln_build 1 %if %{defined eln_build} %global selinuxtype targeted %global moduletype contrib %define semodule_version main %endif Summary: Application Whitelisting Daemon Name: fapolicyd Version: 2.0 Release: 1%{?dist} License: GPL-3.0-or-later URL: http://people.redhat.com/sgrubb/fapolicyd Source0: https://people.redhat.com/sgrubb/fapolicyd/%{name}-%{version}.tar.gz #ELN %Source1: https://github.com/linux-application-whitelisting/%{name}-selinux/archive/refs/heads/%{semodule_version}.tar.gz#/%{name}-selinux-%{semodule_version}.tar.gz BuildRequires: gcc BuildRequires: kernel-headers BuildRequires: autoconf automake make gcc libtool BuildRequires: systemd systemd-devel openssl-devel rpm-devel file-devel file BuildRequires: libcap-ng-devel libseccomp-devel lmdb-devel BuildRequires: python3-devel BuildRequires: uthash-devel %if %{defined asan_build} BuildRequires: libasan %endif %if %{defined eln_build} Recommends: %{name}-selinux %endif Requires(pre): shadow-utils Requires(post): systemd-units Requires(preun): systemd-units Requires(postun): systemd-units %description Fapolicyd (File Access Policy Daemon) implements application whitelisting to decide file access rights. Applications that are known via a reputation source are allowed access while unknown applications are not. The daemon makes use of the kernel's fanotify interface to determine file access rights. %if %{defined eln_build} %package selinux Summary: Fapolicyd selinux Group: Applications/System Requires: %{name} = %{version}-%{release} BuildRequires: selinux-policy %if 0%{?rhel} < 9 BuildRequires: selinux-policy-devel >= 3.14.3-108 %else %if 0%{?rhel} == 9 BuildRequires: selinux-policy-devel >= 38.1.2 %else BuildRequires: selinux-policy-devel >= 38.2 %endif %endif BuildArch: noarch %{?selinux_requires} %description selinux The %{name}-selinux package contains selinux policy for the %{name} daemon. %endif %prep %setup -q %if %{defined eln_build} # selinux %setup -q -D -T -a 1 %endif # generate rules for python sed -i "s|%python2_path%|`readlink -f %{__python2}`|g" rules.d/*.rules sed -i "s|%python3_path%|`readlink -f %{__python3}`|g" rules.d/*.rules # Detect run time linker directly from bash interpret=`readelf -e /usr/bin/bash \ | grep Requesting \ | sed 's/.$//' \ | rev | cut -d" " -f1 \ | rev` sed -i "s|%ld_so_path%|`realpath $interpret`|g" rules.d/*.rules %if 0%{?fedora} || 0%{?rhel} > 9 # Create a sysusers.d config file cat >fapolicyd.sysusers.conf < %{buildroot}/%{_datadir}/%{name}/default-ruleset.known-libs chmod 644 %{buildroot}/%{_datadir}/%{name}/default-ruleset.known-libs %if %{defined eln_build} # selinux install -d %{buildroot}%{_datadir}/selinux/packages/%{selinuxtype} install -m 0644 %{name}-selinux-%{semodule_version}/%{name}.pp.bz2 %{buildroot}%{_datadir}/selinux/packages/%{selinuxtype} install -d -p %{buildroot}%{_datadir}/selinux/devel/include/%{moduletype} install -p -m 644 %{name}-selinux-%{semodule_version}/%{name}.if %{buildroot}%{_datadir}/selinux/devel/include/%{moduletype}/ipp-%{name}.if %endif #cleanup find %{buildroot} \( -name '*.la' -o -name '*.a' \) -delete %if 0%{?fedora} || 0%{?rhel} > 9 install -m0644 -D fapolicyd.sysusers.conf %{buildroot}%{_sysusersdir}/fapolicyd.conf %endif %define manage_default_rules default_changed=0 \ # check changed fapolicyd.rules \ if [ -e %{_sysconfdir}/%{name}/%{name}.rules ]; then \ diff %{_sysconfdir}/%{name}/%{name}.rules %{_datadir}/%{name}/%{name}.rules.known-libs >/dev/null 2>&1 || { \ default_changed=1; \ #echo "change detected in fapolicyd.rules"; \ } \ fi \ if [ -e %{_sysconfdir}/%{name}/rules.d ]; then \ default_ruleset=''; \ # get listing of default rule files in known-libs \ [ -e %{_datadir}/%{name}/default-ruleset.known-libs ] && default_ruleset=`cat %{_datadir}/%{name}/default-ruleset.known-libs`; \ # check for removed or added files \ default_count=`echo "$default_ruleset" | wc -l`; \ current_count=`ls -1 %{_sysconfdir}/%{name}/rules.d/*.rules | wc -l`; \ [ $default_count -eq $current_count ] || { \ default_changed=1; \ # echo "change detected in number of rule files d:$default_count vs c:$current_count"; \ }; \ for file in %{_sysconfdir}/%{name}/rules.d/*.rules; do \ if echo "$default_ruleset" | grep -q "`basename $file`"; then \ # compare content of the rule files \ diff $file %{_datadir}/%{name}/sample-rules/`basename $file` >/dev/null 2>&1 || { \ default_changed=1; \ # echo "change detected in `basename $file`"; \ }; \ else \ # added file detected \ default_changed=1; \ # echo "change detected in added rules file `basename $file`"; \ fi; \ done; \ fi; \ # remove files if no change against default rules detected \ [ $default_changed -eq 0 ] && rm -rf %{_sysconfdir}/%{name}/%{name}.rules %{_sysconfdir}/%{name}/rules.d/* || : \ %check make check VERBOSE=yes %pre %if 0%{?rhel} && 0%{?rhel} <= 9 getent passwd %{name} >/dev/null || useradd -r -M -d %{_localstatedir}/lib/%{name} -s /sbin/nologin -c "Application Whitelisting Daemon" %{name} %endif if [ $1 -eq 2 ]; then # detect changed default rules in case of upgrade %manage_default_rules fi %post # if no pre-existing rule file if [ ! -e %{_sysconfdir}/%{name}/%{name}.rules ] ; then files=`ls %{_sysconfdir}/%{name}/rules.d/ 2>/dev/null | wc -w` # Only if no pre-existing component rules if [ "$files" -eq 0 ] ; then ## Install the known libs policy for rulesfile in `cat %{_datadir}/%{name}/default-ruleset.known-libs`; do cp %{_datadir}/%{name}/sample-rules/$rulesfile %{_sysconfdir}/%{name}/rules.d/ done chgrp %{name} %{_sysconfdir}/%{name}/rules.d/* if [ -x /usr/sbin/restorecon ] ; then # restore correct label /usr/sbin/restorecon -F %{_sysconfdir}/%{name}/rules.d/* fi fagenrules >/dev/null fi fi %systemd_post %{name}.service %preun %systemd_preun %{name}.service if [ $1 -eq 0 ]; then # detect changed default rules in case of uninstall %manage_default_rules else [ -e %{_sysconfdir}/%{name}/%{name}.rules ] && rm -rf %{_sysconfdir}/%{name}/rules.d/* || : fi %postun %systemd_postun_with_restart %{name}.service %files %doc README.md %{!?_licensedir:%global license %%doc} %license COPYING %attr(755,root,root) %dir %{_datadir}/%{name} %attr(755,root,root) %dir %{_datadir}/%{name}/sample-rules %attr(644,root,root) %{_datadir}/%{name}/default-ruleset.known-libs %attr(644,root,root) %{_datadir}/%{name}/sample-rules/* %attr(644,root,root) %{_datadir}/%{name}/fapolicyd-magic.mgc %attr(750,root,%{name}) %dir %{_sysconfdir}/%{name} %attr(750,root,%{name}) %dir %{_sysconfdir}/%{name}/trust.d %attr(750,root,%{name}) %dir %{_sysconfdir}/%{name}/rules.d %attr(644,root,%{name}) %{_sysconfdir}/bash_completion.d/fapolicyd.bash_completion %ghost %verify(not md5 size mtime) %attr(644,root,%{name}) %{_sysconfdir}/%{name}/rules.d/* %ghost %verify(not md5 size mtime) %attr(644,root,%{name}) %{_sysconfdir}/%{name}/%{name}.rules %config(noreplace) %attr(644,root,%{name}) %{_sysconfdir}/%{name}/%{name}.conf %config(noreplace) %attr(644,root,%{name}) %{_sysconfdir}/%{name}/%{name}-filter.conf %config(noreplace) %attr(644,root,%{name}) %{_sysconfdir}/%{name}/%{name}.trust %ghost %attr(644,root,%{name}) %{_sysconfdir}/%{name}/compiled.rules %attr(644,root,root) %{_unitdir}/%{name}.service %attr(644,root,root) %{_tmpfilesdir}/%{name}.conf %attr(755,root,root) %{_sbindir}/%{name} %attr(755,root,root) %{_sbindir}/%{name}-cli %attr(755,root,root) %{_sbindir}/%{name}-perf-test %attr(755,root,root) %{_libexecdir}/%{name}-rpm-loader %attr(755,root,root) %{_sbindir}/fagenrules %attr(644,root,root) %{_mandir}/man8/* %attr(644,root,root) %{_mandir}/man5/* %ghost %attr(440,%{name},%{name}) %verify(not md5 size mtime) %{_localstatedir}/log/%{name}-access.log %attr(770,root,%{name}) %dir %{_localstatedir}/lib/%{name} %attr(770,root,%{name}) %dir /run/%{name} %ghost %attr(600,root,root) %verify(not md5 size mtime) /run/%{name}/%{name}.pid %ghost %attr(660,root,%{name}) /run/%{name}/%{name}.fifo %ghost %attr(640,%{name},%{name}) %verify(not md5 size mtime) /run/%{name}/%{name}.state %ghost %attr(640,%{name},%{name}) %verify(not md5 size mtime) /run/%{name}/%{name}.metrics %ghost %attr(640,%{name},%{name}) %verify(not md5 size mtime) /run/%{name}/%{name}.timing %ghost %attr(660,%{name},%{name}) %verify(not md5 size mtime) %{_localstatedir}/lib/%{name}/data.mdb %ghost %attr(660,%{name},%{name}) %verify(not md5 size mtime) %{_localstatedir}/lib/%{name}/lock.mdb %if 0%{?fedora} || 0%{?rhel} > 9 %{_sysusersdir}/fapolicyd.conf %endif %if %{defined eln_build} %files selinux %{_datadir}/selinux/packages/%{selinuxtype}/%{name}.pp.bz2 %ghost %verify(not md5 size mode mtime) %{_sharedstatedir}/selinux/%{selinuxtype}/active/modules/200/%{name} %{_datadir}/selinux/devel/include/%{moduletype}/ipp-%{name}.if %post selinux %selinux_modules_install -s %{selinuxtype} %{_datadir}/selinux/packages/%{selinuxtype}/%{name}.pp.bz2 %selinux_relabel_post -s %{selinuxtype} %postun selinux if [ $1 -eq 0 ]; then %selinux_modules_uninstall -s %{selinuxtype} %{name} fi %posttrans selinux %selinux_relabel_post -s %{selinuxtype} %endif %changelog * Sun Jun 14 2026 Petr Lautrbach - 2.0-1 - New release fapolicyd-2.0/init/000077500000000000000000000000001524071644000142735ustar00rootroot00000000000000fapolicyd-2.0/init/Makefile.am000066400000000000000000000012711524071644000163300ustar00rootroot00000000000000EXTRA_DIST = \ fapolicyd.service.in \ fapolicyd.conf \ fapolicyd-filter.conf \ fapolicyd.trust \ fapolicyd-tmpfiles.conf \ fapolicyd-magic \ fapolicyd.bash_completion \ fagenrules fapolicyddir = $(sysconfdir)/fapolicyd dist_fapolicyd_DATA = \ fapolicyd.conf \ fapolicyd-filter.conf \ fapolicyd.trust systemdservicedir = $(systemdsystemunitdir) nodist_systemdservice_DATA = fapolicyd.service sbin_SCRIPTS = fagenrules completiondir = $(sysconfdir)/bash_completion.d/ dist_completion_DATA = fapolicyd.bash_completion MAGIC = fapolicyd-magic.mgc pkgdata_DATA = ${MAGIC} CLEANFILES = ${MAGIC} fapolicyd.service ${MAGIC}: $(EXTRA_DIST) file -C -m ${top_srcdir}/init/fapolicyd-magic fapolicyd-2.0/init/fagenrules000066400000000000000000000146701524071644000163610ustar00rootroot00000000000000#!/bin/sh # Script to concatenate rules files found in a base fapolicyd rules directory # to form a single /etc/fapolicyd/compiled.rules file suitable for loading # into the daemon # When forming the interim rules file, both empty lines and comment # lines (starting with # or #) are stripped as the source files # are processed. # # Having formed the interim rules file, the script checks if the file is empty # or is identical to the existing /etc/fapolicyd/compiled.rules and if either # of these cases are true, it does not replace the existing file. # # Variables # # DestinationFile: # Destination rules file # SourceRulesDir: # Directory location to find component rule files # TmpRules: # Temporary interim rules file in the destination directory # RuleFiles: # Temporary list of rule source files # SortedRules: # Temporary naturally sorted list of rule source files # RawRules: # Temporary concatenation of rule source files before filtering # ASuffix: # Suffix for previous fapolicyd.rules file if this script replaces it. # The file is left in the destination directory with suffix with $ASuffix OldDestinationFile=/etc/fapolicyd/fapolicyd.rules DestinationFile=/etc/fapolicyd/compiled.rules DestinationDir=${DestinationFile%/*} SourceRulesDir=/etc/fapolicyd/rules.d TmpRules= RuleFiles= SortedRules= RawRules= ASuffix="prev" OnlyCheck=0 LoadRules=0 RETVAL=0 usage="Usage: $0 [--check|--load]" # Delete the interim file on faults cleanup_tmp() { if [ "${TmpRules}" != "" ] ; then rm -f "${TmpRules}" fi if [ "${RuleFiles}" != "" ] ; then rm -f "${RuleFiles}" fi if [ "${SortedRules}" != "" ] ; then rm -f "${SortedRules}" fi if [ "${RawRules}" != "" ] ; then rm -f "${RawRules}" fi } fail_tmp() { echo "$0: $1" >&2 cleanup_tmp exit 1 } ruleset_hash() { sha256sum "$1" 2>/dev/null | awk '{print $1}' } emit_ruleset_hash() { hash=$(ruleset_hash "$1") if [ "${hash}" = "" ] ; then return 1 fi echo "$0: Ruleset identity: ${hash}" return 0 } trap 'cleanup_tmp; exit 1' HUP INT QUIT PIPE TERM try_load() { pid=$(pidof fapolicyd) if [ $LoadRules -eq 1 ] && [ "$pid" != "" ] ; then kill -HUP "$pid" RETVAL=$? fi } while [ $# -ge 1 ] do if [ "$1" = "--check" ] ; then OnlyCheck=1 elif [ "$1" = "--load" ] ; then LoadRules=1 else echo "$usage" exit 1 fi shift done # Check environment if [ ! -d "${SourceRulesDir}" ]; then echo "$0: No rules directory - ${SourceRulesDir}" try_load exit 1 fi # Preserve the no-rules migration behavior while ignoring non-rule files. # Source names are later read one line at a time, so whitespace is retained. if ! first_rule=$(find "${SourceRulesDir}" -mindepth 1 -maxdepth 1 \ ! -name '.*' \( -type f -o -type l \) -name '*.rules' -print -quit); then echo "$0: Cannot list rules in ${SourceRulesDir}" >&2 exit 1 elif [ "${first_rule}" = "" ] ; then echo "No rules in ${SourceRulesDir}" # won't call this an error as they may not have migrated exit 0 elif [ -e "${OldDestinationFile}" ] ; then echo "Error - both old and new rules exist. Delete one or the other" exit 1 fi # Create the interim rules file ensuring its access modes protect it # from normal users and strip empty lines and comment lines. umask 0137 RuleFiles=$(mktemp "${DestinationDir}/.fagenrules.XXXXXXXX") || fail_tmp "Cannot create temporary rule file list in ${DestinationDir}" SortedRules=$(mktemp "${DestinationDir}/.fagenrules.XXXXXXXX") || fail_tmp "Cannot create sorted rule file list in ${DestinationDir}" RawRules=$(mktemp "${DestinationDir}/.fagenrules.XXXXXXXX") || fail_tmp "Cannot create temporary raw rules file in ${DestinationDir}" TmpRules=$(mktemp "${DestinationDir}/.compiled.rules.XXXXXXXX") || fail_tmp "Cannot create temporary rules file in ${DestinationDir}" if ! find "${SourceRulesDir}" -mindepth 1 -maxdepth 1 \ ! -name '.*' \( -type f -o -type l \) -name '*.rules' -print \ > "${RuleFiles}" ; then fail_tmp "Cannot list rules in ${SourceRulesDir}" fi # Keep the documented ls -v-style ordering without splitting whitespace names. if ! sort -V "${RuleFiles}" > "${SortedRules}" ; then fail_tmp "Cannot sort rules in ${SourceRulesDir}" fi while IFS= read -r rules || [ "${rules}" != "" ] ; do if ! cat "${rules}" >> "${RawRules}" ; then fail_tmp "Cannot read rule file ${rules}" fi if ! printf '\n' >> "${RawRules}" ; then fail_tmp "Cannot write temporary rules file ${RawRules}" fi done < "${SortedRules}" if ! printf '## This file is automatically generated from %s\n' \ "${SourceRulesDir}" >> "${TmpRules}" ; then fail_tmp "Cannot write temporary rules file ${TmpRules}" fi if ! awk ' BEGIN { rest = 0; } { if (length($0) < 1) { next; } if (match($0, "^[[:space:]]*#")) { next; } rules[rest++] = $0; } END { for (i = 0; i < rest; i++) { printf "%s\n", rules[i]; } }' "${RawRules}" >> "${TmpRules}" ; then fail_tmp "Cannot filter rules in ${RawRules}" fi fapolicyd-cli --check-rules "${TmpRules}" err=$? if [ $err -ne 0 ]; then echo "Rules file content:" cat -n "${TmpRules}" cleanup_tmp exit $err fi # If the same then quit if cmp -s "${TmpRules}" "${DestinationFile}" > /dev/null 2>&1 ; then echo "$0: No change" emit_ruleset_hash "${DestinationFile}" || fail_tmp \ "Failed to hash final ruleset ${DestinationFile}" cleanup_tmp try_load exit $RETVAL elif [ $OnlyCheck -eq 1 ] ; then echo "$0: Rules have changed and should be updated" cleanup_tmp exit 0 fi # Otherwise prepare the new file without touching the installed rules. If any # step fails before the final rename, the previous compiled rules remain active. chmod 0644 "${TmpRules}" || fail_tmp "Cannot set mode on ${TmpRules}" chgrp fapolicyd "${TmpRules}" || fail_tmp "Cannot set group on ${TmpRules}" hash=$(ruleset_hash "${TmpRules}") || fail_tmp "Failed to hash candidate ruleset ${TmpRules}" if [ "${hash}" = "" ] ; then fail_tmp "Failed to hash candidate ruleset ${TmpRules}" fi sync "${TmpRules}" || fail_tmp "Failed to fsync ${TmpRules}" if [ -f "${DestinationFile}" ]; then cp -p "${DestinationFile}" "${DestinationFile}.${ASuffix}" || fail_tmp "Cannot backup ${DestinationFile}" fi mv -f "${TmpRules}" "${DestinationFile}" || fail_tmp "Cannot install ${DestinationFile}" TmpRules= # Restore context on MLS system. if [ -x /usr/sbin/restorecon ] ; then /usr/sbin/restorecon -F "${DestinationFile}" fi sync "${DestinationDir}" 2>/dev/null || echo "$0: Warning - failed to fsync ${DestinationDir}" >&2 echo "$0: Ruleset identity: ${hash}" cleanup_tmp try_load exit $RETVAL fapolicyd-2.0/init/fapolicyd-filter.conf000066400000000000000000000042231524071644000204000ustar00rootroot00000000000000# --------------------------------------------------------------------------- # Overview of the default fapolicyd-filter.conf # # Decision flow # 1. Rules are read top-to-bottom. Leading “+” means the path is # considered essential and will be added to the trust database; # leading “-” means the path is skipped. # 2. Indentation creates a parent-child relationship. A child rule # refines the decision of its parent. Think “allow everything # under /usr/, then deny one sub-tree, then allow one file type # back in”, etc. # 3. If no rule matches at the end of the scan, the parent’s decision # stands; the very last top-level rule is therefore the catch-all. # # What the shipped rules do # • Start with “+ /” → everything allowed by default. # • Deny /usr/include/ entirely. (- usr/include/) # • Deny /usr/share/ but re-allow developer artefacts: # *.py *.pl etc # • Deny /usr/src/kernel*/ except: # */scripts/* and */tools/objtool/* # • Deny html, md, conf, and other non-executable files # # For full syntax see: man 5 fapolicyd-filter.conf # --------------------------------------------------------------------------- + / - usr/include/ - usr/share/ # Python byte code + *.py? # Python text files + *.py # Some apps have a private bin + */bin/* # Some apps have a private libexec + */libexec/* # Ruby + *.rb # Perl + *.pl # System tap + *.stp # Javascript + *.js # Java archive + *.jar # M4 + *.m4 # PHP + *.php # Perl Modules + *.pm # Lua + *.lua # Java + *.class # Typescript + *.ts # Typescript JSX + *.tsx # Lisp + *.el # Compiled Lisp + *.elc # Allow Lmod to run in bash profiles.d/modules.sh + lmod/*/bash # Drop all source (kernel/nvidia) - usr/src/*/ # Allow scripts and tools + */scripts/* # Drop any source code - *.c - *.h + */tools/objtool/* # Drop any xz - *.xz # Drop any html - *.html # Drop any json - *.json # Drop any markdown - *.md # Drop any configuration - *.conf # drop png files scattered all over - *.png fapolicyd-2.0/init/fapolicyd-magic000066400000000000000000000013661524071644000172540ustar00rootroot000000000000000 string/wt #!\ /bin/rc Plan 9 shell script text executable !:mime text/x-plan9-shellscript 0 string/wt #!\ /usr/bin/gjs Gnome Javascript text executable !:mime application/javascript 0 belong 0x6f0d0d0a python 3.10 byte-compiled !:mime application/x-bytecode.python 0 search/1/wt #!\ /usr/libexec/platform-python Python script text executable !:strength + 15 !:mime text/x-python 0 string/wt #!\ /usr/bin/guile Guile script text executable !:mime text/x-script.guile 0 string/wt #!\ /usr/sbin/nft Netfilter tables script text executable !:mime text/x-nftables 0 belong 0x4d5a9000 Portable executable !:mime application/vnd.microsoft.portable-executable 0 string/wt #!\ /usr/bin/bpftrace Bpf tracing script text executable !:mime text/x-bpftrace fapolicyd-2.0/init/fapolicyd-tmpfiles.conf000066400000000000000000000001651524071644000207370ustar00rootroot00000000000000d /run/fapolicyd 0770 root fapolicyd - d /var/lib/fapolicyd 770 root fapolicyd - Z /etc/fapolicyd - root fapolicyd - fapolicyd-2.0/init/fapolicyd.bash_completion000066400000000000000000000122231524071644000213350ustar00rootroot00000000000000# fapolicyd-cli (8) completion -*- shell-script -*- # shellcheck shell=bash _fapolicydcli() { local cur prev opts local word i COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" opts="--check-config --check-path --check-rules --check-status --check-metrics \ --check-trustdb --compact-trustdb --check-watch_fs --check-ignore_mounts \ --delete-db --dump-db \ --file --filter --ftype --ftype-ext --help --list --update --reload-rules \ --reset-metrics --timing-start --timing-stop --timer-start \ --timer-stop --test-filter \ --trust-file --lint --verbose --yes -h -d -D -f -t -T -l -u -r -y" # Handle file management subcommands specially so we can complete file paths local file_mode=false subcmd="" have_path=0 filter_used=0 trust_used=0 for word in "${COMP_WORDS[@]}"; do if [[ ${word} == --file || ${word} == -f ]]; then file_mode=true fi done if ${file_mode}; then for ((i = 1; i < ${#COMP_WORDS[@]}; i++)); do word=${COMP_WORDS[$i]} if [[ ${word} == --filter ]]; then filter_used=1 continue fi if [[ ${word} == --trust-file ]]; then trust_used=1 i=$((i+1)) continue fi if [[ -z ${subcmd} && ( ${word} == add || ${word} == delete || ${word} == update ) ]]; then subcmd=${word} continue fi if [[ -n ${subcmd} && ${word} != -* && ${word} != add && ${word} != delete && ${word} != update ]]; then have_path=1 fi done if [[ ${prev} == --trust-file ]]; then if [ -e /usr/share/bash-completion/bash_completion ] ; then _filedir else compopt -o filenames 2>/dev/null mapfile -t COMPREPLY < <(compgen -f -- "${cur}") fi return 0 fi if [[ -z ${subcmd} ]]; then mapfile -t COMPREPLY < <(compgen -W 'add delete update' -- "${cur}") return 0 fi if [[ ${prev} == "${subcmd}" || ( ${have_path} -eq 0 && ${cur} != -* && ${prev} != --filter ) ]]; then if [ -e /usr/share/bash-completion/bash_completion ] ; then _filedir else compopt -o filenames 2>/dev/null mapfile -t COMPREPLY < <(compgen -f -- "${cur}") fi return 0 fi local file_opts="" [[ ${filter_used} -eq 0 ]] && file_opts="${file_opts} --filter" [[ ${trust_used} -eq 0 ]] && file_opts="${file_opts} --trust-file" if [[ -n ${file_opts} && ( -z ${cur} || ${cur} == -* ) ]]; then mapfile -t COMPREPLY < <(compgen -W "${file_opts}" -- "${cur}") [[ ${COMPREPLY-} == *= ]] && compopt -o nospace return 0 fi fi case $prev in --ftype|-t|--ftype-ext|-T|--test-filter) # If bash completions is installed, use it if [ -e /usr/share/bash-completion/bash_completion ] ; then _filedir return 0 else # this is almost as good compopt -o filenames 2>/dev/null mapfile -t COMPREPLY < <(compgen -f -- "${cur}") return 0 fi ;; --check-ignore_mounts) if [ -e /usr/share/bash-completion/bash_completion ] ; then _filedir -d return 0 else compopt -o dirnames 2>/dev/null mapfile -t COMPREPLY < <(compgen -d -- "${cur}") return 0 fi ;; --file|-f) mapfile -t COMPREPLY < <(compgen -W 'add delete update' -- "$cur") return 0 ;; add|update|delete) mapfile -t COMPREPLY < <(compgen -W '--filter --trust-file' -- "$cur") [[ ${COMPREPLY-} == *= ]] && compopt -o nospace return 0 ;; --filter) if [ -e /usr/share/bash-completion/bash_completion ] ; then _filedir else compopt -o filenames 2>/dev/null mapfile -t COMPREPLY < <(compgen -f -- "${cur}") fi return 0 ;; --check-rules) if [[ ${cur} == -* ]]; then mapfile -t COMPREPLY < <(compgen -W '--lint' -- "${cur}") return 0 fi if [ -e /usr/share/bash-completion/bash_completion ] ; then _filedir else compopt -o filenames 2>/dev/null mapfile -t COMPREPLY < <(compgen -f -- "${cur}") fi return 0 ;; --trust-file) if [ -e /usr/share/bash-completion/bash_completion ] ; then _filedir else compopt -o filenames 2>/dev/null mapfile -t COMPREPLY < <(compgen -f -- "${cur}") fi return 0 ;; esac if [[ $cur == -* ]]; then mapfile -t COMPREPLY < <(compgen -W "${opts}" -- "$cur") [[ ${COMPREPLY-} == *= ]] && compopt -o nospace return 0 fi } _fapolicyd() { local cur opts COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" opts="--debug --debug-deny --help --mounts= --no-details --permissive --version" if [[ ${cur} == --mounts=* ]]; then local mount_cur=${cur#--mounts=} if [ -e /usr/share/bash-completion/bash_completion ] ; then _filedir COMPREPLY=( "${COMPREPLY[@]/#/--mounts=}" ) else compopt -o filenames 2>/dev/null mapfile -t COMPREPLY < <(compgen -f -- "${mount_cur}") COMPREPLY=( "${COMPREPLY[@]/#/--mounts=}" ) fi return 0 fi if [[ ${cur} == -* ]] ; then mapfile -t COMPREPLY < <(compgen -W "${opts}" -- "${cur}") [[ ${COMPREPLY-} == *= ]] && compopt -o nospace return 0 fi } _fagenrules() { local cur opts COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" opts="--check --load" if [[ ${cur} == -* ]] ; then mapfile -t COMPREPLY < <(compgen -W "${opts}" -- "${cur}") return 0 fi } complete -F _fapolicydcli fapolicyd-cli complete -F _fapolicyd fapolicyd complete -F _fagenrules fagenrules # ex: filetype=sh fapolicyd-2.0/init/fapolicyd.conf000066400000000000000000000012021524071644000171070ustar00rootroot00000000000000# # This file controls the configuration of the file access policy daemon. # See the fapolicyd.conf man page for explanation. # permissive = 0 nice_val = 14 q_size = 800 decision_threads = 1 uid = fapolicyd gid = fapolicyd do_stat_report = 1 detailed_report = 1 db_max_size = auto subj_cache_size = 4099 obj_cache_size = 8191 watch_fs = ext2,ext3,ext4,tmpfs,xfs,vfat,iso9660,btrfs #ignore_mounts = /path/to/mount1,/path/to/mount2 trust = rpmdb,file integrity = none syslog_format = rule,dec,perm,auid,pid,exe,:,path,ftype,trust rpm_sha256_only = 0 allow_filesystem_mark = 0 report_interval = 0 reset_strategy = never timing_collection = off fapolicyd-2.0/init/fapolicyd.service.in000066400000000000000000000015771524071644000202460ustar00rootroot00000000000000# You should manage this file with systemctl edit utility and not manually [Unit] Description=File Access Policy Daemon DefaultDependencies=no # If rules need user/group lookup, create a drop-in to delay the startup after NSS lookup is available: # # mkdir -p /etc/systemd/system/fapolicyd.service.d # # echo -e "[Unit]\nAfter=nss-user-lookup.target local-fs.target systemd-tmpfiles-setup.service" > /etc/systemd/system/fapolicyd.service.d/nss-lookup.conf # # systemctl daemon-reload After=local-fs.target systemd-tmpfiles-setup.service Documentation=man:fapolicyd(8) [Service] OOMScoreAdjust=-1000 Type=@FAPOLICYD_SERVICE_TYPE@ @FAPOLICYD_SERVICE_NOTIFY_ACCESS@ RuntimeDirectory=fapolicyd @FAPOLICYD_SERVICE_PIDFILE@ ExecStartPre=/usr/sbin/fagenrules ExecStart=@FAPOLICYD_SERVICE_EXEC@ @FAPOLICYD_SERVICE_WATCHDOG@ Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target fapolicyd-2.0/init/fapolicyd.trust000066400000000000000000000003451524071644000173520ustar00rootroot00000000000000# AUTOGENERATED FILE VERSION 2 # This file contains a list of trusted files # # FULL PATH SIZE SHA256 # /home/user/my-ls 157984 61a9960bf7d255a85811f4afcac51067b8f2e4c75e21cf4f2af95319d4ed1b87 fapolicyd-2.0/m4/000077500000000000000000000000001524071644000136505ustar00rootroot00000000000000fapolicyd-2.0/m4/dyn_linker.m4000066400000000000000000000005571524071644000162570ustar00rootroot00000000000000AC_DEFUN([LD_SO_PATH], [ bash_path=`command -v bash` xpath1=`readelf -e $bash_path | grep Requesting | sed 's/.$//' | rev | cut -d" " -f1 | rev` xpath=`realpath $xpath1` if test ! -f "$xpath" ; then AC_MSG_ERROR([Cant find the dynamic linker]) fi echo "dynamic linker is.....$xpath" AC_DEFINE_UNQUOTED(SYSTEM_LD_SO, ["$xpath"], [dynamic linker]) ]) fapolicyd-2.0/rules.d/000077500000000000000000000000001524071644000147045ustar00rootroot00000000000000fapolicyd-2.0/rules.d/10-languages.rules000066400000000000000000000007351524071644000201510ustar00rootroot00000000000000# This file contains the list of all identified languages on the system %languages=application/x-bytecode.ocaml,application/x-bytecode.python,application/java-archive,text/x-java,application/x-java-applet,application/javascript,text/javascript,text/x-awk,text/x-gawk,text/x-lisp,application/x-elc,text/x-lua,text/x-m4,text/x-nftables,text/x-perl,text/x-php,text/x-script.python,text/x-python,text/x-R,text/x-ruby,text/x-script.guile,text/x-tcl,text/x-luatex,text/x-systemtap fapolicyd-2.0/rules.d/20-dracut.rules000066400000000000000000000002031524071644000174540ustar00rootroot00000000000000# Carve out an exception for dracut's initramfs building allow perm=any uid=0 : dir=/var/tmp/ allow perm=any uid=0 trust=1 : all fapolicyd-2.0/rules.d/21-updaters.rules000066400000000000000000000002741524071644000200320ustar00rootroot00000000000000# We have to carve out an exception for the system updaters # or things go very bad (deadlock). allow perm=open exe=/usr/bin/rpm : all allow perm=open exe=%python3_path% comm=dnf : all fapolicyd-2.0/rules.d/22-buildroot.rules000066400000000000000000000017411524071644000202070ustar00rootroot00000000000000# Exception for software builders. # # Software builders create lots of files. Since they were all just created, # fapolicyd has never seen them before and expensive integrity checking has # to be done which will fail because they are all untrusted. Since it's single # purpose is building applications, we need to carve out a permissive domain # for it to operate in. # # The buildroot is protected by file permissions. Noone except root and the # mock account can write to the buildroot. The buildroot is wiped clean after # a build so that nothing can persist. What the following rules say is that we # are trusting the mock account to create and access anything in it's buildroot. # Otherwise, the mock account can use anything that is trusted for any purpose # anywhere else. It is still restricted by user id permissions. # # The following uid and dir should be adjusted to fit your configuration. allow perm=any uid=mock : dir=/home/mock/rpmbuild allow perm=any uid=mock trust=1 : all fapolicyd-2.0/rules.d/30-patterns.rules000066400000000000000000000003411524071644000200360ustar00rootroot00000000000000# This file contains the list of all patterns. Only the ld_so pattern # is enabled by default. deny_audit perm=any pattern=ld_so : all #deny_audit perm=any pattern=ld_preload : all #deny_audit perm=any pattern=static : all fapolicyd-2.0/rules.d/40-bad-elf.rules000066400000000000000000000001451524071644000174730ustar00rootroot00000000000000# Do not allow malformed ELF even if trusted deny_audit perm=any all : ftype=application/x-bad-elf fapolicyd-2.0/rules.d/41-nfsd.rules000066400000000000000000000026661524071644000171460ustar00rootroot00000000000000# Allow NFS clients to open exported content through the kernel NFS server. # # nfsd is a kernel thread, not an executable, so it cannot be added to the # fapolicyd trust database. Kernel threads have PPID 2; pairing that with # nfsd's comm value prevents a user-space process named nfsd from matching. # This deliberately permits only open events. NFS export rules and file # permissions remain responsible for authorizing the remote client. # # WARNING: When exported content resides on an already watched local # filesystem (ext4/xfs), this server-side rule does not require adding nfs or # nfs4 to watch_fs. If exported content instead resides on an NFS client mount, # fapolicyd can evaluate access to it only when that filesystem type is # watched. The default watch list intentionally excludes network filesystems. # The kernel must resolve a mount path while adding its fanotify mark, and # fanotify_mark() has no nonblocking mode. If an NFS server is unreachable # while fapolicyd marks its client mount, a series of events can lead to a # hang waiting for permission decisions. Only watch NFS client mounts when # network and server availability are assured. # # NOTE: This rule implicitly trusts all content from nfs mounts. If you prefer # that only trusted content on nfs is allowed, then you have to manually add # that to the trustdb using fapolicyd-cli --file add instead of # using this rule. allow perm=open ppid=2 comm=nfsd : all fapolicyd-2.0/rules.d/41-shared-obj.rules000066400000000000000000000003701524071644000202200ustar00rootroot00000000000000# Only allow known ELF libs - this is ahead of executable because typical # executable is linked with a dozen or more libraries. allow perm=open all : ftype=application/x-sharedlib trust=1 deny_audit perm=open all : ftype=application/x-sharedlib fapolicyd-2.0/rules.d/42-trusted-elf.rules000066400000000000000000000001071524071644000204370ustar00rootroot00000000000000# Allow trusted programs to execute allow perm=execute all : trust=1 fapolicyd-2.0/rules.d/43-known-elf.rules000066400000000000000000000006171524071644000201100ustar00rootroot00000000000000# Only allow known ELF Applications allow perm=execute all : ftype=application/x-executable trust=1 deny_audit perm=execute all : ftype=application/x-executable # This is a workaround for kernel thinking this is being executed because it # occurs during the execve call for an ELF binary. We catch actual execution # in the ld_so pattern rule. allow perm=execute all : path=%ld_so_path% trust=1 fapolicyd-2.0/rules.d/70-trusted-lang.rules000066400000000000000000000002171524071644000206150ustar00rootroot00000000000000# Allow any program to open trusted language files allow perm=open all : ftype=%languages trust=1 deny_audit perm=any all : ftype=%languages fapolicyd-2.0/rules.d/71-known-python.rules000066400000000000000000000004211524071644000206550ustar00rootroot00000000000000# Only allow system python executables and libs allow perm=any all : ftype=text/x-python trust=1 allow perm=open all : ftype=application/x-bytecode.python trust=1 deny_audit perm=any all : ftype=text/x-python deny_audit perm=any all : ftype=application/x-bytecode.python fapolicyd-2.0/rules.d/72-shell.rules000066400000000000000000000001401524071644000173100ustar00rootroot00000000000000# Allow all shell script execution and sourcing allow perm=any all : ftype=text/x-shellscript fapolicyd-2.0/rules.d/73-known-perl.rules000066400000000000000000000001741524071644000203050ustar00rootroot00000000000000# Only allow system perl files allow perm=any all : ftype=text/x-perl trust=1 deny_audit perm=any all : ftype=text/x-perl fapolicyd-2.0/rules.d/74-known-ocaml.rules000066400000000000000000000002371524071644000204370ustar00rootroot00000000000000# Only allow system Ocaml files allow perm=any all : ftype=application/x-bytecode.ocaml trust=1 deny_audit perm=any all : ftype=application/x-bytecode.ocaml fapolicyd-2.0/rules.d/75-known-php.rules000066400000000000000000000001711524071644000201310ustar00rootroot00000000000000# Only allow system PHP files allow perm=any all : ftype=text/x-php trust=1 deny_audit perm=any all : ftype=text/x-php fapolicyd-2.0/rules.d/76-known-ruby.rules000066400000000000000000000001741524071644000203270ustar00rootroot00000000000000# Only allow system ruby files allow perm=any all : ftype=text/x-ruby trust=1 deny_audit perm=any all : ftype=text/x-ruby fapolicyd-2.0/rules.d/77-known-lua.rules000066400000000000000000000001711524071644000201250ustar00rootroot00000000000000# Only allow system lua files allow perm=any all : ftype=text/x-lua trust=1 deny_audit perm=any all : ftype=text/x-lua fapolicyd-2.0/rules.d/78-known-wine.rules000066400000000000000000000002771524071644000203160ustar00rootroot00000000000000# Only allow system wine files allow perm=any all : ftype=application/vnd.microsoft.portable-executable trust=1 deny_audit perm=any all : ftype=application/vnd.microsoft.portable-executable fapolicyd-2.0/rules.d/90-deny-execute.rules000066400000000000000000000001141524071644000206010ustar00rootroot00000000000000# Deny execution for anything untrusted deny_audit perm=execute all : all fapolicyd-2.0/rules.d/91-deny-lang.rules000066400000000000000000000001321524071644000200610ustar00rootroot00000000000000# Deny all languages not explicitly enabled deny_audit perm=open all : ftype=%languages fapolicyd-2.0/rules.d/95-allow-open.rules000066400000000000000000000001051524071644000202640ustar00rootroot00000000000000# Allow everything else to open any file allow perm=open all : all fapolicyd-2.0/rules.d/Makefile.am000066400000000000000000000026101524071644000167370ustar00rootroot00000000000000# Makefile.am -- # Copyright 2022-24 Red Hat Inc. # All Rights Reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; see the file COPYING. If not, write to the # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor # Boston, MA 02110-1335, USA. # # Authors: # Steve Grubb # CONFIG_CLEAN_FILES = *.rej *.orig EXTRA_DIST = README-rules 10-languages.rules 20-dracut.rules \ 21-updaters.rules 22-buildroot.rules 30-patterns.rules \ 40-bad-elf.rules 41-nfsd.rules 41-shared-obj.rules \ 42-trusted-elf.rules 43-known-elf.rules \ 70-trusted-lang.rules 71-known-python.rules 72-shell.rules \ 73-known-perl.rules 74-known-ocaml.rules 75-known-php.rules \ 76-known-ruby.rules 77-known-lua.rules \ 90-deny-execute.rules 91-deny-lang.rules 95-allow-open.rules rulesdir = $(datadir)/fapolicyd/sample-rules dist_rules_DATA = $(EXTRA_DIST) fapolicyd-2.0/rules.d/README-rules000066400000000000000000000045631524071644000167240ustar00rootroot00000000000000This group of rules are meant to be used with the fagenrules program. The fagenrules program expects rules to be located in /etc/fapolicyd/rules.d/ The rules will get processed in a specific order based on their natural sort order. To make things easier to use, the files in this directory are organized into groups with the following meanings: 10 - macros 20 - loop holes 30 - patterns 40 - ELF rules 50 - user/group access rules 60 - application access rules 70 - language rules 80 - trusted execute 90 - general open access to documents The installed 10-languages.rules file defines the %languages macro. A package that needs an additional MIME type can install a macro fragment ordered after that file and before policy rules, for example 11-vendor-languages.rules: %languages+=application/x-vendor-script The += form only adds MIME types to the existing %languages macro; it cannot create or replace the macro. Repeated MIME types are harmless. Run fagenrules after adding or removing a fragment so compiled.rules reflects the change. NFS servers that export language or other untrusted content can copy the optional 60-nfsd.rules unit into rules.d. It allows the kernel nfsd worker threads to open exported files before the language denial rules are reached. This is not part of either default policy because fapolicyd cannot establish trust for a remote NFS client; NFS export controls and file permissions remain the authorization boundary for that access. that should be thought out and individual files copied to /etc/fapolicyd/rules.d/ Once you have the rules in the rules.d directory, you can load them by running fagenrules --load You can reconstruct the old policy files by including the following: fapolicyd.rules.known-libs -------------------------- 10-languages.rules 20-dracut.rules 21-updaters.rules 30-patterns.rules 40-bad-elf.rules 41-shared-obj.rules 42-trusted-elf.rules 70-trusted-lang.rules 72-shell.rules 90-deny-execute.rules 95-allow-open.rules fapolicyd.rules.restrictive --------------------------- 10-languages.rules 20-dracut.rules 21-updaters.rules 30-patterns.rules 40-bad-elf.rules 41-shared-obj.rules 43-known-elf.rules 71-known-python.rules 72-shell.rules 73-known-perl.rules (optional) 74-known-ocaml.rules (optiona) 75-known-php.rules (optional) 76-known-ruby.rules (optional) 77-known-lua.rules (optional) 90-deny-execute.rules 91-deny-lang.rules 95-allow-open.rules fapolicyd-2.0/src/000077500000000000000000000000001524071644000141175ustar00rootroot00000000000000fapolicyd-2.0/src/Makefile.am000066400000000000000000000101421524071644000161510ustar00rootroot00000000000000SUBDIRS = tests CONFIG_CLEAN_FILES = *.loT *.rej *.orig AM_CPPFLAGS = \ -I${top_srcdir} \ -I${top_srcdir}/src/library check-recursive: libfapolicyd.la sbin_PROGRAMS = fapolicyd fapolicyd-cli if WITH_PERF_TEST sbin_PROGRAMS += fapolicyd-perf-test endif libexec_PROGRAMS = lib_LTLIBRARIES= libfapolicyd.la fapolicyd_CFLAGS = -std=gnu11 -fPIE -DPIE -pthread -g -W -Wall -Wshadow -Wundef -Wno-unused-result -Wno-unused-parameter -D_GNU_SOURCE -DLIBEXECDIR='"$(libexecdir)"' fapolicyd_LDFLAGS = -pie -Wl,-z,relro -Wl,-z,now -static fapolicyd_LDADD = libfapolicyd.la fapolicyd_cli_CFLAGS = $(fapolicyd_CFLAGS) fapolicyd_cli_LDFLAGS = $(fapolicyd_LDFLAGS) fapolicyd_cli_LDADD = libfapolicyd.la if WITH_PERF_TEST fapolicyd_perf_test_CFLAGS = $(fapolicyd_CFLAGS) fapolicyd_perf_test_LDFLAGS = $(fapolicyd_LDFLAGS) fapolicyd_perf_test_LDADD = libfapolicyd.la endif libfapolicyd_la_SOURCES = \ library/avl.c \ library/avl.h \ library/attr-lookup-metrics.c \ library/attr-lookup-metrics.h \ library/attr-sets.c \ library/attr-sets.h \ library/backend-manager.c \ library/backend-manager.h \ library/conf.h \ library/database.c \ library/database.h \ library/database-internal.h \ library/database-update.c \ library/daemon-config.c \ library/daemon-config.h \ library/decision-config.c \ library/decision-config.h \ library/decision-context.h \ library/decision-defer.c \ library/decision-defer.h \ library/decision-event.h \ library/decision-timing.c \ library/decision-timing.h \ library/escape.c \ library/escape.h \ library/event.c \ library/event.h \ library/failure-action.c \ library/failure-action.h \ library/fapolicyd-defs.h \ library/fapolicyd-backend.h \ library/fd-fgets.c \ library/fd-fgets.h \ library/file.c \ library/file.h \ library/file-backend.c \ library/gcc-attributes.h \ library/llist.c \ library/llist.h \ library/lru.c \ library/lru.h \ library/message.c \ library/message.h \ library/nv.h \ library/object-attr.c \ library/object-attr.h \ library/object.c \ library/object.h \ library/paths.h \ library/policy.c \ library/policy.h \ library/policy-metrics.c \ library/policy-metrics.h \ library/process.c \ library/process.h \ library/queue.c \ library/queue.h \ library/rules.c \ library/rules.h \ library/subject-attr.c \ library/subject-attr.h \ library/subject.c \ library/subject.h \ library/stack.c \ library/stack.h \ library/string-util.c \ library/string-util.h \ library/trust-file.c \ library/trust-file.h \ library/filter.c \ library/filter.h if WITH_RPM libfapolicyd_la_SOURCES += \ library/rpm-backend.c libexec_PROGRAMS += fapolicyd-rpm-loader fapolicyd_rpm_loader_SOURCES = \ handler/fapolicyd-rpm-loader.c fapolicyd_rpm_loader_CFLAGS = $(fapolicyd_CFLAGS) fapolicyd_rpm_loader_LDFLAGS = $(fapolicyd_LDFLAGS) fapolicyd_rpm_loader_LDADD = libfapolicyd.la endif if WITH_DEB libfapolicyd_la_SOURCES += library/deb-backend.c libfapolicyd_la_LIBADD = -ldpkg -lmd libexec_PROGRAMS += fapolicyd-deb-loader fapolicyd_deb_loader_SOURCES = \ handler/fapolicyd-deb-loader.c fapolicyd_deb_loader_CFLAGS = $(fapolicyd_CFLAGS) fapolicyd_deb_loader_LDFLAGS = $(fapolicyd_LDFLAGS) fapolicyd_deb_loader_LDADD = libfapolicyd.la fapolicyd_CFLAGS += -DLIBDPKG_VOLATILE_API LIBS += -ldpkg -lmd endif if NEED_MD5 libfapolicyd_la_SOURCES += \ library/md5-backend.c \ library/md5-backend.h endif libfapolicyd_la_CFLAGS = $(fapolicyd_CFLAGS) libfapolicyd_la_LDFLAGS = $(fapolicyd_LDFLAGS) -lpthread fapolicyd_SOURCES = \ daemon/fanotify-fs-error.c \ daemon/fanotify-fs-error.h \ daemon/fapolicyd.c \ daemon/decision-worker.c \ daemon/decision-worker.h \ daemon/mounts.c \ daemon/mounts.h \ daemon/notify.c \ daemon/notify.h \ daemon/state-report.c \ daemon/state-report.h \ daemon/systemd-notify.c \ daemon/systemd-notify.h \ daemon/worker-health.c \ daemon/worker-health.h fapolicyd_cli_SOURCES = \ cli/cli-privilege.c \ cli/cli-privilege.h \ cli/fapolicyd-cli.c \ cli/file-cli.c \ cli/file-cli.h \ cli/ignore-mounts.c \ cli/ignore-mounts.h \ cli/rule-lint.c \ cli/rule-lint.h if WITH_PERF_TEST fapolicyd_perf_test_SOURCES = \ perf-test/fapolicyd-perf-test.c endif fapolicyd-2.0/src/cli/000077500000000000000000000000001524071644000146665ustar00rootroot00000000000000fapolicyd-2.0/src/cli/cli-privilege.c000066400000000000000000000076301524071644000175730ustar00rootroot00000000000000/* * cli-privilege.c - privilege checks for fapolicyd-cli commands * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include "cli-privilege.h" #define FAPOLICYD_CONFIG_DIR "/etc/fapolicyd" struct cli_credentials { uid_t euid; gid_t egid; gid_t allowed_gid; const gid_t *groups; int group_count; }; /* * privilege_satisfied - evaluate a captured identity against a requirement. * @requirement: privilege policy selected by the command. * @credentials: effective IDs and supplementary groups to inspect. * Returns true when the identity meets the selected requirement. */ static bool privilege_satisfied(enum cli_privilege_requirement requirement, const struct cli_credentials *credentials) { if (credentials->euid == 0) return true; if (requirement == CLI_PRIVILEGE_ROOT) return false; if (credentials->egid == credentials->allowed_gid) return true; for (int i = 0; i < credentials->group_count; i++) if (credentials->groups[i] == credentials->allowed_gid) return true; return false; } /* * cli_require_privilege - check the caller against a command requirement. * @requirement: whether root alone or root/group membership is accepted. * Returns 0 when permitted and 1 after printing a tailored error otherwise. */ int cli_require_privilege(enum cli_privilege_requirement requirement) { struct cli_credentials credentials = { .euid = geteuid(), .egid = getegid(), .allowed_gid = (gid_t)-1, .groups = NULL, .group_count = 0, }; struct group *group; struct stat config_dir; gid_t *groups = NULL; int group_count; if (credentials.euid == 0) return 0; if (requirement == CLI_PRIVILEGE_ROOT) goto denied; /* * The config directory remains available when the runtime directory and * FIFO disappear, so its group is the stable administrative identity. */ if (stat(FAPOLICYD_CONFIG_DIR, &config_dir)) { fprintf(stderr, "Cannot inspect %s: %s\n", FAPOLICYD_CONFIG_DIR, strerror(errno)); goto denied; } credentials.allowed_gid = config_dir.st_gid; group_count = getgroups(0, NULL); if (group_count < 0) { fprintf(stderr, "Cannot inspect supplementary groups: %s\n", strerror(errno)); goto denied; } if (group_count > 0) { groups = malloc((size_t)group_count * sizeof(*groups)); if (groups == NULL) { fprintf(stderr, "Cannot allocate supplementary group list\n"); goto denied; } group_count = getgroups(group_count, groups); if (group_count < 0) { fprintf(stderr, "Cannot inspect supplementary groups: %s\n", strerror(errno)); free(groups); goto denied; } credentials.groups = groups; credentials.group_count = group_count; } if (privilege_satisfied(requirement, &credentials)) { free(groups); return 0; } free(groups); denied: if (requirement == CLI_PRIVILEGE_ROOT) fprintf(stderr, "Error: this command requires root privileges.\n"); else { group = credentials.allowed_gid == (gid_t)-1 ? NULL : getgrgid(credentials.allowed_gid); if (group) fprintf(stderr, "Error: this command requires root privileges or " "membership in the %s group.\n", group->gr_name); else if (credentials.allowed_gid != (gid_t)-1) fprintf(stderr, "Error: this command requires root privileges or " "membership in group ID %" PRIuMAX ".\n", (uintmax_t)credentials.allowed_gid); else fprintf(stderr, "Error: this command requires root privileges or " "membership in the group that owns %s.\n", FAPOLICYD_CONFIG_DIR); if (group) endgrent(); } return 1; } fapolicyd-2.0/src/cli/cli-privilege.h000066400000000000000000000014671524071644000176020ustar00rootroot00000000000000/* * cli-privilege.h - privilege requirements for fapolicyd-cli commands * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef CLI_PRIVILEGE_HEADER #define CLI_PRIVILEGE_HEADER enum cli_privilege_requirement { CLI_PRIVILEGE_ROOT, CLI_PRIVILEGE_ROOT_OR_GROUP, }; /* * cli_require_privilege - check the caller against a command requirement. * @requirement: whether root alone or root/group membership is accepted. * Returns 0 when permitted and 1 after printing a tailored error otherwise. */ int cli_require_privilege(enum cli_privilege_requirement requirement); #endif fapolicyd-2.0/src/cli/fapolicyd-cli.c000066400000000000000000001253751524071644000175660ustar00rootroot00000000000000/* * fapolicy-cli.c - CLI tool for fapolicyd * Copyright (c) 2019-2022 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka * Steve Grubb * Zoltan Fridrich */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // basename #include "policy.h" #include "rules.h" #include "database.h" #include "file-cli.h" #include "file.h" #include "fapolicyd-backend.h" #include "string-util.h" #include "daemon-config.h" #include "decision-config.h" #include "gcc-attributes.h" #include "message.h" #include "llist.h" #include "avl.h" #include "fd-fgets.h" #include "paths.h" #include "filter.h" #include "file.h" #include "ignore-mounts.h" #include "rule-lint.h" #include "cli-privilege.h" bool verbose = false; static bool lint_rules = false; static bool assume_yes = false; static int send_state_report_signal(unsigned int pid, report_intent_t intent, char *reason, size_t reason_len) __attr_access ((__write_only__, 3, 4)); static int send_timing_signal(unsigned int pid, report_intent_t intent, char *reason, size_t reason_len) __attr_access ((__write_only__, 3, 4)); static int get_daemon_pid(unsigned int *pid, char *reason, size_t reason_len) __attr_access ((__write_only__, 2, 3)); static const char *usage = "Fapolicyd CLI Tool\n\n" "--check-config Check the daemon config for syntax errors\n" "--check-path Check files in $PATH against the trustdb for problems\n" "--check-status Dump daemon health and configuration state\n" "--check-metrics Dump daemon runtime counters and cache metrics\n" "--check-trustdb Check the trustdb against files on disk for problems\n" "--compact-trustdb Rebuild and compact the daemon trustdb LMDB environment\n" "--check-watch_fs Check watch_fs against currently mounted file systems\n" "--check-ignore_mounts [path] Scan ignored mounts for risky content\n" "--check-rules [path] Validate rules file syntax without loading\n" "--lint Enable policy lint warnings with --check-rules\n" "--reset-metrics Dump metrics and reset counters when daemon allows it\n" "--timing-start Start a manual decision timing run\n" "--timing-stop Stop manual decision timing and dump timing report\n" "--timer-start Alias for --timing-start\n" "--timer-stop Alias for --timing-stop\n" "--verbose Enable verbose output for select commands\n" "-d, --delete-db Delete the trust database\n" "-D, --dump-db Dump the trust database contents\n" "-f, --file cmd path Manage the file trust database\n" "-h, --help Prints this help message\n" "-t, --ftype file-path Prints out the mime type of a file\n" "-T, --ftype-ext file-path Emit NUL-delimited path and mime type\n" "-l, --list Prints a list of the daemon's rules with numbers\n" "-r, --reload-rules Notifies fapolicyd to perform reload of rules\n" "-y, --yes Do not prompt before a manual metrics reset\n" #ifdef HAVE_LIBRPM "--test-filter path Test FILTER_FILE against path and trace to stdout\n" #endif "--filter Use FILTER_FILE for --file add or update\n" "--trust-file file Use after --file to specify trust file\n" "-u, --update Notifies fapolicyd to perform update of database\n" ; static struct option long_opts[] = { {"check-config",0, NULL, 1 }, {"check-watch_fs",0, NULL, 2 }, {"check-ignore_mounts", 2, NULL, 7 }, {"verbose", 0, NULL, 8 }, {"check-trustdb",0, NULL, 3 }, {"check-status",0, NULL, 4 }, {"check-path", 0, NULL, 5 }, {"check-metrics",0, NULL, 14 }, {"compact-trustdb", 0, NULL, 15 }, {"check-rules", 2, NULL, 9 }, {"lint", 0, NULL, 10 }, {"reset-metrics", 0, NULL, 11 }, {"timing-start", 0, NULL, 12 }, {"timing-stop", 0, NULL, 13 }, {"timer-start", 0, NULL, 12 }, {"timer-stop", 0, NULL, 13 }, {"yes", 0, NULL, 'y'}, {"delete-db", 0, NULL, 'd'}, {"dump-db", 0, NULL, 'D'}, {"file", 1, NULL, 'f'}, {"help", 0, NULL, 'h'}, {"ftype", 1, NULL, 't'}, {"ftype-ext", 1, NULL, 'T'}, {"list", 0, NULL, 'l'}, {"update", 0, NULL, 'u'}, {"reload-rules", 0, NULL, 'r'}, #ifdef HAVE_LIBRPM {"test-filter", 1, NULL, 6 }, #endif { NULL, 0, NULL, 0 } }; atomic_bool stop = 0; // Library needs this unsigned int debug_mode = 0; // Library needs this conf_t config; // Library needs this static void reset_config(void) { decision_config_destroy(); free_daemon_config(&config); memset(&config, 0, sizeof(config)); } typedef enum _reload_code { DB, RULES, COMPACT } reload_code; static char *get_line(FILE *f) { char *line = NULL; size_t len = 0; if (getline(&line, &len, f) != -1) { /* remove newline */ char *ptr = strchr(line, 0x0a); if (ptr) *ptr = 0; return line; } free(line); return NULL; } static int do_delete_db(void) { if (unlink_db()) return CLI_EXIT_DB_ERROR; return CLI_EXIT_SUCCESS; } // This function opens the trust db and iterates over the entries. // It returns CLI_EXIT_SUCCESS on success and CLI_EXIT_DB_ERROR on failure static int verify_file(const char *path, off_t size, const char *sha, unsigned int tsource); static int do_dump_db(void) { int rc, exit_rc = CLI_EXIT_SUCCESS; MDB_env *env; MDB_txn *txn; MDB_dbi dbi; MDB_stat status; MDB_cursor *cursor; MDB_val key, val; char active_name[TRUST_DB_GENERATION_NAME_SIZE]; rc = mdb_env_create(&env); if (rc) { fprintf(stderr, "mdb_env_create failed, error %d %s\n", rc, mdb_strerror(rc)); return CLI_EXIT_DB_ERROR; } rc = mdb_env_set_maxdbs(env, 128); if (rc) { fprintf(stderr, "mdb_env_set_maxdbs failed, error %d %s\n", rc, mdb_strerror(rc)); exit_rc = CLI_EXIT_DB_ERROR; goto env_close; } /* * Dumping walks the live database while the daemon may reload trust DB * generations, so this read-only transaction must enter LMDB's reader * table instead of bypassing locking. */ rc = mdb_env_open(env, DB_DIR, MDB_RDONLY, 0660); if (rc) { fprintf(stderr, "mdb_env_open failed, error %d %s\n", rc, mdb_strerror(rc)); exit_rc = CLI_EXIT_DB_ERROR; goto env_close; } rc = mdb_env_stat(env, &status); if (rc) { fprintf(stderr, "mdb_env_stat failed, error %d %s\n", rc, mdb_strerror(rc)); exit_rc = CLI_EXIT_DB_ERROR; goto env_close; } if (status.ms_entries == 0) { printf("Trust database is empty\n"); goto env_close; // Note: rc is 0 to get here } rc = mdb_txn_begin(env, NULL, MDB_RDONLY, &txn); if (rc) { fprintf(stderr, "mdb_txn_begin failed, error %d %s\n", rc, mdb_strerror(rc)); exit_rc = CLI_EXIT_DB_ERROR; goto env_close; } /* * The daemon publishes trust DB generations as named databases and * records the active name in metadata. Older databases did not have this * metadata, so fall back to DB_NAME for compatibility. */ rc = database_read_active_name(txn, active_name, sizeof(active_name)); if (rc) { fprintf(stderr, "trust metadata read failed, error %d %s\n", rc, mdb_strerror(rc)); exit_rc = CLI_EXIT_DB_ERROR; goto txn_abort; } rc = mdb_dbi_open(txn, active_name, MDB_DUPSORT, &dbi); if (rc) { fprintf(stderr, "mdb_open failed, error %d %s\n", rc, mdb_strerror(rc)); exit_rc = CLI_EXIT_DB_ERROR; goto txn_abort; } rc = mdb_stat(txn, dbi, &status); if (rc) { fprintf(stderr, "mdb_stat failed, error %d %s\n", rc, mdb_strerror(rc)); exit_rc = CLI_EXIT_DB_ERROR; goto dbi_close; } if (status.ms_entries == 0) { printf("Trust database is empty\n"); goto dbi_close; } rc = mdb_cursor_open(txn, dbi, &cursor); if (rc) { fprintf(stderr, "mdb_cursor_open failed, error %d %s\n", rc, mdb_strerror(rc)); exit_rc = CLI_EXIT_DB_ERROR; goto dbi_close; } rc = mdb_cursor_get(cursor, &key, &val, MDB_FIRST); if (rc) { if (rc == MDB_NOTFOUND) { printf("Trust database is empty\n"); goto cursor_close; } fprintf(stderr, "mdb_cursor_get failed, error %d %s\n", rc, mdb_strerror(rc)); exit_rc = CLI_EXIT_DB_ERROR; goto cursor_close; } do { char *path = NULL, *data = NULL, sha[FILE_DIGEST_STRING_MAX]; unsigned int tsource; trustdb_size_t size; const char *source; path = malloc(key.mv_size + 1); if (!path) goto next_record; memcpy(path, key.mv_data, key.mv_size); path[key.mv_size] = 0; data = malloc(val.mv_size + 1); if (!data) goto next_record; memcpy(data, val.mv_data, val.mv_size); data[val.mv_size] = 0; if (sscanf(data, DATA_FORMAT_IN, &tsource, &size, sha) != 3) goto next_record; source = lookup_tsource(tsource); printf("%s %s %" PRIu64 " %s\n", source, path, size, sha); next_record: free(data); free(path); // Try to get the duplicate. If it doesn't exist, get the next one rc = mdb_cursor_get(cursor, &key, &val, MDB_NEXT_DUP); if (rc == MDB_NOTFOUND) rc = mdb_cursor_get(cursor, &key, &val, MDB_NEXT_NODUP); } while (rc == 0); if (rc != MDB_NOTFOUND) exit_rc = CLI_EXIT_DB_ERROR; cursor_close: mdb_cursor_close(cursor); dbi_close: mdb_close(env, dbi); txn_abort: mdb_txn_abort(txn); env_close: mdb_env_close(env); return exit_rc; } static int parse_file_args(int argc, char * const argv[], const char **path, const char **trust_file, bool *use_filter, bool path_optional) { *path = NULL; *trust_file = NULL; *use_filter = false; for (int i = 0; i < argc; i++) { if (!strcmp(argv[i], "--filter")) { if (*use_filter) return CLI_EXIT_USAGE; *use_filter = true; continue; } if (!strcmp(argv[i], "--trust-file")) { if (*trust_file || i + 1 >= argc) return CLI_EXIT_USAGE; *trust_file = argv[++i]; continue; } if (*path == NULL) { *path = argv[i]; continue; } return CLI_EXIT_USAGE; } if (!path_optional && *path == NULL) return CLI_EXIT_USAGE; return CLI_EXIT_SUCCESS; } static int do_file_add(int argc, char * const argv[]) { char full_path[PATH_MAX] = { 0 }; const char *path = NULL; const char *trust_file = NULL; bool use_filter = false; int rc = parse_file_args(argc, argv, &path, &trust_file, &use_filter, false); if (rc) return rc; if (!realpath(path, full_path)) return CLI_EXIT_PATH_CONFIG; return file_append(full_path, trust_file, use_filter); } static int do_file_delete(int argc, char * const argv[]) { char full_path[PATH_MAX] = { 0 }; if (argc == 1) { if (!realpath(argv[0], full_path)) return CLI_EXIT_PATH_CONFIG; return file_delete(full_path, NULL); } if (argc == 3) { if (!realpath(argv[0], full_path)) return CLI_EXIT_PATH_CONFIG; if (strcmp("--trust-file", argv[1])) return CLI_EXIT_USAGE; return file_delete(full_path, argv[2]); } return CLI_EXIT_USAGE; } static int do_file_update(int argc, char * const argv[]) { char full_path[PATH_MAX] = { 0 }; const char *path = NULL; const char *trust_file = NULL; bool use_filter = false; int rc = parse_file_args(argc, argv, &path, &trust_file, &use_filter, true); if (rc) return rc; if (path) { if (!realpath(path, full_path)) return CLI_EXIT_PATH_CONFIG; path = full_path; } else { path = "/"; } return file_update(path, trust_file, use_filter); } static int do_manage_files(int argc, char * const argv[]) { int rc = CLI_EXIT_SUCCESS; if (argc < 1 || argc > 5) { fprintf(stderr, "Wrong number of arguments\n"); fprintf(stderr, "\n%s", usage); return CLI_EXIT_USAGE; } if (!strcmp("add", argv[0])) rc = do_file_add(argc - 1, argv + 1); else if (!strcmp("delete", argv[0])) rc = do_file_delete(argc - 1, argv + 1); else if (!strcmp("update", argv[0])) rc = do_file_update(argc - 1, argv + 1); else { fprintf(stderr, "%s is not a valid option, choose one of add|delete|update\n", argv[0]); fprintf(stderr, "\n%s", usage); return CLI_EXIT_USAGE; } switch (rc) { case CLI_EXIT_SUCCESS: // no error return CLI_EXIT_SUCCESS; case CLI_EXIT_USAGE: // args error fprintf(stderr, "Wrong number of arguments\n"); fprintf(stderr, "\n%s", usage); return rc; case CLI_EXIT_PATH_CONFIG: // realpath error fprintf(stderr, "Can't obtain realpath from: %s\n", argv[1]); fprintf(stderr, "\n%s", usage); return rc; default: // file function errors break; } return rc; } static int do_ftype(const char *path, bool print_path) { int fd; const char *ptr = NULL; char buf[80]; struct stat sb; struct file_info i; // We need to open in non-blocking mode because if its a // fifo, it will hang the program. fd = open(path, O_RDONLY|O_NONBLOCK); if (fd < 0) { fprintf(stderr, "Cannot open %s - %s\n", path, strerror(errno)); return CLI_EXIT_IO; } if (fstat(fd, &sb) != 0) { fprintf(stderr, "Cannot stat %s - %s\n", path, strerror(errno)); close(fd); return CLI_EXIT_IO; } // Setup file info with bare essentials i.device = sb.st_dev; i.mode = sb.st_mode; i.size = sb.st_size; if (file_init()) { fprintf(stderr, "Cannot initialize file helper libraries\n"); close(fd); return CLI_EXIT_INTERNAL; } ptr = get_file_type_from_fd(fd, &i, path, sizeof(buf), buf); file_close(); close(fd); if (print_path) { /* Keep pathname bytes from creating synthetic output records. */ fputs(path, stdout); fputc('\0', stdout); fputs(ptr ? ptr : "unknown", stdout); fputc('\0', stdout); } else if (ptr) printf("%s\n", ptr); else printf("unknown\n"); return CLI_EXIT_SUCCESS; } static int do_list(void) { unsigned count = 1; FILE *f = fopen(OLD_RULES_FILE, "rm"); char *buf; if (f == NULL) { f = fopen(RULES_FILE, "rm"); if (f == NULL) { fprintf(stderr, "Cannot open rules file (%s)\n", strerror(errno)); return CLI_EXIT_IO; } } else { FILE *t = fopen(RULES_FILE, "rm"); if (t) { fclose(t); fclose(f); fprintf(stderr, "Error - old and new rules file detected. " "Delete one or the other.\n"); return CLI_EXIT_PATH_CONFIG; } } while ((buf = get_line(f))) { char *str = buf; while (*str) { if (!isblank((unsigned char)*str)) break; str++; } if (*str == 0) // blank line goto next_iteration; if (*str == '#') //comment line goto next_iteration; if (*str == '%') { printf("-> %s\n", buf); goto next_iteration; } printf("%u. %s\n", count, buf); count++; next_iteration: free(buf); } fclose(f); return CLI_EXIT_SUCCESS; } static int do_reload(int code) { int fd = -1; struct stat s; char command; char str[2]; ssize_t ret; switch (code) { case DB: command = RELOAD_TRUSTDB_COMMAND; break; case RULES: command = RELOAD_RULES_COMMAND; break; case COMPACT: command = COMPACT_TRUSTDB_COMMAND; break; default: return CLI_EXIT_INTERNAL; } str[0] = command; str[1] = '\n'; fd = open(fifo_path, O_WRONLY); if (fd == -1) { fprintf(stderr, "Open: %s -> %s\n", fifo_path, strerror(errno)); return CLI_EXIT_DAEMON_IPC; } if (fstat(fd, &s) == -1) { fprintf(stderr, "Stat: %s -> %s\n", fifo_path, strerror(errno)); close(fd); return CLI_EXIT_DAEMON_IPC; } else { if (!S_ISFIFO(s.st_mode)) { fprintf(stderr, "File: %s exists but it is not a pipe!\n", fifo_path); close(fd); return CLI_EXIT_DAEMON_IPC; } // we will require pipe to have 0660 permissions mode_t mode = s.st_mode & ~S_IFMT; if (mode != 0660) { fprintf(stderr, "File: %s has 0%o instead of 0660 \n", fifo_path, mode); close(fd); return CLI_EXIT_DAEMON_IPC; } } do { ret = write(fd, str, sizeof(str)); } while (ret == -1 && errno == EINTR); if (ret != (ssize_t)sizeof(str)) { if (ret < 0) fprintf(stderr, "Write: %s -> %s\n", fifo_path, strerror(errno)); else fprintf(stderr, "Write: %s -> short write (%zd of %zu bytes)\n", fifo_path, ret, sizeof(str)); close(fd); return CLI_EXIT_DAEMON_IPC; } if (close(fd)) { fprintf(stderr,"Close: %s -> %s\n", fifo_path, strerror(errno)); return CLI_EXIT_DAEMON_IPC; } printf("Fapolicyd was notified\n"); return CLI_EXIT_SUCCESS; } static const char *bad_filesystems[] = { "autofs", "bdev", "binder", "binfmt_misc", "bpf", "cgroup", "cgroup2", "configfs", "cpuset", "debugfs", "devpts", "devtmpfs", "efivarfs", "fusectl", "fuse.gvfsd-fuse", "fuse.portal", "hugetlbfs", "mqueue", "nsfs", "overlay", // No source of trust for what's in this "pipefs", "proc", "pstore", "resctrl", "rpc_pipefs", "securityfs", "selinuxfs", "sockfs", "sysfs", "tracefs" }; #define FS_NAMES (sizeof(bad_filesystems)/sizeof(bad_filesystems[0])) // Returns 1 if not a real file system and 0 if its a file system we can watch static int not_watchable(const char *type) { unsigned int i; for (i = 0; i < FS_NAMES; i++) if (strcmp(bad_filesystems[i], type) == 0) return 1; return 0; } // Returns CLI_EXIT_SUCCESS on success or other CLI_EXIT_* codes on failure. // Finding unwatched file systems is not considered an error static int check_watch_fs(void) { char buf[PATH_MAX * 2], device[1025], point[4097]; char type[32], mntops[128]; int fs_req, fs_passno, fd, found = 0, alloc_err = 0, read_err = 0; list_t fs, mnt; char *ptr, *saved, *tmp; set_message_mode(MSG_STDERR, DBG_YES); reset_config(); if (load_daemon_config(&config)) { reset_config(); return CLI_EXIT_PATH_CONFIG; } if (config.watch_fs == NULL) { fprintf(stderr, "File systems to watch is empty"); reset_config(); return CLI_EXIT_PATH_CONFIG; } tmp = strdup(config.watch_fs); if (tmp == NULL) { reset_config(); return CLI_EXIT_INTERNAL; } list_init(&fs); ptr = strtok_r(tmp, ",", &saved); while (ptr) { char *index = strdup(ptr); char *data = strdup("0"); if (!index || !data || list_append(&fs, index, data)) { free(index); free(data); alloc_err = 1; } ptr = strtok_r(NULL, ",", &saved); } free(tmp); fd = open("/proc/mounts", O_RDONLY); if (fd < 0) { fprintf(stderr, "Unable to open mounts\n"); reset_config(); list_empty(&fs); return CLI_EXIT_IO; } fd_fgets_state_t *st = fd_fgets_init(); if (!st) { fprintf(stderr, "Failed fd_fgets_init\n"); reset_config(); list_empty(&fs); close(fd); return CLI_EXIT_INTERNAL; } // Build the list of mount point types list_init(&mnt); do { int read_rc = fd_fgets_r(st, buf, sizeof(buf), fd); /* A read error does not initialize buf or mark the state at EOF. */ if (read_rc < 0) { fprintf(stderr, "Unable to read mounts\n"); read_err = 1; break; } if (read_rc > 0) { sscanf(buf, "%1024s %4096s %31s %127s %d %d\n", device,point, type, mntops, &fs_req, &fs_passno); // Some file systems are not watchable if (not_watchable(type)) continue; char *index = strdup(type); char *data = strdup("0"); if (!index || !data || list_append(&mnt, index, data)) { free(index); free(data); alloc_err = 1; } } } while (!fd_fgets_eof_r(st)); fd_fgets_destroy(st); close(fd); // Now search the list we just built for (list_item_t *lptr = mnt.first; lptr; ) { // See if the file system is watched if (list_contains(&fs, lptr->index) == 0) { found = 1; printf("%s not watched\n", (char *)lptr->index); // Remove the file system so that we get 1 report char *tmpfs = strdup(lptr->index); if (tmpfs == NULL) { alloc_err = 1; break; } while (list_remove(&mnt, tmpfs)) ; free(tmpfs); // Start from the beginning lptr = mnt.first; continue; } lptr = lptr->next; } reset_config(); list_empty(&fs); list_empty(&mnt); if (read_err) return CLI_EXIT_IO; if (alloc_err) return CLI_EXIT_INTERNAL; if (found == 0) printf("Nothing appears missing\n"); return CLI_EXIT_SUCCESS; } // Returns 0 = everything is OK, 1 = there is a problem static int verify_file(const char *path, off_t size, const char *sha, unsigned int tsource) { int fd, warn_sha = 0; struct stat sb; file_hash_alg_t alg; size_t digest_len, expected_len; const char *alg_name; digest_len = strlen(sha); alg = file_hash_alg(digest_len); expected_len = file_hash_length(alg) * 2; /* * Non-RPM trust fragments historically used SHA256, but newer stores * may contain longer digests (for example SHA512). Fall back to * SHA256 only when the digest length cannot be mapped to a known * algorithm so legacy entries keep working. */ if (expected_len == 0) expected_len = file_hash_length(FILE_HASH_ALG_SHA256) * 2; if (alg == FILE_HASH_ALG_NONE) alg = FILE_HASH_ALG_SHA256; if (digest_len != expected_len) { printf("%s miscompares: cannot infer digest algorithm\n", path); return 1; } fd = open(path, O_RDONLY); if (fd < 0) { printf("Can't open %s (%s)\n", path, strerror(errno)); return 1; } if (fstat(fd, &sb)) { printf("Can't stat %s (%s)\n", path, strerror(errno)); close(fd); return 1; } if (sb.st_size != size) { printf("%s miscompares: file size\n", path); close(fd); return 1; } char *sha_buf = get_hash_from_fd2(fd, sb.st_size, alg); close(fd); if (sha_buf == NULL || strcmp(sha, sha_buf)) warn_sha = 1; free(sha_buf); if (warn_sha) { alg_name = file_hash_alg_name(alg); printf("%s miscompares: %s\n", path, alg_name ? alg_name : "digest"); return 1; } return 0; } static int check_trustdb(void) { int verification_failed = 0, exit_rc = CLI_EXIT_SUCCESS; unsigned long unverifiable = 0; set_message_mode(MSG_STDERR, DBG_NO); reset_config(); if (load_daemon_config(&config)) { reset_config(); return CLI_EXIT_PATH_CONFIG; } set_message_mode(MSG_QUIET, DBG_NO); if (decision_config_publish(&config)) { set_message_mode(MSG_STDERR, DBG_NO); fprintf(stderr, "Unable to publish decision configuration\n"); reset_config(); return CLI_EXIT_INTERNAL; } int rc = walk_database_start(&config); reset_config(); if (rc == WALK_DATABASE_EMPTY) return CLI_EXIT_SUCCESS; if (rc) return CLI_EXIT_DB_ERROR; do { unsigned int tsource; trustdb_size_t stored_size; off_t size; char sha[FILE_DIGEST_STRING_MAX]; char data[TRUSTDB_DATA_BUFSZ]; char *path; // Get the entry and format it for use. walkdb_entry_t *entry = walk_database_get_entry(); if (entry->path_is_hashed) { /* A SHA-512 key is not evidence that its record miscompares. */ unverifiable++; continue; } path = malloc(entry->path.mv_size + 1); if (path == NULL) { fprintf(stderr, "Out of memory while verifying trust database\n"); exit_rc = CLI_EXIT_INTERNAL; break; } memcpy(path, entry->path.mv_data, entry->path.mv_size); path[entry->path.mv_size] = '\0'; snprintf(data, sizeof(data), "%.*s", (int) entry->data.mv_size, (char *) entry->data.mv_data); if (sscanf(data, DATA_FORMAT_IN, &tsource, &stored_size, sha) != 3 || trustdb_size_to_off_t(stored_size, &size)) { fprintf(stderr, "%s data entry is corrupted\n", path); verification_failed = 1; free(path); continue; } if (verify_file(path, size, sha, tsource)) verification_failed = 1; free(path); } while ((rc = walk_database_next()) == WALK_DATABASE_NEXT); walk_database_finish(); if (rc == WALK_DATABASE_NEXT_ERROR) exit_rc = CLI_EXIT_DB_ERROR; if (unverifiable) { fprintf(stderr, "Warning: %lu hashed trust database %s could not be " "verified: the original %s %s unavailable\n", unverifiable, unverifiable == 1 ? "entry" : "entries", unverifiable == 1 ? "path" : "paths", unverifiable == 1 ? "is" : "are"); } if (verification_failed && exit_rc == CLI_EXIT_SUCCESS) exit_rc = CLI_EXIT_GENERIC; if (!verification_failed && unverifiable == 0 && exit_rc == CLI_EXIT_SUCCESS) puts("No problems found"); return exit_rc; } static int is_link(const char *path) { struct stat sb; if (lstat(path, &sb)) { fprintf(stderr, "Can't stat %s\n", path); return -1; } if (S_ISLNK(sb.st_mode)) return 1; return 0; } // Check that the file is in the trust db static int path_found = 0; static int path_scan_error = 0; static int check_file(const char *fpath, const struct stat *sb, int typeflag, struct FTW *s_unused __attribute__ ((unused))) { int ret = FTW_CONTINUE; struct file_info info = { 0 }; struct stat opened; int fd; /* nftw() leaves sb undefined for FTW_NS entries. */ if (typeflag == FTW_NS || typeflag == FTW_DNR) { fprintf(stderr, "Unable to inspect %s during PATH scan\n", fpath); path_scan_error = 1; return ret; } if (typeflag != FTW_F || S_ISREG(sb->st_mode) == 0) return ret; fd = open_verified_regular_file(fpath, sb, &opened); if (fd < 0) { fprintf(stderr, "Unable to safely open %s (%s)\n", fpath, strerror(errno)); path_scan_error = 1; return ret; } info.device = opened.st_dev; info.inode = opened.st_ino; info.mode = opened.st_mode; info.size = opened.st_size; info.time = opened.st_mtim; if (check_trust_database_readonly(fpath, &info, fd) != 1) { path_found = 1; fprintf(stderr, "%s is not trusted\n", fpath); } close(fd); return ret; } static int check_path(void) { char *ptr, *saved; int link, walk_rc; const char *env_path = getenv("PATH"); if (env_path == NULL) { puts("PATH not found"); return CLI_EXIT_PATH_CONFIG; } set_message_mode(MSG_STDERR, DBG_NO); reset_config(); if (load_daemon_config(&config)) { reset_config(); return CLI_EXIT_PATH_CONFIG; } set_message_mode(MSG_QUIET, DBG_NO); if (decision_config_publish(&config)) { set_message_mode(MSG_STDERR, DBG_NO); fprintf(stderr, "Unable to publish decision configuration\n"); reset_config(); return CLI_EXIT_INTERNAL; } int rc = database_readonly_lookup_start(); if (rc) { set_message_mode(MSG_STDERR, DBG_NO); fprintf(stderr, "Cannot initialize trust database (%d)\n", rc); reset_config(); return CLI_EXIT_DB_ERROR; } path_found = 0; path_scan_error = 0; char *path = strdup(env_path); if (path == NULL) { fprintf(stderr, "Out of memory while reading PATH\n"); database_readonly_lookup_finish(); reset_config(); return CLI_EXIT_INTERNAL; } ptr = strtok_r(path, ":", &saved); while (ptr) { link = is_link(ptr); if (link) { if (link < 0) path_scan_error = 1; goto next; } walk_rc = nftw(ptr, check_file, 1024, FTW_PHYS); if (walk_rc) { fprintf(stderr, "Unable to scan %s (%s)\n", ptr, strerror(errno)); path_scan_error = 1; } next: ptr = strtok_r(NULL, ":", &saved); } free(path); database_readonly_lookup_finish(); reset_config(); if (path_found == 0 && path_scan_error == 0) puts("No problems found"); if (path_scan_error) return CLI_EXIT_IO; return CLI_EXIT_SUCCESS; } /* * confirm_metric_reset - ask before sending a SIGUSR1 reset intent. * Returns 1 when the caller confirms, 0 otherwise. */ static int confirm_metric_reset(void) { char answer[8]; fprintf(stderr, "Request runtime metrics reset with this metrics report? [y/N] "); fflush(stderr); if (fgets(answer, sizeof(answer), stdin) == NULL) return 0; return answer[0] == 'y' || answer[0] == 'Y'; } /* * confirm_timing_report_overwrite - ask before replacing a timing report. * Returns 1 when the caller confirms, 0 otherwise. */ static int confirm_timing_report_overwrite(void) { char answer[8]; fprintf(stderr, "Existing timing report %s may contain a long diagnostic run.\n", TIMING_REPORT); fprintf(stderr, "Overwrite it with this stop request? [y/N] "); fflush(stderr); if (fgets(answer, sizeof(answer), stdin) == NULL) return 0; return answer[0] == 'y' || answer[0] == 'Y'; } /* * prepare_timing_report_overwrite - protect an existing timing report. * Returns a CLI_EXIT_* value. */ static int prepare_timing_report_overwrite(void) { struct stat st; if (stat(TIMING_REPORT, &st)) { if (errno == ENOENT) return CLI_EXIT_SUCCESS; fprintf(stderr, "Cannot stat %s: %s\n", TIMING_REPORT, strerror(errno)); return CLI_EXIT_IO; } if (!confirm_timing_report_overwrite()) { fprintf(stderr, "Leaving existing timing report unchanged\n"); return CLI_EXIT_NOOP; } if (unlink(TIMING_REPORT) && errno != ENOENT) { fprintf(stderr, "Cannot remove %s: %s\n", TIMING_REPORT, strerror(errno)); return CLI_EXIT_IO; } return CLI_EXIT_SUCCESS; } /* * check_metric_reset_strategy - verify reset intent against on-disk config. * @reset_metrics: reset intent flag to clear when manual reset is unlikely. * Returns nothing. */ static void check_metric_reset_strategy(int *reset_metrics) { conf_t disk_config; const char *strategy; if (!*reset_metrics) return; memset(&disk_config, 0, sizeof(disk_config)); set_message_mode(MSG_STDERR, DBG_NO); if (load_daemon_config(&disk_config)) { fprintf(stderr, "Unable to verify reset_strategy in %s; sending a " "plain --check-metrics request.\n", CONFIG_FILE); fprintf(stderr, "The daemon's active setting may differ from the " "on-disk configuration.\n"); *reset_metrics = 0; free_daemon_config(&disk_config); set_message_mode(MSG_QUIET, DBG_NO); return; } strategy = lookup_reset_strategy(disk_config.reset_strategy); if (disk_config.reset_strategy != RESET_MANUAL) { fprintf(stderr, "On-disk reset_strategy is %s, not manual; " "--reset-metrics appears unable to reset metrics.\n", strategy ? strategy : "unknown"); fprintf(stderr, "Sending a plain --check-metrics request. The daemon's " "active setting may differ until config is reloaded.\n"); *reset_metrics = 0; } free_daemon_config(&disk_config); set_message_mode(MSG_QUIET, DBG_NO); } /* * send_state_report_signal - request a report from fapolicyd. * @pid: daemon PID to signal. * @intent: report intent to send. * @reason: error text buffer. * @reason_len: size of @reason. * Returns 0 on success, non-zero on failure. */ static int send_state_report_signal(unsigned int pid, report_intent_t intent, char *reason, size_t reason_len) { union sigval value; value.sival_int = intent; if (sigqueue(pid, SIGUSR1, value)) { snprintf(reason, reason_len, "signal failed: %s", strerror(errno)); return 1; } return 0; } /* * send_timing_signal - send a SIGUSR1 timing intent to fapolicyd. * @pid: daemon PID to signal. * @intent: timing intent to send. * @reason: error text buffer. * @reason_len: size of @reason. * Returns 0 on success, non-zero on failure. */ static int send_timing_signal(unsigned int pid, report_intent_t intent, char *reason, size_t reason_len) { union sigval value; value.sival_int = intent; if (sigqueue(pid, SIGUSR1, value)) { snprintf(reason, reason_len, "signal failed: %s", strerror(errno)); return 1; } return 0; } /* * get_daemon_pid - read and validate the fapolicyd pid file. * @pid: output pid on success. * @reason: error text buffer. * @reason_len: size of @reason. * Returns 0 on success, non-zero on failure. */ static int get_daemon_pid(unsigned int *pid, char *reason, size_t reason_len) { fd_fgets_state_t *st; int pidfd; st = fd_fgets_init(); if (!st) { snprintf(reason, reason_len, "internal allocation failure"); return 1; } pidfd = open(pidfile, O_RDONLY); if (pidfd >= 0) { char pid_buf[16]; int read_rc; read_rc = fd_fgets_r(st, pid_buf, sizeof(pid_buf), pidfd); if (read_rc > 0) { char exe_buf[64]; errno = 0; *pid = strtoul(pid_buf, NULL, 10); if (errno) { snprintf(reason, reason_len, "bad pid in pid file"); goto err_out; } if (get_program_from_pid(*pid, sizeof(exe_buf), exe_buf) == NULL) { snprintf(reason, reason_len, "can't read proc file"); goto err_out; } if (strcmp(basename(exe_buf), "fapolicyd")) { snprintf(reason, reason_len, "pid file doesn't point to fapolicyd"); goto err_out; } close(pidfd); fd_fgets_destroy(st); return 0; } if (read_rc < 0) snprintf(reason, reason_len, "can't read pid file"); else snprintf(reason, reason_len, "unreadable pid file"); } else snprintf(reason, reason_len, "no pid file"); err_out: if (pidfd >= 0) close(pidfd); fd_fgets_destroy(st); return 1; } /* * display_report_file - wait for a daemon report and write it to stdout. * @path: report path to read. * @reason: output error reason on failure. * Returns 0 on success, non-zero on timeout or I/O failure. */ static int display_report_file(const char *path, const char **reason) { fd_fgets_state_t *st; unsigned int tries = 0; int rpt_fd; st = fd_fgets_init(); if (!st) { *reason = "internal allocation failure"; return 1; } retry: sleep(1); rpt_fd = open(path, O_RDONLY); if (rpt_fd < 0) { if (tries < 25) { tries++; goto retry; } *reason = "timed out waiting for report"; fd_fgets_destroy(st); return 1; } fd_fgets_clear_r(st); for (;;) { char buf[80]; int read_rc; read_rc = fd_fgets_r(st, buf, sizeof(buf), rpt_fd); /* fd_fgets_r() leaves buf and EOF unchanged on read errors. */ if (read_rc < 0) { *reason = "failed reading report"; close(rpt_fd); fd_fgets_destroy(st); return 1; } if (read_rc > 0) write(1, buf, strlen(buf)); if (fd_fgets_eof_r(st)) break; } close(rpt_fd); fd_fgets_destroy(st); return 0; } /* * do_timing_control - request a manual decision timing control action. * @intent: timing intent to send. * Returns a CLI_EXIT_* value. */ static int do_timing_control(report_intent_t intent) { const char *reason; unsigned int pid; char signal_reason[80]; if (get_daemon_pid(&pid, signal_reason, sizeof(signal_reason))) { printf("Can't find fapolicyd: %s\n", signal_reason); return CLI_EXIT_DAEMON_IPC; } if (intent == REPORT_INTENT_TIMING_STOP) { int rc = prepare_timing_report_overwrite(); if (rc != CLI_EXIT_SUCCESS) return rc; } if (send_timing_signal(pid, intent, signal_reason, sizeof(signal_reason))) { printf("Can't signal fapolicyd: %s\n", signal_reason); return CLI_EXIT_DAEMON_IPC; } if (intent == REPORT_INTENT_TIMING_ARM) printf("Decision timing start requested\n"); else if (display_report_file(TIMING_REPORT, &reason)) { printf("Can't read decision timing report: %s\n", reason); return CLI_EXIT_DAEMON_IPC; } return CLI_EXIT_SUCCESS; } /* * do_status_report - request and display a daemon report. * @intent: report intent to send. * @reset_metrics: non-zero when the user requested a metrics reset. * Returns a CLI_EXIT_* value. */ static int do_status_report(report_intent_t intent, int reset_metrics) { const char *reason = "no pid file"; const char *report_path; unsigned int pid; char signal_reason[80]; int fd; if (get_daemon_pid(&pid, signal_reason, sizeof(signal_reason))) { printf("Can't find fapolicyd: %s\n", signal_reason); return CLI_EXIT_DAEMON_IPC; } check_metric_reset_strategy(&reset_metrics); if (reset_metrics && !assume_yes && !confirm_metric_reset()) return CLI_EXIT_NOOP; if (reset_metrics) intent = REPORT_INTENT_RESET_METRICS; else if (intent == REPORT_INTENT_RESET_METRICS) intent = REPORT_INTENT_METRICS; report_path = intent == REPORT_INTENT_STATUS ? STAT_REPORT : METRICS_REPORT; // delete the old report unlink(report_path); // send the signal for the report if (send_state_report_signal(pid, intent, signal_reason, sizeof(signal_reason))) { printf("Can't signal fapolicyd: %s\n", signal_reason); return CLI_EXIT_DAEMON_IPC; } // Access a file to provoke a response fd = open(CONFIG_FILE, O_RDONLY); if (fd >= 0) close(fd); if (display_report_file(report_path, &reason)) { printf("Can't read daemon report: %s\n", reason); return CLI_EXIT_DAEMON_IPC; } return CLI_EXIT_SUCCESS; } #ifdef HAVE_LIBRPM static int do_test_filter(const char *path) { set_message_mode(MSG_STDERR, DBG_NO); filter_set_trace(stdout); if (filter_init()) { fprintf(stderr, "filter_init failed\n"); return CLI_EXIT_RULE_FILTER; } if (filter_load_file(FILTER_FILE)) { filter_destroy(); fprintf(stderr, "filter_load_file failed\n"); return CLI_EXIT_RULE_FILTER; } filter_check(path); filter_destroy(); return CLI_EXIT_SUCCESS; } #endif /* * run_cli_command - execute the requested CLI command. * @arg_count: number of sanitized arguments. * @args: sanitized argument vector. * Returns a CLI_EXIT_* status code. */ static int run_cli_command(int arg_count, char **args) { int opt, option_index, rc = CLI_EXIT_GENERIC; if (arg_count == 1) { fprintf(stderr, "Too few arguments\n\n"); fprintf(stderr, "%s", usage); return CLI_EXIT_USAGE; } optind = 1; /* Run getopt_long on the sanitized copy so command parsing behaves * exactly as before --verbose was introduced. */ opt = getopt_long(arg_count, (char * const *)args, "Ddf:ht:T:lury", long_opts, &option_index); if (assume_yes && opt != 11) goto args_err; switch (opt) { case 'd': if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP)) return CLI_EXIT_IO; rc = do_delete_db(); break; case 'D': if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP)) return CLI_EXIT_IO; rc = do_dump_db(); break; case 'f': if (lint_rules) goto args_err; if (arg_count > 7) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT)) return CLI_EXIT_IO; // fapolicyd-cli, -f, | operation, path ... // skip the first two args rc = do_manage_files(arg_count-2, args+2); break; case 'h': if (lint_rules) goto args_err; printf("%s", usage); rc = CLI_EXIT_SUCCESS; break; case 't': case 'T': if (lint_rules) goto args_err; if (arg_count > 3) goto args_err; rc = do_ftype(optarg, opt == 'T'); break; case 'l': if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP)) return CLI_EXIT_IO; rc = do_list(); break; case 'u': if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP)) return CLI_EXIT_IO; rc = do_reload(DB); break; case 'r': if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP)) return CLI_EXIT_IO; rc = do_reload(RULES); break; // Now the pure long options case 1: { // --check-config if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP)) return CLI_EXIT_IO; set_message_mode(MSG_STDERR, DBG_YES); reset_config(); if (load_daemon_config(&config)) { reset_config(); fprintf(stderr, "Configuration errors reported\n"); return CLI_EXIT_PATH_CONFIG; } else { printf("Daemon config is OK\n"); reset_config(); return CLI_EXIT_SUCCESS; } } break; case 2: // --check-watch_fs if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP)) return CLI_EXIT_IO; return check_watch_fs(); break; case 3: // --check-trustdb if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP)) return CLI_EXIT_IO; return check_trustdb(); break; case 4: // --check-status if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT)) return CLI_EXIT_IO; return do_status_report(REPORT_INTENT_STATUS, 0); break; case 5: // --check-path if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP)) return CLI_EXIT_IO; return check_path(); break; case 14: // --check-metrics if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT)) return CLI_EXIT_IO; return do_status_report(REPORT_INTENT_METRICS, 0); break; case 15: // --compact-trustdb if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP)) return CLI_EXIT_IO; return do_reload(COMPACT); break; case 7: { // --check-ignore_mounts const char *override = optarg; if (lint_rules) goto args_err; if (override == NULL && optind < arg_count && args[optind][0] != '-') override = args[optind++]; if (optind < arg_count) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP)) return CLI_EXIT_IO; return check_ignore_mounts(override); } break; case 9: { // --check-rules const char *path = optarg; if (path == NULL && optind < arg_count && args[optind][0] != '-') path = args[optind++]; if (optind < arg_count) goto args_err; if ((path == NULL || !strcmp(path, OLD_RULES_FILE) || !strcmp(path, RULES_FILE)) && cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP)) return CLI_EXIT_IO; return check_rules_file(path, lint_rules); } break; case 10: goto args_err; case 11: // --reset-metrics if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT)) return CLI_EXIT_IO; return do_status_report(REPORT_INTENT_RESET_METRICS, 1); case 12: // --timing-start if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT)) return CLI_EXIT_IO; return do_timing_control(REPORT_INTENT_TIMING_ARM); case 13: // --timing-stop if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT)) return CLI_EXIT_IO; return do_timing_control(REPORT_INTENT_TIMING_STOP); case 'y': goto args_err; #ifdef HAVE_LIBRPM case 6: { // --test-filter if (lint_rules) goto args_err; if (arg_count > 3) goto args_err; if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP)) return CLI_EXIT_IO; return do_test_filter(optarg); } break; #endif default: printf("%s", usage); rc = CLI_EXIT_USAGE; } return rc; args_err: fprintf(stderr, "Too many arguments\n\n"); fprintf(stderr, "%s", usage); return CLI_EXIT_USAGE; } int main(int argc, char * const argv[]) { int arg_count = 0; char **args; int rc; args = calloc(argc + 1, sizeof(*args)); if (args == NULL) { fprintf(stderr, "Cannot allocate argument list\n"); return CLI_EXIT_GENERIC; } for (int i = 0; i < argc; i++) { if (strcmp(argv[i], "--verbose") == 0) { verbose = true; continue; } if (strcmp(argv[i], "--lint") == 0) { lint_rules = true; continue; } if (strcmp(argv[i], "--yes") == 0 || strcmp(argv[i], "-y") == 0) { assume_yes = true; continue; } args[arg_count++] = argv[i]; } args[arg_count] = NULL; rc = run_cli_command(arg_count, args); free(args); return rc; } fapolicyd-2.0/src/cli/file-cli.c000066400000000000000000000151131524071644000165170ustar00rootroot00000000000000/* * file-cli.c - implementation of CLI option file * Copyright (c) 2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Zoltan Fridrich */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include "llist.h" #include "message.h" #include "string-util.h" #include "trust-file.h" #include "filter.h" #include "file-cli.h" #define FTW_NOPENFD 1024 #define FTW_FLAGS (FTW_ACTIONRETVAL | FTW_PHYS) static list_t add_list; static int ftw_add_list_append(const char *fpath, const struct stat *sb __attribute__ ((unused)), int typeflag, struct FTW *ftwbuf __attribute__ ((unused))) { if (typeflag == FTW_F) { if (S_ISREG(sb->st_mode)) { char *tmp = strdup(fpath); if (!tmp) { errno = ENOMEM; return FTW_STOP; } if (list_append(&add_list, tmp, NULL)) { free(tmp); errno = ENOMEM; return FTW_STOP; } } else { msg(LOG_INFO, "Skipping non regular file: %s", fpath); } } else if (typeflag == FTW_SL) { char target[PATH_MAX]; ssize_t len = readlink(fpath, target, sizeof (target) - 1); if (len == -1) { msg(LOG_ERR, "Cannot read value of symbolic link %s: %s", fpath, strerror(errno)); return FTW_CONTINUE; } target[len] = '\0'; struct stat st; if (stat(fpath, &st) == -1) { msg(LOG_WARNING, "Cannot stat symbolic link %s pointing to %s: %s", fpath, target, strerror(errno)); } else if (target[0] != '/' && realpath(fpath, target) == NULL) { msg(LOG_WARNING, "Cannot resolve symbolic link %s: %s", fpath, strerror(errno)); } else { msg(LOG_INFO, "Skipping symbolic link %s: " "consider adding target %s", fpath, target); } } return FTW_CONTINUE; } /** * Load path into add_list. If path is a directory, * loads all regular files within the directory tree * * @param path Path to load into add_list * @return CLI_EXIT_SUCCESS on success, CLI_EXIT_IO for filesystem problems, * and CLI_EXIT_INTERNAL on allocation failures */ static int add_list_load_path(const char *path) { int fd = open(path, O_RDONLY|O_NONBLOCK); if (fd < 0) { msg(LOG_ERR, "Cannot open %s", path); return CLI_EXIT_IO; } struct stat sb; if (fstat(fd, &sb)) { msg(LOG_ERR, "Cannot stat %s", path); close(fd); return CLI_EXIT_IO; } close(fd); int rc; if (S_ISDIR(sb.st_mode)) rc = nftw(path, &ftw_add_list_append, FTW_NOPENFD, FTW_FLAGS); else if (S_ISREG(sb.st_mode)) { char *tmp = strdup(path); if (!tmp) return CLI_EXIT_INTERNAL; rc = list_append(&add_list, tmp, NULL); if (rc) free(tmp); } else { msg(LOG_INFO, "Skipping non regular file: %s", path); rc = 0; } if (rc) { if (errno == ENOMEM) return CLI_EXIT_INTERNAL; return CLI_EXIT_IO; } return CLI_EXIT_SUCCESS; } /** * Validate trust file name provided by the CLI. * * @param fname Name of trust file within trust.d directory * @return true if \p fname is a simple filename, false otherwise */ static bool trust_file_name_valid(const char *fname) { if (fname == NULL || fname[0] == '\0') return false; if (strchr(fname, '/')) return false; if (strcmp(fname, ".") == 0 || strcmp(fname, "..") == 0) return false; return true; } int file_append(const char *path, const char *fname, bool use_filter) { int rc; set_message_mode(MSG_STDERR, DBG_NO); list_init(&add_list); rc = add_list_load_path(path); if (rc) { list_empty(&add_list); // could be partially populated by nftw return rc; } if (use_filter && filter_prune_list(&add_list, NULL)) { list_empty(&add_list); return CLI_EXIT_RULE_FILTER; } trust_file_rm_duplicates_all(&add_list); if (add_list.count == 0) { msg(LOG_ERR, "After removing duplicates, there is nothing to add"); return CLI_EXIT_NOOP; } if (fname && !trust_file_name_valid(fname)) { msg(LOG_ERR, "Invalid trust file name: %s", fname); list_empty(&add_list); return CLI_EXIT_USAGE; } char *dest = fname ? fapolicyd_strcat(TRUST_DIR_PATH, fname) : TRUST_FILE_PATH; if (dest == NULL) { list_empty(&add_list); return CLI_EXIT_INTERNAL; } rc = trust_file_append(dest, &add_list); list_empty(&add_list); if (fname) free(dest); return rc ? CLI_EXIT_IO : CLI_EXIT_SUCCESS; } int file_delete(const char *path, const char *fname) { int count = 0, rc; set_message_mode(MSG_STDERR, DBG_NO); if (fname && !trust_file_name_valid(fname)) { msg(LOG_ERR, "Invalid trust file name: %s", fname); return CLI_EXIT_USAGE; } if (fname) { char *file = fapolicyd_strcat(TRUST_DIR_PATH, fname); if (file) { count = trust_file_delete_path(file, path); free(file); } else return CLI_EXIT_INTERNAL; } else { count = trust_file_delete_path_all(path); } if (count < 0) rc = CLI_EXIT_PATH_CONFIG; else if (count == 0) { msg(LOG_ERR, "%s is not in the trust database", path); rc = CLI_EXIT_NOOP; } else rc = CLI_EXIT_SUCCESS; return rc; } int file_update(const char *path, const char *fname, bool use_filter) { set_message_mode(MSG_STDERR, DBG_NO); int count = 0, rc = CLI_EXIT_SUCCESS; bool filter_ready = false; if (fname && !trust_file_name_valid(fname)) { msg(LOG_ERR, "Invalid trust file name: %s", fname); return CLI_EXIT_USAGE; } if (use_filter) { if (filter_init()) return CLI_EXIT_RULE_FILTER; if (filter_load_file(NULL)) { filter_destroy(); return CLI_EXIT_RULE_FILTER; } filter_ready = true; } if (fname) { char *file = fapolicyd_strcat(TRUST_DIR_PATH, fname); if (file) { count = trust_file_update_path(file, path, use_filter); free(file); } else count = -1; } else { count = trust_file_update_path_all(path, use_filter); } if (filter_ready) filter_destroy(); if (count < 0) rc = CLI_EXIT_PATH_CONFIG; else if (count == 0) { msg(LOG_ERR, "%s is not in the trust database", path); rc = CLI_EXIT_NOOP; } return rc; } fapolicyd-2.0/src/cli/file-cli.h000066400000000000000000000070111524071644000165220ustar00rootroot00000000000000/* * file-backend.h - Header file for CLI option file * Copyright (c) 2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * Radovan Sroka * Zoltan Fridrich */ #ifndef FILE_CLI_H #define FILE_CLI_H #include enum cli_exit_status { CLI_EXIT_SUCCESS = 0, CLI_EXIT_GENERIC = 1, CLI_EXIT_USAGE = 2, CLI_EXIT_PATH_CONFIG = 3, CLI_EXIT_DB_ERROR = 4, CLI_EXIT_RULE_FILTER = 5, CLI_EXIT_DAEMON_IPC = 6, CLI_EXIT_IO = 7, CLI_EXIT_INTERNAL = 8, CLI_EXIT_NOOP = 9, }; /** * Append a path into the file trust database * * @param path Path to append into the file trust database * @param fname Filename where \p path should be written. If NULL, then * \p path is written into fapolicyd.trust file. Otherwise, * write \p path into file \p fname within the trust.d directory * @param use_filter When true, apply the filter configuration to the list of * files gathered from \p path before writing anything * @return CLI_EXIT_SUCCESS on success, CLI_EXIT_NOOP when no new entries are * added, CLI_EXIT_RULE_FILTER for filter failures, CLI_EXIT_INTERNAL on * allocation failures, and CLI_EXIT_IO for filesystem errors. */ int file_append(const char *path, const char *fname, bool use_filter); /** * Delete a path from the file trust database. * It matches all occurrances so that a directory may be passed and * all parts of it get deleted * * @param path Path to delete from the file trust database * @param fname Filename from which \p path should be deleted. If NULL, then * \p path is deleted from fapolicyd.trust file. Otherwise, * deletes \p path from file \p fname within the trust.d directory * @return CLI_EXIT_SUCCESS on success, CLI_EXIT_NOOP when nothing is removed, * CLI_EXIT_IO for filesystem errors, and CLI_EXIT_PATH_CONFIG when trust * files cannot be parsed. */ int file_delete(const char *path, const char *fname); /** * Update a path in the file trust database. * It matches all occurrances so that a directory may be passed and * all parts of it get updated * * @param path Path to update in the file trust database * @param fname Filename in which \p path should be updated. If NULL, then * \p path is updated in fapolicyd.trust file. Otherwise, * updates \p path in file \p fname within the trust.d directory * @param use_filter When true, apply the filter configuration to the list of * files being updated so that filtered paths are skipped * @return CLI_EXIT_SUCCESS on success, CLI_EXIT_NOOP when nothing is updated, * CLI_EXIT_RULE_FILTER for filter parsing errors, CLI_EXIT_IO for * filesystem errors, and CLI_EXIT_PATH_CONFIG when trust files cannot be * parsed. */ int file_update(const char *path, const char *fname, bool use_filter); #endif fapolicyd-2.0/src/cli/ignore-mounts.c000066400000000000000000000617611524071644000176530ustar00rootroot00000000000000/* * ignore-mounts.c - CLI ignore_mounts scanner * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "avl.h" #include "conf.h" #include "daemon-config.h" #include "file-cli.h" #include "file.h" #include "ignore-mounts.h" #include "llist.h" #include "message.h" #include "paths.h" #include "string-util.h" extern bool verbose; extern conf_t config; enum risk_category { RISK_EXECUTABLE_REGULAR, RISK_ELF_SHARED, RISK_ARCHIVE, RISK_BYTECODE, RISK_PLUGIN_RUNTIME_DIR, RISK_LANGUAGE, RISK_CATEGORY_COUNT }; #define RISK_BIT(risk) (1U << (risk)) struct risk_counts { unsigned long total_entries; unsigned long category[RISK_CATEGORY_COUNT]; }; struct mount_scan_state { const avl_tree_t *languages; struct risk_counts *counts; int had_error; }; static struct mount_scan_state scan_state; static const char * const risk_labels[RISK_CATEGORY_COUNT] = { "executable regular files", "ELF/shared objects", "archives/JARs/ZIPs", "bytecode caches", "plugin/runtime directories", "language/interpreter files", }; /* * reset_ignore_mounts_config - release CLI config used during the scan. * Returns nothing. */ static void reset_ignore_mounts_config(void) { free_daemon_config(&config); memset(&config, 0, sizeof(config)); } /* * append_mount_entry - duplicate an ignore_mounts entry into a list. * @mount: trimmed ignore_mounts entry. * @data: list receiving duplicated entries. * Returns 0 on success and 1 on allocation failure. */ static int append_mount_entry(const char *mount, void *data) { list_t *mounts = data; char *copy = strdup(mount); if (copy == NULL) return 1; if (list_append(mounts, copy, NULL)) { free(copy); return 1; } return 0; } /* * populate_mount_list - split ignore_mounts string into individual entries. * @ignore_list: comma separated mount list from the configuration. * @mounts: list that receives duplicated mount paths. * Returns 0 on success and 1 on allocation failure. */ static int populate_mount_list(const char *ignore_list, list_t *mounts) { int rc; if (ignore_list == NULL) return 0; rc = iterate_ignore_mounts(ignore_list, append_mount_entry, mounts); if (rc) { list_empty(mounts); return 1; } return 0; } /* * path_basename - return the final path component without modifying path. * @path: path to inspect. * Returns a pointer inside @path. */ static const char *path_basename(const char *path) { const char *slash = strrchr(path, '/'); return slash ? slash + 1 : path; } /* * name_in_list - compare a name against a NULL terminated list. * @name: file or directory basename. * @list: NULL terminated list of lowercase names. * Returns 1 when @name matches a list entry, 0 otherwise. */ static int name_in_list(const char *name, const char * const *list) { for (unsigned int i = 0; list[i]; i++) { if (strcasecmp(name, list[i]) == 0) return 1; } return 0; } /* * path_has_suffix - case-insensitive suffix check for risk extensions. * @path: path to inspect. * @suffix: file suffix, including the leading period. * Returns 1 when @path ends in @suffix, 0 otherwise. */ static int path_has_suffix(const char *path, const char *suffix) { size_t path_len = strlen(path); size_t suffix_len = strlen(suffix); if (path_len < suffix_len) return 0; return strcasecmp(path + path_len - suffix_len, suffix) == 0; } /* * path_has_suffix_list - check a path against known risk extensions. * @path: path to inspect. * @suffixes: NULL terminated list of suffixes. * Returns 1 when any suffix matches, 0 otherwise. */ static int path_has_suffix_list(const char *path, const char * const *suffixes) { for (unsigned int i = 0; suffixes[i]; i++) { if (path_has_suffix(path, suffixes[i])) return 1; } return 0; } /* * path_has_component - find an exact component in a slash separated path. * @path: path to inspect. * @component: component name to find. * Returns 1 when the component is present, 0 otherwise. */ static int path_has_component(const char *path, const char *component) { const char *match = path; size_t component_len = strlen(component); while ((match = strstr(match, component))) { int left_ok = match == path || match[-1] == '/'; int right_ok = match[component_len] == 0 || match[component_len] == '/'; if (left_ok && right_ok) return 1; match += component_len; } return 0; } struct language_entry { avl_t avl; char *mime; }; /* * compare_language_entry - compare two MIME tree nodes alphabetically. * @a: first tree entry for comparison. * @b: second tree entry for comparison. * Returns <0 when @a sorts before @b, >0 when it sorts after, and 0 when they * match. */ static int compare_language_entry(void *a, void *b) { const struct language_entry *la = a; const struct language_entry *lb = b; return strcmp(la->mime, lb->mime); } /* * insert_language_mime - add a MIME string to the %languages tree. * @languages: AVL tree tracking the known MIME values. * @mime: MIME string trimmed from the rules file. * Returns 0 on success, including an already-present MIME, and 1 on an * invalid argument or allocation failure. */ static int insert_language_mime(avl_tree_t *languages, const char *mime) { struct language_entry *entry; struct language_entry key = { .mime = (char *)mime, }; avl_t *ret; if (languages == NULL || mime == NULL) return 1; /* Package fragments may repeat a MIME already present in the base set. */ if (avl_search(languages, &key.avl) != NULL) return 0; entry = malloc(sizeof(*entry)); if (entry == NULL) return 1; entry->mime = strdup(mime); if (entry->mime == NULL) { free(entry); return 1; } ret = avl_insert(languages, &entry->avl); if (ret != &entry->avl) { free(entry->mime); free(entry); } return 0; } /* * free_language_mimes - release all nodes stored in the MIME AVL tree. * @languages: AVL tree previously filled by load_language_mimes(). */ static void free_language_mimes(avl_tree_t *languages) { while (languages->root) { struct language_entry *entry; entry = (struct language_entry *) avl_remove(languages, languages->root); if (entry == NULL) break; free(entry->mime); free(entry); } } /* * mime_is_language - check whether MIME belongs to the %languages macro. * @languages: AVL tree built from %languages. * @mime: MIME string returned by file type detection. * Returns 1 on match, 0 otherwise. */ static int mime_is_language(const avl_tree_t *languages, const char *mime) { struct language_entry key = { .mime = (char *)mime, }; if (languages == NULL || mime == NULL) return 0; return avl_search(languages, &key.avl) != NULL; } /* * mime_is_elf_shared - classify ELF, shared library, and object MIME names. * @mime: MIME string returned by file type detection. * Returns 1 when the MIME is ELF-related, 0 otherwise. */ static int mime_is_elf_shared(const char *mime) { static const char * const elf_mimes[] = { "application/x-bad-elf", "application/x-executable", "application/x-object", "application/x-pie-executable", "application/x-sharedlib", NULL }; return mime && name_in_list(mime, elf_mimes); } /* * mime_is_archive - classify archive, compressed archive, JAR, and ZIP MIME. * @mime: MIME string returned by file type detection. * Returns 1 when the MIME is archive-like, 0 otherwise. */ static int mime_is_archive(const char *mime) { static const char * const archive_mimes[] = { "application/gzip", "application/java-archive", "application/vnd.android.package-archive", "application/vnd.ms-cab-compressed", "application/vnd.rar", "application/x-archive", "application/x-7z-compressed", "application/x-bzip", "application/x-bzip2", "application/x-compress", "application/x-cpio", "application/x-gzip", "application/x-java-jmod", "application/x-java-archive", "application/x-java-pack200", "application/x-lrzip", "application/x-lzip", "application/x-rar", "application/x-rpm", "application/x-stuffit", "application/x-tar", "application/x-xz", "application/x-xar", "application/x-zip", "application/x-zoo", "application/x-zstd", "application/zstd", "application/zip", NULL }; return mime && name_in_list(mime, archive_mimes); } /* * mime_is_bytecode - classify bytecode MIME names. * @mime: MIME string returned by file type detection. * Returns 1 when the MIME is bytecode-like, 0 otherwise. */ static int mime_is_bytecode(const char *mime) { static const char * const bytecode_mimes[] = { "application/java-vm", "application/wasm", "application/x-bytecode.python", "application/x-elc", "application/x-java-applet", "application/x-lua-bytecode", "application/x-python-bytecode", NULL }; if (mime == NULL) return 0; if (strstr(mime, "bytecode")) return 1; return name_in_list(mime, bytecode_mimes); } /* * path_is_archive - classify archive-like paths by extension. * @path: path to inspect. * Returns 1 when the suffix indicates an archive, 0 otherwise. */ static int path_is_archive(const char *path) { static const char * const archive_suffixes[] = { ".7z", ".apk", ".bz2", ".ear", ".egg", ".gz", ".jar", ".rar", ".tar", ".tar.bz2", ".tar.gz", ".tar.xz", ".tar.zst", ".tbz", ".tbz2", ".tgz", ".txz", ".war", ".whl", ".xz", ".zip", ".zst", NULL }; return path_has_suffix_list(path, archive_suffixes); } /* * path_is_bytecode - classify bytecode files and cache paths. * @path: path to inspect. * Returns 1 when the path indicates bytecode, 0 otherwise. */ static int path_is_bytecode(const char *path) { static const char * const bytecode_suffixes[] = { ".class", ".elc", ".luac", ".pyc", ".pyo", ".wasm", NULL }; return path_has_component(path, "__pycache__") || path_has_suffix_list(path, bytecode_suffixes); } /* * path_is_plugin_runtime_dir - classify plugin or runtime dependency dirs. * @path: directory path to inspect. * Returns 1 when the basename is a known plugin/runtime directory. */ static int path_is_plugin_runtime_dir(const char *path) { static const char * const runtime_dirs[] = { ".venv", "add-ons", "addons", "bower_components", "dist-packages", "extension", "extensions", "gems", "node_modules", "pear", "pecl", "perl5", "plugin", "plugins", "site-packages", "vendor", "venv", "virtualenv", NULL }; return name_in_list(path_basename(path), runtime_dirs); } /* * classify_file_risks - classify a regular file into risk categories. * @path: file path being scanned. * @sb: stat data for the file. * @mime: MIME string returned by file type detection. * @languages: AVL tree built from %languages. * Returns a bitmask of risk categories. */ static unsigned int classify_file_risks(const char *path, const struct stat *sb, const char *mime, const avl_tree_t *languages) { unsigned int risks = 0; if (sb->st_mode & 0111) risks |= RISK_BIT(RISK_EXECUTABLE_REGULAR); if (mime_is_elf_shared(mime)) risks |= RISK_BIT(RISK_ELF_SHARED); if (mime_is_archive(mime) || path_is_archive(path)) risks |= RISK_BIT(RISK_ARCHIVE); if (mime_is_bytecode(mime) || path_is_bytecode(path)) risks |= RISK_BIT(RISK_BYTECODE); if (mime_is_language(languages, mime)) risks |= RISK_BIT(RISK_LANGUAGE); return risks; } /* * classify_dir_risks - classify a directory into risk categories. * @path: directory path being scanned. * Returns a bitmask of risk categories. */ static unsigned int classify_dir_risks(const char *path) { unsigned int risks = 0; if (strcasecmp(path_basename(path), "__pycache__") == 0) risks |= RISK_BIT(RISK_BYTECODE); if (path_is_plugin_runtime_dir(path)) risks |= RISK_BIT(RISK_PLUGIN_RUNTIME_DIR); return risks; } /* * print_verbose_risks - print one verbose risk entry. * @path: file or directory path that matched. * @risks: risk bitmask for the path. * @mime: optional MIME string for regular files. * Returns nothing. */ static void print_verbose_risks(const char *path, unsigned int risks, const char *mime) { int first = 1; printf("%s: ", path); for (unsigned int i = 0; i < RISK_CATEGORY_COUNT; i++) { if ((risks & RISK_BIT(i)) == 0) continue; printf("%s%s", first ? "" : ", ", risk_labels[i]); first = 0; } if (mime && *mime) printf(" (%s)", mime); putchar('\n'); } /* * record_risk_entry - update counters and optionally print a verbose entry. * @path: file or directory path that matched. * @risks: risk bitmask for the path. * @mime: optional MIME string for regular files. * Returns nothing. */ static void record_risk_entry(const char *path, unsigned int risks, const char *mime) { if (risks == 0) return; if (verbose) print_verbose_risks(path, risks, mime); if (scan_state.counts) { scan_state.counts->total_entries++; for (unsigned int i = 0; i < RISK_CATEGORY_COUNT; i++) { if (risks & RISK_BIT(i)) scan_state.counts->category[i]++; } } } /* * add_risk_counts - add per-mount counts into an aggregate. * @total: aggregate counts to update. * @mount: per-mount counts to add. * Returns nothing. */ static void add_risk_counts(struct risk_counts *total, const struct risk_counts *mount) { total->total_entries += mount->total_entries; for (unsigned int i = 0; i < RISK_CATEGORY_COUNT; i++) total->category[i] += mount->category[i]; } /* * print_risk_summary - print the per-mount risk summary. * @mount: mount point that was scanned. * @counts: risk counts collected while scanning the mount. * Returns nothing. */ static void print_risk_summary(const char *mount, const struct risk_counts *counts) { printf("Summary for %s:\n", mount); printf(" total risky entries: %lu\n", counts->total_entries); for (unsigned int i = 0; i < RISK_CATEGORY_COUNT; i++) printf(" %s: %lu\n", risk_labels[i], counts->category[i]); } /* * print_skipped_summary - print a summary for a mount that was not scanned. * @mount: mount point or configured path that was skipped. * Returns nothing. */ static void print_skipped_summary(const char *mount) { printf("Summary for %s:\n", mount); printf(" scan skipped\n"); } /* * append_language_mimes - add a comma-separated MIME list to a tree. * @languages: AVL tree populated with MIME type strings. * @values: comma-separated MIME type list from a language set line. * * Returns 0 on success and 1 on allocation failure. */ static int append_language_mimes(avl_tree_t *languages, const char *values) { char *tmp; char *ptr, *saved; int rc = 0; tmp = strdup(values); if (tmp == NULL) return 1; ptr = strtok_r(tmp, ",", &saved); while (ptr) { char *mime = fapolicyd_strtrim(ptr); if (*mime && insert_language_mime(languages, mime)) { rc = 1; break; } ptr = strtok_r(NULL, ",", &saved); } free(tmp); return rc; } /* * load_language_mimes_from_file - load %languages and its extensions. * @languages: AVL tree populated with MIME type strings. * @path: compiled or source policy file to read. * * Returns 0 on success, 1 on malformed input, or -1 when @path cannot be * opened. An extension must follow the initial %languages definition. */ static int load_language_mimes_from_file(avl_tree_t *languages, const char *path) { FILE *fp; char *line = NULL; size_t len = 0; int rc = 1; int found = 0; fp = fopen(path, "rm"); if (fp == NULL) return -1; while (getline(&line, &len, fp) != -1) { char *entry = fapolicyd_strtrim(line); const char *values; if (strncmp(entry, "%languages=", 11) == 0) { if (found) goto done; values = entry + 11; } else if (strncmp(entry, "%languages+=", 12) == 0) { if (!found || entry[12] == '\0') goto done; values = entry + 12; } else { continue; } if (append_language_mimes(languages, values)) goto done; found = 1; } if (found) rc = 0; done: if (rc) free_language_mimes(languages); free(line); fclose(fp); return rc; } /* * load_language_mimes - gather MIME types belonging to %languages. * @languages: AVL tree populated with MIME type strings. * @source_path: returns the path used while loading definitions. * * Prefer compiled.rules because it is the policy the daemon actually loads, * including package-installed %languages+= fragments. Fall back to the base * source file only before fagenrules has produced a compiled policy. * Returns 0 on success and 1 on failure. */ static int load_language_mimes(avl_tree_t *languages, const char **source_path) { int rc; *source_path = RULES_FILE; rc = load_language_mimes_from_file(languages, *source_path); if (rc != -1) return rc; *source_path = LANGUAGE_RULES_FILE; rc = load_language_mimes_from_file(languages, *source_path); return rc == 0 ? 0 : 1; } /* * is_mount_point - determine whether the supplied path is a mount point. * @path: directory to inspect. * Returns 1 when the path is mounted, 0 when it is not, and -1 when the * mount table cannot be read. */ static int is_mount_point(const char *path) { FILE *fp; struct mntent *ent; fp = setmntent(MOUNTS_FILE, "r"); if (fp == NULL) return -1; while ((ent = getmntent(fp))) { if (strcmp(ent->mnt_dir, path) == 0) { endmntent(fp); return 1; } } endmntent(fp); return 0; } /* * validate_override_mount - verify CLI override path and copy it to config. * @override: path supplied by the administrator. * Returns 0 on success and 1 on failure. */ static int validate_override_mount(const char *override) { char resolved[PATH_MAX]; char *rpath; struct stat sb; int mount_rc; rpath = realpath(override, resolved); if (rpath == NULL) { fprintf(stderr, "Cannot resolve %s (%s)\n", override, strerror(errno)); return CLI_EXIT_PATH_CONFIG; } if (stat(rpath, &sb) || S_ISDIR(sb.st_mode) == 0) { fprintf(stderr, "%s is not a directory\n", rpath); return CLI_EXIT_PATH_CONFIG; } mount_rc = is_mount_point(rpath); if (mount_rc <= 0) { if (mount_rc == 0) fprintf(stderr, "%s is not a mount point\n", rpath); else fprintf(stderr, "Unable to read %s (%s)\n", MOUNTS_FILE, strerror(errno)); return CLI_EXIT_PATH_CONFIG; } free((void *)config.ignore_mounts); config.ignore_mounts = strdup(rpath); if (config.ignore_mounts == NULL) { fprintf(stderr, "Out of memory\n"); return CLI_EXIT_INTERNAL; } return CLI_EXIT_SUCCESS; } /* * load_ignore_mounts_config - populate ignore_mounts field for scanning. * @override: optional CLI path override. * Returns 0 on success and 1 on failure. */ static int load_ignore_mounts_config(const char *override) { if (override) return validate_override_mount(override); set_message_mode(MSG_STDERR, DBG_YES); if (load_daemon_config(&config)) return CLI_EXIT_PATH_CONFIG; return CLI_EXIT_SUCCESS; } /* * inspect_mount_entry - nftw callback that records risky files and dirs. * @fpath: path of the entry being inspected. * @sb: stat buffer describing the entry. * @typeflag: nftw type flag. * @ftwbuf_unused: unused nftw traversal metadata. * Returns FTW_CONTINUE so the walk keeps running. */ static int inspect_mount_entry(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf_unused __attribute__ ((unused))) { int fd; struct file_info info; struct stat opened; char buf[128]; char *mime; unsigned int risks; /* nftw() does not initialize sb for FTW_NS entries. */ if (typeflag == FTW_NS || typeflag == FTW_DNR) { fprintf(stderr, "Unable to inspect %s during ignored-mount scan\n", fpath); scan_state.had_error = 1; return FTW_CONTINUE; } if (typeflag == FTW_D || typeflag == FTW_DP) { risks = classify_dir_risks(fpath); record_risk_entry(fpath, risks, NULL); return FTW_CONTINUE; } /* Only evaluate regular files discovered during the walk. */ if (typeflag != FTW_F || S_ISREG(sb->st_mode) == 0) return FTW_CONTINUE; /* Open the file and collect metadata for libmagic. */ fd = open_verified_regular_file(fpath, sb, &opened); if (fd < 0) { fprintf(stderr, "Unable to safely open %s (%s)\n", fpath, strerror(errno)); scan_state.had_error = 1; return FTW_CONTINUE; } memset(&info, 0, sizeof(info)); info.device = opened.st_dev; info.inode = opened.st_ino; info.mode = opened.st_mode; info.size = opened.st_size; info.time = opened.st_mtim; mime = get_file_type_from_fd(fd, &info, fpath, sizeof(buf), buf); close(fd); if (mime == NULL) { fprintf(stderr, "Unable to determine mime for %s\n", fpath); scan_state.had_error = 1; return FTW_CONTINUE; } risks = classify_file_risks(fpath, &opened, mime, scan_state.languages); record_risk_entry(fpath, risks, buf); return FTW_CONTINUE; } /* * scan_mount_entry - scan a single ignore_mounts entry for risky content. * @mount: entry from config.ignore_mounts. * @risk_totals: aggregate counters updated with matches. * @override: 0 ignore_mounts list, 1 command line override * Returns 0 when the mount was scanned successfully and 1 when errors * prevent a full scan. */ static int scan_mount_entry(const char *mount, struct risk_counts *risk_totals, int override) { char resolved[PATH_MAX]; char *rpath; struct risk_counts mount_counts = { 0 }; struct stat sb; int rc = CLI_EXIT_SUCCESS; int scanned = 0; rpath = realpath(mount, resolved); if (rpath == NULL) { fprintf(stderr, "Cannot resolve %s (%s)\n", mount, strerror(errno)); print_skipped_summary(mount); return CLI_EXIT_PATH_CONFIG; } if (stat(rpath, &sb)) { fprintf(stderr, "%s does not exist\n", rpath); print_skipped_summary(rpath); return CLI_EXIT_PATH_CONFIG; } if (S_ISDIR(sb.st_mode) == 0) { fprintf(stderr, "%s is not a directory\n", rpath); print_skipped_summary(rpath); return CLI_EXIT_PATH_CONFIG; } const char *warning = NULL; int mount_rc = check_ignore_mount_warning(MOUNTS_FILE, rpath, &warning); if (warning) { if (override && warning[0] == 'i') warning += 20; // skip the ignore_mount part fprintf(stderr, warning, rpath, MOUNTS_FILE); fputc('\n', stderr); } // A warning was already printed - just return if (mount_rc != 1) return CLI_EXIT_PATH_CONFIG; scan_state.counts = &mount_counts; scan_state.had_error = 0; if (nftw(rpath, inspect_mount_entry, 1024, FTW_PHYS)) { fprintf(stderr, "Unable to scan %s (%s)\n", rpath, strerror(errno)); print_skipped_summary(rpath); rc = CLI_EXIT_IO; } else scanned = 1; if (scan_state.had_error) rc = CLI_EXIT_IO; if (scanned) { print_risk_summary(rpath, &mount_counts); add_risk_counts(risk_totals, &mount_counts); } scan_state.counts = NULL; if (!scanned) return rc; return rc; } /* * check_ignore_mounts - validate ignore_mounts entries and scan for matches. * @override: optional mount path provided on the command line. * Returns CLI_EXIT_SUCCESS when no risky entries are found, CLI_EXIT_GENERIC * when risky entries are detected, and other CLI_EXIT_* codes on error. */ int check_ignore_mounts(const char *override) { list_t mounts; avl_tree_t languages; int rc = CLI_EXIT_SUCCESS; struct risk_counts risk_totals = { 0 }; int errors = 0; int file_ready = 0; const char *languages_path; reset_ignore_mounts_config(); list_init(&mounts); avl_init(&languages, compare_language_entry); /* Load ignore_mounts either from the override path or daemon config. */ rc = load_ignore_mounts_config(override); if (rc) goto finish; if (config.ignore_mounts == NULL) { printf("No ignore_mounts entries configured\n"); rc = CLI_EXIT_SUCCESS; goto finish; } if (populate_mount_list(config.ignore_mounts, &mounts)) { fprintf(stderr, "Failed to parse ignore_mounts entries\n"); rc = CLI_EXIT_INTERNAL; goto finish; } if (mounts.first == NULL) { printf("No ignore_mounts entries configured\n"); rc = CLI_EXIT_SUCCESS; goto finish; } /* Build a fast lookup tree of MIME types associated with %languages. */ if (load_language_mimes(&languages, &languages_path)) { fprintf(stderr, "Unable to load %%languages definitions from %s\n", languages_path); rc = CLI_EXIT_RULE_FILTER; goto finish; } /* Initialize libmagic once so nftw() callbacks can reuse it. */ if (file_init()) { fprintf(stderr, "Cannot initialize file helper libraries\n"); rc = CLI_EXIT_INTERNAL; goto finish; } file_ready = 1; scan_state.languages = &languages; /* Walk each ignore_mounts entry and flag risky files or directories. */ for (list_item_t *lptr = mounts.first; lptr; lptr = lptr->next) { int scan_rc = scan_mount_entry(lptr->index, &risk_totals, override ? 1 : 0); if (scan_rc) { errors = 1; if (rc == CLI_EXIT_SUCCESS) rc = scan_rc; } } if (errors == 0 && risk_totals.total_entries == 0) rc = CLI_EXIT_SUCCESS; finish: if (file_ready) file_close(); list_empty(&mounts); free_language_mimes(&languages); scan_state.languages = NULL; scan_state.counts = NULL; scan_state.had_error = 0; reset_ignore_mounts_config(); if (risk_totals.total_entries > 0) return CLI_EXIT_GENERIC; if (errors) return rc; return rc; } fapolicyd-2.0/src/cli/ignore-mounts.h000066400000000000000000000007061524071644000176500ustar00rootroot00000000000000/* * ignore-mounts.h - CLI ignore_mounts scanner * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef IGNORE_MOUNTS_HEADER #define IGNORE_MOUNTS_HEADER int check_ignore_mounts(const char *override); #endif fapolicyd-2.0/src/cli/rule-lint.c000066400000000000000000000251011524071644000167440ustar00rootroot00000000000000/* * rule-lint.c - CLI rule validation and lint checks * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #include "config.h" #include #include #include #include #include #include "file-cli.h" #include "message.h" #include "paths.h" #include "policy.h" #include "rules.h" #include "rule-lint.h" struct lint_path_status { const char *path; int old_rules; int compiled_rules; int stale_old_rules; }; /* * get_rule_line - read one rule file line without its trailing newline. * @f: rule file stream. * Returns a heap-allocated line or NULL at end of file. */ static char *get_rule_line(FILE *f) { char *line = NULL; size_t len = 0; if (getline(&line, &len, f) != -1) { char *ptr = strchr(line, 0x0a); if (ptr) *ptr = 0; return line; } free(line); return NULL; } /* * rule_is_broad_subject - check if a rule has an unrestricted subject side. * @rule: parsed rule to inspect. * Returns 1 when the subject side is all, 0 otherwise. */ static int rule_is_broad_subject(const lnode *rule) { for (unsigned int i = 0; i < rule->s_count; i++) if (rule->s[i].type == ALL_SUBJ) return 1; return 0; } /* * rule_is_broad_object - check if a rule has an unrestricted object side. * @rule: parsed rule to inspect. * Returns 1 when the object side is all, 0 otherwise. */ static int rule_is_broad_object(const lnode *rule) { for (unsigned int i = 0; i < rule->o_count; i++) if (rule->o[i].type == ALL_OBJ) return 1; return 0; } /* * rule_is_deny - check if a rule denies access. * @rule: parsed rule to inspect. * Returns 1 for deny-family decisions, 0 otherwise. */ static int rule_is_deny(const lnode *rule) { return (rule->d & DENY) == DENY; } /* * rule_is_allow - check if a rule allows access. * @rule: parsed rule to inspect. * Returns 1 for allow-family decisions, 0 otherwise. */ static int rule_is_allow(const lnode *rule) { return (rule->d & ALLOW) == ALLOW; } /* * rule_matches_execute - check if a rule can match execute events. * @rule: parsed rule to inspect. * Returns 1 when perm=execute or perm=any, 0 otherwise. */ static int rule_matches_execute(const lnode *rule) { return rule->a == EXEC_ACC || rule->a == ANY_ACC; } /* * rule_matches_open - check if a rule can match open events. * @rule: parsed rule to inspect. * Returns 1 when perm=open or perm=any, 0 otherwise. */ static int rule_matches_open(const lnode *rule) { return rule->a == OPEN_ACC || rule->a == ANY_ACC; } /* * rule_has_language_ftype - check if a rule matches the %languages set. * @rule: parsed rule to inspect. * Returns 1 when an object ftype attribute references %languages. */ static int rule_has_language_ftype(const lnode *rule) { for (unsigned int i = 0; i < rule->o_count; i++) { if (rule->o[i].type != FTYPE || rule->o[i].set == NULL) continue; if (rule->o[i].set->name && strcmp(rule->o[i].set->name, "languages") == 0) return 1; } return 0; } /* * rule_has_only_language_ftype - check for a broad language object match. * @rule: parsed rule to inspect. * Returns 1 when the object side is exactly ftype=%languages. */ static int rule_has_only_language_ftype(const lnode *rule) { return rule->o_count == 1 && rule_has_language_ftype(rule); } /* * print_rule_location - print the rule number and source file location. * @path: rule file path used for parsing. * @rule: parsed rule to locate. */ static void print_rule_location(const char *path, const lnode *rule) { fprintf(stderr, "rule %u at %s:%u", rule->num + 1, path, rule->lineno); } /* * lint_rules_policy - emit policy-shape warnings for default-allow gaps. * @rules: parsed rule list to inspect. * @path: rule file path used for parsing. * @stale_old_rules: non-zero when old rules shadow compiled.rules. * Returns CLI_EXIT_GENERIC when warnings were emitted, CLI_EXIT_SUCCESS * otherwise. Syntax validation is handled before this function runs. */ static int lint_rules_policy(const llist *rules, const char *path, int stale_old_rules) { const lnode *rule; const lnode *last_exec_rule = NULL; const lnode *first_lang_deny = NULL; const lnode *first_open_allow = NULL; int warnings = 0; int has_languages; has_languages = attr_sets_find(rules->sets, "languages") != NULL; for (rule = rules_first_node(rules); rule; rule = rules_next_node(rule)) { if (rule_matches_execute(rule)) last_exec_rule = rule; if (!first_lang_deny && rule_is_deny(rule) && rule_matches_open(rule) && rule_is_broad_subject(rule) && rule_has_only_language_ftype(rule)) first_lang_deny = rule; if (!first_open_allow && rule_is_allow(rule) && rule_matches_open(rule) && rule_is_broad_subject(rule) && rule_is_broad_object(rule)) first_open_allow = rule; } if (stale_old_rules) { fprintf(stderr, "Policy lint warning: %s exists alongside " "%s; fapolicyd loads the old rules file first and " "ignores compiled.rules\n", OLD_RULES_FILE, RULES_FILE); fprintf(stderr, "Policy lint hint: remove %s or migrate it " "into rules.d and rerun fagenrules\n", OLD_RULES_FILE); warnings = 1; } if (!last_exec_rule || !rule_is_deny(last_exec_rule) || !rule_is_broad_subject(last_exec_rule) || !rule_is_broad_object(last_exec_rule)) { fprintf(stderr, "Policy lint warning: executable events can " "fall through; no terminal broad execute deny found"); if (last_exec_rule) { fprintf(stderr, " after "); print_rule_location(path, last_exec_rule); } else fprintf(stderr, " in %s", path); fprintf(stderr, "\nPolicy lint hint: add a final " "\"deny_audit perm=execute all : all\" rule\n"); warnings = 1; } if (!has_languages) { fprintf(stderr, "Policy lint warning: %%languages is not " "defined in %s; programmatic ftype coverage cannot " "be checked\n", path); warnings = 1; } else if (!first_lang_deny) { fprintf(stderr, "Policy lint warning: programmatic opens can " "fall through; no broad %%languages open deny found"); if (first_open_allow) { fprintf(stderr, " before "); print_rule_location(path, first_open_allow); } else fprintf(stderr, " in %s", path); fprintf(stderr, "\nPolicy lint hint: add " "\"deny_audit perm=open all : ftype=%%languages\" " "before broad open allows\n"); warnings = 1; } if (first_open_allow && (!first_lang_deny || first_open_allow->num < first_lang_deny->num)) { fprintf(stderr, "Policy lint warning: broad open allow "); print_rule_location(path, first_open_allow); if (first_lang_deny) { fprintf(stderr, " appears before programmatic-content " "deny "); print_rule_location(path, first_lang_deny); } else fprintf(stderr, " can shadow programmatic-content " "denies"); fprintf(stderr, "\n"); warnings = 1; } if (warnings == 0) printf("Policy lint found no warnings\n"); return warnings ? CLI_EXIT_GENERIC : CLI_EXIT_SUCCESS; } /* * is_active_rules_path - check if a path names an installed rules file. * @path: path supplied by the caller. * Returns 1 when @path names the old or compiled rules file. */ static int is_active_rules_path(const char *path) { return path && (strcmp(path, OLD_RULES_FILE) == 0 || strcmp(path, RULES_FILE) == 0); } /* * select_rules_path - select the rules file and record lint path state. * @requested_path: caller supplied path, or NULL for the active rules file. * @lint_rules: non-zero when policy lint warnings are enabled. * @status: selected path and old/new rule-file state. * Returns CLI_EXIT_SUCCESS when a single candidate was selected. */ static int select_rules_path(const char *requested_path, int lint_rules, struct lint_path_status *status) { memset(status, 0, sizeof(*status)); status->path = requested_path; status->old_rules = access(OLD_RULES_FILE, F_OK) == 0; status->compiled_rules = access(RULES_FILE, F_OK) == 0; if (status->old_rules && status->compiled_rules && (requested_path == NULL || is_active_rules_path(requested_path))) status->stale_old_rules = lint_rules; if (requested_path) return CLI_EXIT_SUCCESS; if (status->old_rules && status->compiled_rules && !lint_rules) { fprintf(stderr, "Error - old and new rules file detected. " "Delete one or the other.\n"); return CLI_EXIT_PATH_CONFIG; } if (status->old_rules) status->path = OLD_RULES_FILE; else status->path = RULES_FILE; return CLI_EXIT_SUCCESS; } /* * check_rules_with_daemon_parser - validate rules through policy.c parser. * @f: opened rule stream positioned at the beginning. * Returns CLI_EXIT_SUCCESS when the daemon parser accepts the stream, or * CLI_EXIT_RULE_FILTER on parser failure. */ static int check_rules_with_daemon_parser(FILE *f) { if (validate_rules_from_stream(f)) return CLI_EXIT_RULE_FILTER; return CLI_EXIT_SUCCESS; } /* * check_rules_file - parse a rules file and optionally lint policy shape. * @path: rule file path to inspect, or NULL for the active rules file. * @lint_rules: non-zero enables policy lint warnings after syntax checks. * Returns CLI_EXIT_SUCCESS when validation passes and lint finds no warnings. */ int check_rules_file(const char *path, int lint_rules) { FILE *f; int rc, lineno = 1, invalid = 0; char *line = NULL; llist temp_rules; struct lint_path_status path_status; unsigned int cnt; set_message_mode(MSG_STDERR, DBG_NO); rc = select_rules_path(path, lint_rules, &path_status); if (rc) return rc; path = path_status.path; f = fopen(path, "r"); if (f == NULL) { fprintf(stderr, "Cannot open rules file %s (%s)\n", path, strerror(errno)); return CLI_EXIT_IO; } rc = check_rules_with_daemon_parser(f); if (rc) { fclose(f); return rc; } rewind(f); if (rules_create(&temp_rules)) { fprintf(stderr, "Failed to create rules list\n"); fclose(f); return CLI_EXIT_INTERNAL; } while ((line = get_rule_line(f))) { rc = rules_append(&temp_rules, line, lineno); if (rc) { fprintf(stderr, "Rule validation failed at line %d\n", lineno); invalid = 1; } free(line); lineno++; } cnt = temp_rules.cnt; fclose(f); if (invalid) { rules_clear(&temp_rules); return CLI_EXIT_RULE_FILTER; } if (cnt == 0) { fprintf(stderr, "No rules found in file\n"); rules_clear(&temp_rules); return CLI_EXIT_RULE_FILTER; } printf("Rules file is valid (%u rules)\n", cnt); if (lint_rules) { fflush(stdout); rc = lint_rules_policy(&temp_rules, path, path_status.stale_old_rules); } else rc = CLI_EXIT_SUCCESS; rules_clear(&temp_rules); return rc; } fapolicyd-2.0/src/cli/rule-lint.h000066400000000000000000000007151524071644000167550ustar00rootroot00000000000000/* * rule-lint.h - CLI rule validation and lint checks * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef RULE_LINT_HEADER #define RULE_LINT_HEADER int check_rules_file(const char *path, int lint_rules); #endif fapolicyd-2.0/src/daemon/000077500000000000000000000000001524071644000153625ustar00rootroot00000000000000fapolicyd-2.0/src/daemon/decision-worker.c000066400000000000000000001543701524071644000206440ustar00rootroot00000000000000/* * decision-worker.c - daemon decision worker pool * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ /* * Overview * -------- * * decision-worker.c owns the daemon's policy-decision worker pool. Its charter * is to create worker slots, route each fanotify permission event to the stable * subject owner, run worker threads, manage worker-local decision contexts, * drain queued and deferred permission events during shutdown, and assemble * queue/defer reports from worker-owned state. * * The state owned here is the worker array, active worker count, worker queues, * worker thread IDs, report timer fd, saved queue metrics, and timing * queue-depth hooks. This file must not own the fanotify permission group, the * kernel event read loop, mount marks, or FAN_Q_OVERFLOW handling; notify.c * owns those boundaries and hands permission metadata to this module only * after it decides the record needs a policy decision. * * The worker pool talks to worker-health.c through a narrow snapshot callback: * this file provides queue metrics, worker IDs, and health objects while the * health module owns the stall algorithm, watchdog ping decisions, and * worker_stall failure action. notify.c passes the fanotify fd and mask as * runtime inputs because workers must answer permission events but must not * manipulate fanotify marks or read kernel batches themselves. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "attr-lookup-metrics.h" #include "conf.h" #include "daemon-config.h" #include "decision-config.h" #include "decision-context.h" #include "decision-defer.h" #include "decision-timing.h" #include "decision-worker.h" #include "event.h" #include "file.h" #include "message.h" #include "policy.h" #include "queue.h" #include "state-report.h" #include "worker-health.h" #define DEFER_RECHECK_INTERVAL_SEC 1 #define DECISION_WORKER_MAX DAEMON_CONFIG_DECISION_THREADS_MAX #define HEALTH_MONITOR_INTERVAL_SEC 3 extern atomic_bool stop, run_stats; extern conf_t config; struct decision_worker { unsigned int id; struct queue *queue; struct decision_context *context; pthread_t thread; atomic_int tid; struct worker_health health; }; static struct queue_metrics last_queue_metrics[DECISION_WORKER_MAX]; static unsigned int last_queue_metrics_count; static struct decision_worker decision_workers[DECISION_WORKER_MAX]; static unsigned int active_decision_workers; static atomic_bool decision_worker_pool_stopping = ATOMIC_VAR_INIT(true); static pthread_mutex_t decision_worker_pool_lock = PTHREAD_MUTEX_INITIALIZER; static unsigned int timing_saved_queue_depth[DECISION_WORKER_MAX]; static int rpt_timer_fd = -1; static unsigned int rpt_interval; static struct decision_worker_runtime worker_runtime = { .fanotify_fd = -1, .fanotify_mask = NULL, .report_interval = 0, }; static void *decision_worker_main(void *arg); static void dispatch_decision_event(struct decision_worker *worker, decision_event_t *event, int *rpt_is_stale); static unsigned int release_ready_deferred_events( struct decision_worker *worker, int *rpt_is_stale); static unsigned int shutdown_deferred_events(struct decision_worker *worker); static unsigned int shutdown_queued_events(struct decision_worker *worker); static void save_last_queue_metrics(void); static int setup_decision_worker(const conf_t *conf, unsigned int worker_id); static void cleanup_worker_setup(unsigned int worker_count); static int worker_owns_reports(const struct decision_worker *worker); static int report_dequeue_timeout(struct decision_worker *worker, const struct timespec *report_timeout, struct timespec *timeout); static unsigned int timing_queue_depth_reset(void *ctx); static unsigned int timing_queue_depth_restore(void *ctx, unsigned int saved); static struct decision_worker *dispatcher_worker_for_metadata( const struct fanotify_event_metadata *metadata, unsigned int *worker_index); static void decision_worker_pool_begin_shutdown(void); /* * decision_worker_pool_active_count - return workers currently receiving * events. * * Returns zero before worker-pool initialization, otherwise the active count. */ unsigned int decision_worker_pool_active_count(void) { return active_decision_workers; } /* * dispatcher_subject_key - choose the stable key used for worker routing. * @metadata: fanotify permission metadata. * * The first worker-pool implementation routes by pid. This deliberately * matches the current subject state model; do not replace it with round-robin * or queue-depth balancing because that would split one subject's startup * sequence across workers. * * Returns a non-negative key suitable for modulo worker selection. */ static unsigned int dispatcher_subject_key( const struct fanotify_event_metadata *metadata) { if (metadata == NULL || metadata->pid <= 0) return 0; return (unsigned int)metadata->pid; } /* * dispatcher_worker_index_from_key - map one subject key to a worker. * * NOTE: This is the only way possible to route incoming events to a worker. * Rerouting to available workers is impossible because we need the same pid * to always land on the same worker so that the pattern detection state * machine can see the whole startup in the same cache. Never change this * algorithm! * * @subject_key: stable key from dispatcher_subject_key(). * @worker_count: active decision workers. * * Returns the selected worker index. A zero worker count falls back to zero so * unit tests can exercise the pure routing calculation without a live daemon. */ static unsigned int dispatcher_worker_index_from_key( unsigned int subject_key, unsigned int worker_count) { if (worker_count == 0) return 0; return subject_key % worker_count; } /* * dispatcher_worker_for_metadata - select the worker for one permission event. * @metadata: fanotify permission metadata. * @worker_index: optional destination for the selected index. * * Returns the selected worker, or NULL when the worker pool is not initialized. */ static struct decision_worker *dispatcher_worker_for_metadata( const struct fanotify_event_metadata *metadata, unsigned int *worker_index) { unsigned int index; index = dispatcher_worker_index_from_key( dispatcher_subject_key(metadata), active_decision_workers); if (worker_index) *worker_index = index; if (index >= active_decision_workers) { errno = ENODEV; return NULL; } if (decision_workers[index].queue == NULL) { errno = ENODEV; return NULL; } return &decision_workers[index]; } /* * decision_worker_pool_enqueue - hand one permission fd to a worker. * @metadata: fanotify metadata read by handle_events(). * @worker_index: optional destination for the selected index. * * On success, the selected worker queue owns the copied metadata and therefore * owns the permission fd reply/close obligation. On failure, ownership stays * with the dispatcher and the caller must answer the fanotify event. * * Returns 0 on success and -1 on failure with errno set. */ int decision_worker_pool_enqueue( const struct fanotify_event_metadata *metadata, unsigned int *worker_index) { struct decision_worker *worker; decision_event_t event; unsigned int index; int rc; if (worker_index) *worker_index = DECISION_EVENT_NO_WORKER; /* * Shutdown holds this lock while it closes admission. That makes the * successful q_enqueue() handoff complete before workers can drain and * close their queues, so the dispatcher never loses a permission fd. */ pthread_mutex_lock(&decision_worker_pool_lock); if (atomic_load_explicit(&decision_worker_pool_stopping, memory_order_acquire)) { errno = ESHUTDOWN; rc = -1; goto out; } worker = dispatcher_worker_for_metadata(metadata, &index); if (worker_index) *worker_index = index; if (worker == NULL) { rc = -1; goto out; } decision_event_init(&event, metadata); event.worker_index = index; rc = q_enqueue(worker->queue, &event); out: pthread_mutex_unlock(&decision_worker_pool_lock); return rc; } /* * decision_worker_pool_begin_shutdown - stop accepting dispatcher handoffs. * * The caller must do this before waking queues for shutdown. The mutex waits * for any in-flight q_enqueue() to publish its event before a worker drains * the queue and exits. * * Returns nothing. */ static void decision_worker_pool_begin_shutdown(void) { pthread_mutex_lock(&decision_worker_pool_lock); atomic_store_explicit(&decision_worker_pool_stopping, true, memory_order_release); pthread_mutex_unlock(&decision_worker_pool_lock); } /* * worker_context - return the decision context owned by a worker. * @worker: decision worker, or NULL in unit-test helper paths. * * Unit tests can exercise queue/defer cleanup without full daemon startup. In * that case the default thread context is used, matching the older single * global-context behavior. * * Returns the worker context or the current thread context fallback. */ static struct decision_context *worker_context(struct decision_worker *worker) { if (worker && worker->context) return worker->context; return decision_context_current(); } /* * worker_defer_queue - return the worker-local subject defer array. * @worker: decision worker whose deferred events should be accessed. * Returns the defer queue owned by the worker context. */ static struct decision_defer_queue *worker_defer_queue( struct decision_worker *worker) { return &worker_context(worker)->defer_queue; } /* * worker_owns_reports - determine whether a worker owns daemon reports. * @worker: decision worker to inspect. * * Interval and signal-triggered reports use shared report state. Keep that * control-plane work on worker 0 while other workers only process decisions. * * Returns 1 when @worker owns reports, 0 otherwise. */ static int worker_owns_reports(const struct decision_worker *worker) { return worker && worker->id == 0; } /* * report_dequeue_timeout - combine report and deferred-event deadlines. * @worker: report-owning decision worker whose defer queue is inspected. * @report_timeout: absolute CLOCK_REALTIME deadline for the next report. * @timeout: destination for the absolute queue wait deadline. * * A deferred permission event may be waiting for stale BUILDING state to be * evicted. The eviction check runs only when deferred events are revisited, so * the report owner must not sleep until a later report deadline and bypass the * one-second defer cadence used by every other worker. * * Returns 1 when deferred events need rechecking, 0 otherwise. */ static int report_dequeue_timeout(struct decision_worker *worker, const struct timespec *report_timeout, struct timespec *timeout) { struct timespec defer_timeout; if (worker == NULL || report_timeout == NULL || timeout == NULL) return 0; *timeout = *report_timeout; if (!decision_defer_has_events(worker_defer_queue(worker))) return 0; if (clock_gettime(CLOCK_REALTIME, &defer_timeout) == 0) { defer_timeout.tv_sec += DEFER_RECHECK_INTERVAL_SEC; if (defer_timeout.tv_sec < timeout->tv_sec || (defer_timeout.tv_sec == timeout->tv_sec && defer_timeout.tv_nsec < timeout->tv_nsec)) *timeout = defer_timeout; } return 1; } /* * setup_decision_worker - initialize one worker slot before threads start. * @conf: daemon configuration. * @worker_id: slot to initialize. * * Worker 0 reuses the context created by init_event_system(). Additional * workers get private contexts and private file-helper state so libmagic, * udev, caches, counters, and defers are not shared across decision threads. * * Returns 0 on success and -1 on failure with errno set when practical. */ static int setup_decision_worker(const conf_t *conf, unsigned int worker_id) { struct decision_context *previous = decision_context_current(); struct decision_worker *worker = &decision_workers[worker_id]; int rc; worker->id = worker_id; worker->queue = NULL; worker->context = NULL; atomic_store_explicit(&worker->tid, 0, memory_order_relaxed); worker_health_init(&worker->health); if (worker_id == 0) worker->context = previous; else { worker->context = decision_context_create(conf); if (worker->context == NULL) return -1; decision_context_set_current(worker->context); rc = file_init(); decision_context_set_current(previous); if (rc) { decision_context_destroy(worker->context); worker->context = NULL; errno = ENOMEM; return -1; } } worker->queue = q_open(conf->q_size); if (worker->queue == NULL) { if (worker_id != 0) { decision_context_destroy(worker->context); worker->context = NULL; } return -1; } decision_context_set_worker_id(worker->context, worker_id); return 0; } /* * cleanup_worker_setup - release partially initialized worker startup state. * @worker_count: number of worker slots that may own resources. * * Returns nothing. */ static void cleanup_worker_setup(unsigned int worker_count) { unsigned int i; for (i = 0; i < worker_count; i++) { struct decision_worker *worker = &decision_workers[i]; if (worker->queue) { q_close(worker->queue); worker->queue = NULL; } if (i != 0 && worker->context) { decision_context_destroy(worker->context); worker->context = NULL; } } active_decision_workers = 0; atomic_store_explicit(&decision_worker_pool_stopping, true, memory_order_release); decision_timing_set_active_workers(0); decision_timing_set_queue_depth_hooks(NULL, NULL, NULL); } /* * decision_worker_pool_open - initialize worker slots and queues. * @conf: daemon configuration. * * Returns 0 on success and -1 after logging and cleaning up setup failures. */ int decision_worker_pool_open(const conf_t *conf) { unsigned int i; atomic_store_explicit(&decision_worker_pool_stopping, true, memory_order_release); active_decision_workers = 0; decision_timing_set_active_workers(0); if (conf->decision_threads == 0 || conf->decision_threads > DECISION_WORKER_MAX) { msg(LOG_ERR, "Invalid decision_threads value %u", conf->decision_threads); exit(1); } for (i = 0; i < conf->decision_threads; i++) { if (setup_decision_worker(conf, i)) { msg(LOG_ERR, "Failed setting up decision worker %u (%s)", i, strerror(errno)); cleanup_worker_setup(i + 1); return -1; } } active_decision_workers = conf->decision_threads; atomic_store_explicit(&decision_worker_pool_stopping, false, memory_order_release); decision_timing_set_active_workers(active_decision_workers); save_last_queue_metrics(); decision_timing_set_queue_depth_hooks(timing_queue_depth_reset, timing_queue_depth_restore, NULL); return 0; } /* * decision_worker_collect_health - snapshot worker inputs for health checks. * @ctx: unused callback context. * @views: destination array supplied by worker-health.c. * @max: maximum number of views available. * * Returns the number of worker views copied. */ static unsigned int decision_worker_collect_health(void *ctx, struct worker_health_view *views, unsigned int max) { unsigned int i, count = 0; (void)ctx; if (views == NULL) return 0; for (i = 0; i < active_decision_workers && count < max; i++) { struct decision_worker *worker = &decision_workers[i]; if (worker->queue == NULL) continue; views[count].id = worker->id; views[count].tid = atomic_load_explicit(&worker->tid, memory_order_relaxed); views[count].health = &worker->health; q_metrics_snapshot(worker->queue, &views[count].metrics); count++; } return count; } /* * decision_worker_health_nudge - wake worker queues after health events. * @ctx: unused callback context. * Returns nothing. */ static void decision_worker_health_nudge(void *ctx) { (void)ctx; decision_worker_pool_nudge(); } /* * decision_worker_pool_start - start workers and health monitoring. * @runtime: fanotify runtime state used by worker-owned replies. * * Returns zero on success or a pthread_create error code after logging and * joining any workers already started. */ int decision_worker_pool_start(const struct decision_worker_runtime *runtime) { unsigned int i, started_workers = 0; int rc; worker_runtime = *runtime; rpt_interval = runtime->report_interval; for (i = 0; i < active_decision_workers; i++) { struct decision_worker *worker = &decision_workers[i]; rc = pthread_create(&worker->thread, NULL, decision_worker_main, worker); if (rc) { msg(LOG_ERR, "Failed to create decision worker %u (%s)", worker->id, strerror(rc)); atomic_store(&stop, true); decision_worker_pool_begin_shutdown(); for (i = 0; i < started_workers; i++) q_shutdown(decision_workers[i].queue); for (i = 0; i < started_workers; i++) pthread_join(decision_workers[i].thread, NULL); return rc; } started_workers++; } msg(LOG_INFO, "Activated %u fanotify decision worker%s", active_decision_workers, active_decision_workers == 1 ? "" : "s"); rc = worker_health_monitor_start(decision_worker_collect_health, decision_worker_health_nudge, NULL); if (rc) { msg(LOG_ERR, "Failed to create health monitor thread (%s)", strerror(rc)); atomic_store(&stop, true); decision_worker_pool_begin_shutdown(); for (i = 0; i < active_decision_workers; i++) q_shutdown(decision_workers[i].queue); for (i = 0; i < active_decision_workers; i++) pthread_join(decision_workers[i].thread, NULL); if (rpt_timer_fd != -1) { close(rpt_timer_fd); rpt_timer_fd = -1; } return rc; } return 0; } /* * queue_metrics_merge - fold one worker queue snapshot into an aggregate. * @aggregate: aggregate metrics being built. * @metrics: worker queue metrics to add. * Returns nothing. */ static void queue_metrics_merge(struct queue_metrics *aggregate, const struct queue_metrics *metrics) { aggregate->current_depth += metrics->current_depth; if (metrics->max_depth > aggregate->max_depth) aggregate->max_depth = metrics->max_depth; aggregate->full_count += metrics->full_count; if (metrics->oldest_age_ns > aggregate->oldest_age_ns) aggregate->oldest_age_ns = metrics->oldest_age_ns; } /* * save_last_queue_metrics - retain final queue snapshots for reports. * Returns nothing. */ static void save_last_queue_metrics(void) { unsigned int i; last_queue_metrics_count = 0; for (i = 0; i < active_decision_workers; i++) { struct decision_worker *worker = &decision_workers[i]; if (worker->queue == NULL) continue; q_metrics_snapshot(worker->queue, &last_queue_metrics[i]); last_queue_metrics_count = i + 1; } } /* * decision_worker_pool_shutdown - stop workers after fanotify group close. * Returns nothing. */ void decision_worker_pool_shutdown(void) { unsigned int i; decision_worker_pool_begin_shutdown(); for (i = 0; i < active_decision_workers; i++) q_shutdown(decision_workers[i].queue); for (i = 0; i < active_decision_workers; i++) pthread_join(decision_workers[i].thread, NULL); worker_health_monitor_join(); } /* * decision_worker_pool_close - release worker queues after normal shutdown. * Returns nothing. */ void decision_worker_pool_close(void) { unsigned int i; save_last_queue_metrics(); for (i = 0; i < active_decision_workers; i++) { struct decision_context *ctx = decision_workers[i].context; if (ctx == NULL) continue; decision_defer_metrics_snapshot_reset(&ctx->defer_queue, &ctx->last_defer_metrics, 0); } decision_timing_set_queue_depth_hooks(NULL, NULL, NULL); decision_timing_set_active_workers(0); for (i = 0; i < active_decision_workers; i++) { if (decision_workers[i].queue == NULL) continue; q_close(decision_workers[i].queue); decision_workers[i].queue = NULL; decision_workers[i].context = NULL; } active_decision_workers = 0; if (rpt_timer_fd != -1) { close(rpt_timer_fd); rpt_timer_fd = -1; } worker_runtime.fanotify_fd = -1; worker_runtime.fanotify_mask = NULL; worker_runtime.report_interval = 0; } /* * decision_worker_pool_discard - release setup state before daemon exit. * Returns nothing. */ void decision_worker_pool_discard(void) { decision_worker_pool_begin_shutdown(); if (rpt_timer_fd != -1) { close(rpt_timer_fd); rpt_timer_fd = -1; } cleanup_worker_setup(active_decision_workers); worker_runtime.fanotify_fd = -1; worker_runtime.fanotify_mask = NULL; worker_runtime.report_interval = 0; } /* * decision_worker_pool_nudge - wake all worker queues without stopping them. * * Report and signal paths use this to interrupt timed queue waits. The actual * shutdown state is changed only by decision_worker_pool_shutdown(), after the * dispatcher has finished handing off its current fanotify batch. * Returns nothing. */ void decision_worker_pool_nudge(void) { unsigned int i; for (i = 0; i < active_decision_workers; i++) q_shutdown(decision_workers[i].queue); } /* * timing_queue_depth_reset - reset timing run max queue depth. * @ctx: unused. * * Returns the largest max-depth value saved across worker queues. */ static unsigned int timing_queue_depth_reset(void *ctx) { unsigned int i, saved, aggregate = 0; (void)ctx; for (i = 0; i < active_decision_workers; i++) { struct decision_worker *worker = &decision_workers[i]; if (worker->queue == NULL) { timing_saved_queue_depth[i] = 0; continue; } saved = q_max_depth_snapshot_reset(worker->queue); timing_saved_queue_depth[i] = saved; if (saved > aggregate) aggregate = saved; } return aggregate; } /* * timing_queue_depth_restore - snapshot timing run queue depth and restore. * @ctx: unused. * @saved: aggregate value returned by timing_queue_depth_reset(). * * Returns the largest max depth observed across worker queues during the run. */ static unsigned int timing_queue_depth_restore(void *ctx, unsigned int saved) { unsigned int i, current, aggregate = 0; (void)ctx; (void)saved; for (i = 0; i < active_decision_workers; i++) { struct decision_worker *worker = &decision_workers[i]; if (worker->queue == NULL) continue; current = q_max_depth_snapshot_restore(worker->queue, timing_saved_queue_depth[i]); if (current > aggregate) aggregate = current; timing_saved_queue_depth[i] = 0; } return aggregate; } struct defer_worker_snapshot { unsigned int worker_id; struct decision_defer_metrics metrics; }; struct defer_report_snapshot { struct decision_defer_metrics metrics; struct defer_worker_snapshot workers[DECISION_WORKER_MAX]; unsigned int worker_count; int reset; }; /* * defer_metrics_merge - fold one worker defer snapshot into an aggregate. * @aggregate: aggregate defer metrics reported to operators. * @metrics: metrics copied from one worker-owned defer array. * Returns nothing. */ static void defer_metrics_merge(struct decision_defer_metrics *aggregate, const struct decision_defer_metrics *metrics) { aggregate->capacity += metrics->capacity; aggregate->current_depth += metrics->current_depth; aggregate->deferred_events += metrics->deferred_events; aggregate->max_depth += metrics->max_depth; aggregate->fallbacks += metrics->fallbacks; if (metrics->oldest_age_ns > aggregate->oldest_age_ns) aggregate->oldest_age_ns = metrics->oldest_age_ns; } /* * defer_report_snapshot_context - snapshot one worker defer array. * @ctx: worker context being sampled. * @data: struct defer_report_snapshot aggregate. * Returns nothing. */ static void defer_report_snapshot_context(struct decision_context *ctx, void *data) { struct defer_report_snapshot *snapshot = data; struct decision_defer_metrics metrics; unsigned int index; if (ctx == NULL || snapshot == NULL) return; decision_defer_metrics_snapshot_reset(&ctx->defer_queue, &metrics, snapshot->reset); ctx->last_defer_metrics = metrics; defer_metrics_merge(&snapshot->metrics, &metrics); index = snapshot->worker_count; if (index < DECISION_WORKER_MAX) { snapshot->workers[index].worker_id = ctx->worker_id; snapshot->workers[index].metrics = metrics; snapshot->worker_count++; } } /* * defer_report_snapshot_reset - snapshot defer counters across workers. * @snapshot: destination for aggregate and per-worker defer metrics. * @reset: non-zero resets interval counters after copying them. * Returns nothing. */ static void defer_report_snapshot_reset(struct defer_report_snapshot *snapshot, int reset) { if (snapshot == NULL) return; memset(snapshot, 0, sizeof(*snapshot)); snapshot->reset = reset; decision_context_for_each(defer_report_snapshot_context, snapshot); } /* * defer_worker_snapshot_sort - order worker defer snapshots by worker id. * @snapshot: defer report snapshot to sort. * Returns nothing. */ static void defer_worker_snapshot_sort(struct defer_report_snapshot *snapshot) { unsigned int i, j; if (snapshot == NULL) return; for (i = 0; i < snapshot->worker_count; i++) { for (j = i + 1; j < snapshot->worker_count; j++) { unsigned int left = snapshot->workers[i].worker_id; unsigned int right = snapshot->workers[j].worker_id; if (left <= right) continue; { struct defer_worker_snapshot tmp = snapshot->workers[i]; snapshot->workers[i] = snapshot->workers[j]; snapshot->workers[j] = tmp; } } } } /* * decision_worker_pool_queue_report - write worker queue metrics. * @f: output stream. * Returns nothing. */ void decision_worker_pool_queue_report(FILE *f) { decision_worker_pool_queue_report_reset(f, 0); } /* * decision_worker_pool_queue_report_reset - write worker queue metrics. * @f: output stream. * @reset: non-zero resets interval counters after copying them. * Returns nothing. */ void decision_worker_pool_queue_report_reset(FILE *f, int reset) { struct queue_metrics aggregate = { 0 }; struct queue_metrics metrics[DECISION_WORKER_MAX]; unsigned int worker_ids[DECISION_WORKER_MAX]; unsigned int count = 0; unsigned int i; if (f == NULL) return; if (active_decision_workers) { for (i = 0; i < active_decision_workers; i++) { struct decision_worker *worker = &decision_workers[i]; if (worker->queue == NULL) continue; q_metrics_snapshot_reset(worker->queue, &metrics[count], reset); worker_ids[count] = worker->id; queue_metrics_merge(&aggregate, &metrics[count]); count++; } } else { for (i = 0; i < last_queue_metrics_count; i++) { metrics[count] = last_queue_metrics[i]; worker_ids[count] = i; queue_metrics_merge(&aggregate, &metrics[count]); count++; } } q_metrics_report(f, &aggregate); for (i = 0; i < count; i++) q_metrics_report_worker(f, worker_ids[i], &metrics[i]); { struct defer_report_snapshot defer_snapshot; defer_report_snapshot_reset(&defer_snapshot, reset); decision_defer_metrics_report(f, &defer_snapshot.metrics); if (defer_snapshot.worker_count) { defer_worker_snapshot_sort(&defer_snapshot); fprintf(f, "\nPer-worker subject defer activity:\n"); for (i = 0; i < defer_snapshot.worker_count; i++) decision_defer_metrics_report_worker(f, defer_snapshot.workers[i].worker_id, &defer_snapshot.workers[i].metrics); } } } /* * decision_worker_pool_queue_health_report - write per-worker health lines. * @f: report stream. * Returns nothing. */ void decision_worker_pool_queue_health_report(FILE *f) { unsigned int i; if (f == NULL) return; if (active_decision_workers) { for (i = 0; i < active_decision_workers; i++) { struct queue_metrics metrics; struct decision_worker *worker = &decision_workers[i]; if (worker->queue == NULL) continue; worker_health_report(f, worker->id, &worker->health); q_metrics_snapshot(worker->queue, &metrics); q_metrics_report_worker(f, worker->id, &metrics); } return; } for (i = 0; i < last_queue_metrics_count; i++) q_metrics_report_worker(f, i, &last_queue_metrics[i]); } /* * decision_worker_pool_defer_config_report - write startup defer capacity. * @f: report stream. * Returns nothing. */ void decision_worker_pool_defer_config_report(FILE *f) { struct defer_report_snapshot snapshot; struct decision_defer_metrics metrics; if (f == NULL) return; defer_report_snapshot_reset(&snapshot, 0); metrics = snapshot.metrics; if (snapshot.worker_count) metrics.capacity /= snapshot.worker_count; decision_defer_config_report(f, &metrics); } /* * decision_worker_pool_defer_fallback_report - write defer fallback health. * @f: report stream. * Returns nothing. */ void decision_worker_pool_defer_fallback_report(FILE *f) { struct defer_report_snapshot snapshot; if (f == NULL) return; defer_report_snapshot_reset(&snapshot, 0); decision_defer_fallback_report(f, &snapshot.metrics); } /* * decision_worker_pool_defer_age_report - write oldest deferred event age. * @f: report stream. * Returns nothing. */ void decision_worker_pool_defer_age_report(FILE *f) { struct defer_report_snapshot snapshot; if (f == NULL) return; defer_report_snapshot_reset(&snapshot, 0); decision_defer_age_report(f, &snapshot.metrics); } /* * decision_worker_pool_defer_health_report - write defer health indicators. * @f: report stream. * Returns nothing. */ void decision_worker_pool_defer_health_report(FILE *f) { struct defer_report_snapshot snapshot; if (f == NULL) return; defer_report_snapshot_reset(&snapshot, 0); decision_defer_health_report(f, &snapshot.metrics); } // disable interval reports, used on unrecoverable errors static void rpt_disable(const char *why) { rpt_interval = 0; if (rpt_timer_fd != -1) { close(rpt_timer_fd); rpt_timer_fd = -1; } msg(LOG_INFO, "interval reports disabled; %s", why); } // initialize interval reporting static void rpt_init(struct timespec *t) { struct itimerspec rpt_deadline; t->tv_nsec = t->tv_sec = 0; rpt_timer_fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK); if (rpt_timer_fd == -1) { rpt_disable("timer create failure"); return; } if (clock_gettime(CLOCK_REALTIME, t)) { // gettime errors are unrecoverable rpt_disable("clock failure"); return; } t->tv_sec += rpt_interval; rpt_deadline.it_interval.tv_sec = rpt_interval; rpt_deadline.it_interval.tv_nsec = 0; rpt_deadline.it_value = *t; if (timerfd_settime(rpt_timer_fd, TFD_TIMER_ABSTIME, &rpt_deadline, NULL) == -1) { // settime errors are unrecoverable rpt_disable(strerror(errno)); } else { msg(LOG_INFO, "interval reports configured; %us", rpt_interval); } } /* * run_decision_event - execute one policy decision for an event envelope. * @worker: decision worker that owns the event and reply fd. * @event: event to process. * * Timing starts only when an event is actually processed. A deferred event * keeps its original queue timestamp so queue wait includes time spent parked * behind a building subject. */ static void run_decision_event(struct decision_worker *worker, decision_event_t *event) { attr_lookup_metrics_set_worker(worker->id); worker_health_event_begin(&worker->health); decision_timing_decision_begin(worker->id); decision_timing_queue_dequeued(event->enqueue_ns); make_policy_decision(event, worker_runtime.fanotify_fd, *worker_runtime.fanotify_mask); decision_timing_decision_end(); worker_health_event_end(&worker->health); } /* * dispatch_decision_event - process one worker-owned event and release defers. * @worker: decision worker that owns the event and reply fd. * @event: event envelope from the inter-thread queue. * @rpt_is_stale: interval report dirty flag. * * If another pid owns the same subject slot while its pattern state is still * before STATE_FULL, the event is parked in the bounded defer array. When the * array is full, processing falls back to the historical eviction behavior so * memory and blocked permission events remain bounded. */ static void dispatch_decision_event(struct decision_worker *worker, decision_event_t *event, int *rpt_is_stale) { struct decision_defer_queue *defer = worker_defer_queue(worker); if (event->worker_index == DECISION_EVENT_NO_WORKER) event->worker_index = worker->id; else if (event->worker_index != worker->id) msg(LOG_WARNING, "Decision event worker ownership mismatch: dispatcher " "assigned worker %u but worker %u dequeued PID %d", event->worker_index, worker->id, event->metadata.pid); // The wrapper may already carry a slot when it comes from the defer list. if (event->subject_slot == DECISION_EVENT_NO_SLOT) event->subject_slot = event_subject_slot(event->metadata.pid); /* * Park only when another pid owns this subject slot and still needs * its startup pattern state. If the array is full, continue into * normal processing so new_event() applies the historical eviction * behavior. */ if (event_subject_slot_is_blocked(event->subject_slot, event->metadata.pid)) { if (decision_defer_push(defer, event) == 0) { *rpt_is_stale = 1; return; } decision_defer_count_fallback(defer); } for (;;) { unsigned int slot; /* * Turn one completed subject slot into a chain of policy * decisions. This lets backed-up events for that slot flow * through immediately instead of waiting for the next fanotify * dequeue cycle. * * Process the current event. This may be the original queue * event or a deferred event popped at the bottom of the loop. */ *rpt_is_stale = 1; run_decision_event(worker, event); /* * make_policy_decision() sets completed_subject_slot only when * processing leaves a slot empty, STATE_FULL, or later. Without * that signal there is no deferred work that can be unblocked. */ slot = event->completed_subject_slot; if (slot == DECISION_EVENT_NO_SLOT) return; /* * A deferred event can start building a fresh subject in this * same slot. Stop if it became blocked again. Otherwise pop * the oldest event waiting for this slot and repeat. * * The loop cannot run forever: every iteration either returns * or removes one entry from the fixed-size defer array. */ if (!event_subject_slot_is_unblocked(slot)) return; if (!decision_defer_pop_slot(defer, slot, event)) return; } } /* * dispatch_dequeued_decision_event - record queue progress before dispatch. * @worker: decision worker that dequeued the event. * @event: event envelope removed from the inter-thread queue. * @rpt_is_stale: interval report dirty flag. * * A successful dequeue is worker progress even when dispatch parks the event * in the defer array and returns without running a policy decision. Refresh * health first so bounded deferral cannot look like a stalled worker. * * Returns nothing. */ static void dispatch_dequeued_decision_event(struct decision_worker *worker, decision_event_t *event, int *rpt_is_stale) { worker_health_heartbeat(&worker->health); dispatch_decision_event(worker, event, rpt_is_stale); } /* * deferred_event_is_ready - test whether a parked event can run now. * @event: deferred event to inspect. * @ctx: unused predicate context. * * Calling event_subject_slot_is_blocked() intentionally reuses the same * traced/stale BUILDING eviction check used by fresh events. Without this * recheck, a deferred event can wait forever when no later event collides with * the same subject slot. * * Returns 1 when the event can run, 0 when it must remain deferred. */ static int deferred_event_is_ready(const decision_event_t *event, void *ctx) { (void)ctx; return !event_subject_slot_is_blocked(event->subject_slot, event->metadata.pid); } /* * release_ready_deferred_events - run deferred events that are unblocked. * @rpt_is_stale: interval report dirty flag. * * Periodic rechecks keep the 10 second BUILDING stale timeout effective even * when no new fanotify event arrives for the same subject slot. Each pass pops * the oldest ready event and dispatches it through the normal decision path. * * Returns the number of deferred events released. */ static unsigned int release_ready_deferred_events( struct decision_worker *worker, int *rpt_is_stale) { struct decision_defer_queue *defer = worker_defer_queue(worker); decision_event_t event; unsigned int count = 0; while (decision_defer_has_events(defer) && decision_defer_pop_if(defer, deferred_event_is_ready, NULL, &event)) { dispatch_decision_event(worker, &event, rpt_is_stale); count++; } if (count) msg(LOG_DEBUG, "Released %u deferred fanotify events", count); return count; } /* * shutdown_fallback_decision - get the shutdown reply decision. * Returns FAN_ALLOW in permissive mode and FAN_DENY otherwise. */ static int shutdown_fallback_decision(void) { if (decision_config_permissive(NULL)) return FAN_ALLOW; return FAN_DENY; } /* * shutdown_queued_events - reply to every event left in a worker queue. * @worker: decision worker whose queue is being drained. * * A decision worker exits its main loop only after worker-pool shutdown starts. * Any permission event that reached the worker queue but was not processed yet * still owns a live fd and can leave the requesting task blocked. During * shutdown, answer those queued events with the same permissive fallback * policy used for other bounded failure paths. * * Returns the number of events answered. */ static unsigned int shutdown_queued_events(struct decision_worker *worker) { decision_event_t event; unsigned int count = 0; int decision = shutdown_fallback_decision(); if (worker == NULL || worker->queue == NULL) return 0; while (q_queue_length(worker->queue) > 0) { if (q_dequeue(worker->queue, &event) != 1) break; reply_event(worker_runtime.fanotify_fd, &event.metadata, decision, NULL); count++; } return count; } /* * shutdown_deferred_events - reply to every event left in the defer array. * * Deferred fanotify permission events still own live fds. During shutdown each * must be answered exactly once, using the same permissive fallback policy as * queue-full handling, so the blocked task and descriptor are released. * * Returns the number of events answered. */ static unsigned int shutdown_deferred_events(struct decision_worker *worker) { struct decision_defer_queue *defer = worker_defer_queue(worker); decision_event_t event; unsigned int count = 0; int decision = shutdown_fallback_decision(); while (decision_defer_pop_any(defer, &event)) { reply_event(worker_runtime.fanotify_fd, &event.metadata, decision, NULL); count++; } return count; } #ifdef TEST_SUBJECT_DEFER void test_notify_worker_pool_destroy(void); /* * test_notify_queue_reset - initialize worker queue state for unit tests. * @entries: fixed queue capacity. * * Returns 0 on success and -1 on allocation failure. */ int test_notify_queue_reset(unsigned int entries) { struct decision_worker *worker = &decision_workers[0]; if (worker->queue != NULL) q_close(worker->queue); worker->id = 0; worker->context = decision_context_current(); decision_context_set_worker_id(worker->context, 0); atomic_store_explicit(&worker->tid, 0, memory_order_relaxed); worker_health_init(&worker->health); worker->queue = q_open(entries); active_decision_workers = worker->queue ? 1 : 0; atomic_store_explicit(&decision_worker_pool_stopping, worker->queue == NULL, memory_order_release); return worker->queue == NULL ? -1 : 0; } /* * test_notify_queue_destroy - release worker queue state after unit tests. * Returns nothing. */ void test_notify_queue_destroy(void) { struct decision_worker *worker = &decision_workers[0]; if (worker->queue == NULL) return; q_close(worker->queue); worker->queue = NULL; worker->context = NULL; active_decision_workers = 0; atomic_store_explicit(&decision_worker_pool_stopping, true, memory_order_release); } /* * test_notify_queue_push - enqueue an event in worker queue state. * @event: event copied into the queue. * * Returns 0 on success and -1 when the queue rejects the event. */ int test_notify_queue_push(const decision_event_t *event) { return q_enqueue(decision_workers[0].queue, event); } /* * test_notify_worker_heartbeat_ns - expose worker health for unit tests. * @worker_id: worker whose heartbeat should be read. * * Returns the raw heartbeat timestamp, or 0 when @worker_id is invalid. */ uint64_t test_notify_worker_heartbeat_ns(unsigned int worker_id) { if (worker_id >= active_decision_workers) return 0; return atomic_load_explicit( &decision_workers[worker_id].health.heartbeat_ns, memory_order_relaxed); } /* * test_notify_worker_set_heartbeat_ns - set worker health for unit tests. * @worker_id: worker whose heartbeat should be changed. * @heartbeat_ns: raw timestamp to store. * * Returns nothing. */ void test_notify_worker_set_heartbeat_ns(unsigned int worker_id, uint64_t heartbeat_ns) { if (worker_id >= active_decision_workers) return; atomic_store_explicit( &decision_workers[worker_id].health.heartbeat_ns, heartbeat_ns, memory_order_relaxed); } /* * test_notify_worker_dequeue_dispatch - run one production dequeue dispatch. * @worker_id: worker queue to drain once. * * Returns 1 when one event was dispatched, 0 for an empty queue, or -1 on * invalid worker/queue state. */ int test_notify_worker_dequeue_dispatch(unsigned int worker_id) { struct decision_worker *worker; decision_event_t event; int rpt_is_stale = 0; int rc; if (worker_id >= active_decision_workers) { errno = EINVAL; return -1; } worker = &decision_workers[worker_id]; if (worker->queue == NULL) { errno = EINVAL; return -1; } rc = q_dequeue(worker->queue, &event); if (rc != 1) return rc; dispatch_dequeued_decision_event(worker, &event, &rpt_is_stale); return 1; } /* * test_notify_shutdown_queued_events - run production queue cleanup. * Returns the number of queued events answered. */ unsigned int test_notify_shutdown_queued_events(void) { return shutdown_queued_events(&decision_workers[0]); } /* * test_notify_defer_reset - initialize worker defer state for unit tests. * @subj_cache_size: subject cache size used to derive defer capacity. * * Returns 0 on success and -1 on allocation failure. */ int test_notify_defer_reset(unsigned int subj_cache_size) { struct decision_worker *worker = &decision_workers[0]; struct decision_defer_queue *defer; worker->id = 0; worker->context = decision_context_current(); decision_context_set_worker_id(worker->context, 0); defer = worker_defer_queue(worker); decision_defer_destroy(defer); return decision_defer_init(defer, subj_cache_size); } /* * test_notify_defer_destroy - release worker defer state after unit tests. * Returns nothing. */ void test_notify_defer_destroy(void) { struct decision_worker *worker = &decision_workers[0]; decision_defer_destroy(worker_defer_queue(worker)); worker->context = NULL; } /* * test_notify_defer_push - park an event in worker defer state. * @event: event copied into the defer queue. * * Returns 0 on success and -1 when the queue rejects the event. */ int test_notify_defer_push(const decision_event_t *event) { return decision_defer_push(worker_defer_queue(&decision_workers[0]), event); } /* * test_notify_shutdown_deferred_events - run production shutdown cleanup. * Returns the number of deferred events answered. */ unsigned int test_notify_shutdown_deferred_events(void) { return shutdown_deferred_events(&decision_workers[0]); } /* * test_notify_worker_index - expose stable subject routing for tests. * @pid: synthetic fanotify pid. * @workers: synthetic active worker count. * * Returns the worker index selected by the dispatcher key function. */ unsigned int test_notify_worker_index(pid_t pid, unsigned int workers) { struct fanotify_event_metadata metadata = { .pid = pid, }; return dispatcher_worker_index_from_key( dispatcher_subject_key(&metadata), workers); } /* * test_notify_worker_pool_reset - initialize multiple worker queues for tests. * @workers: number of synthetic active workers. * @entries: queue capacity per worker. * * Returns 0 on success and -1 on allocation failure. */ int test_notify_worker_pool_reset(unsigned int workers, unsigned int entries) { unsigned int i; if (workers == 0 || workers > DECISION_WORKER_MAX) { errno = EINVAL; return -1; } for (i = 0; i < DECISION_WORKER_MAX; i++) { if (decision_workers[i].queue) { q_close(decision_workers[i].queue); decision_workers[i].queue = NULL; } decision_workers[i].context = NULL; } active_decision_workers = 0; for (i = 0; i < workers; i++) { decision_workers[i].id = i; decision_workers[i].context = decision_context_current(); decision_context_set_worker_id(decision_workers[i].context, i); atomic_store_explicit(&decision_workers[i].tid, 0, memory_order_relaxed); worker_health_init(&decision_workers[i].health); decision_workers[i].queue = q_open(entries); if (decision_workers[i].queue == NULL) { test_notify_worker_pool_destroy(); return -1; } } active_decision_workers = workers; atomic_store_explicit(&decision_worker_pool_stopping, false, memory_order_release); decision_timing_set_active_workers(workers); return 0; } /* * test_notify_worker_pool_destroy - release synthetic worker queues. * Returns nothing. */ void test_notify_worker_pool_destroy(void) { unsigned int i; for (i = 0; i < DECISION_WORKER_MAX; i++) { if (decision_workers[i].queue) { q_close(decision_workers[i].queue); decision_workers[i].queue = NULL; } decision_workers[i].context = NULL; } active_decision_workers = 0; atomic_store_explicit(&decision_worker_pool_stopping, true, memory_order_release); decision_timing_set_active_workers(0); } /* * test_notify_worker_pool_begin_shutdown - close synthetic queue admission. * * Returns nothing. */ void test_notify_worker_pool_begin_shutdown(void) { decision_worker_pool_begin_shutdown(); } /* * test_notify_enqueue_pid_fd - route one synthetic event through dispatcher. * @pid: synthetic fanotify pid. * @event_fd: marker stored in metadata.fd for ordering assertions. * * Returns 0 on successful enqueue and -1 on dispatcher/queue failure. */ int test_notify_enqueue_pid_fd(pid_t pid, int event_fd) { struct fanotify_event_metadata metadata = { .fd = event_fd, .pid = pid, .mask = FAN_OPEN_PERM, }; return decision_worker_pool_enqueue(&metadata, NULL); } /* * test_notify_worker_queue_depth - return synthetic worker queue depth. * @worker_id: worker queue to inspect. * * Returns the current queue depth, or UINT_MAX when @worker_id is invalid. */ unsigned int test_notify_worker_queue_depth(unsigned int worker_id) { if (worker_id >= active_decision_workers || decision_workers[worker_id].queue == NULL) return UINT_MAX; return q_queue_length(decision_workers[worker_id].queue); } /* * test_notify_worker_drain - drain one synthetic worker queue. * @worker_id: worker queue to drain. * @pids: optional destination for dequeued pids. * @fds: optional destination for dequeued metadata fd markers. * @max: maximum entries available in @pids and @fds. * * Returns the number of events drained. */ unsigned int test_notify_worker_drain(unsigned int worker_id, pid_t *pids, int *fds, unsigned int max) { decision_event_t event; unsigned int count = 0; struct queue *queue; if (worker_id >= active_decision_workers) return 0; queue = decision_workers[worker_id].queue; if (queue == NULL) return 0; while (count < max && q_queue_length(queue) > 0) { if (q_dequeue(queue, &event) != 1) break; if (pids) pids[count] = event.metadata.pid; if (fds) fds[count] = event.metadata.fd; count++; } return count; } /* * test_notify_report_timer_init - expose report timer setup to unit tests. * @interval: report interval in seconds. * @timeout: destination for the first queue timeout. * @timer: destination for the active timer state. * * Returns zero after configuring the timer, or -1 on setup/read failure. */ int test_notify_report_timer_init(unsigned int interval, struct timespec *timeout, struct itimerspec *timer) { if (timeout == NULL || timer == NULL) { errno = EINVAL; return -1; } rpt_interval = interval; rpt_init(timeout); if (rpt_timer_fd == -1) return -1; return timerfd_gettime(rpt_timer_fd, timer); } /* * test_notify_report_dequeue_timeout - expose the report/defer deadline. * @report_timeout: synthetic absolute report deadline. * @timeout: destination for the selected queue deadline. * * Returns 1 when worker 0 has deferred events, 0 otherwise. */ int test_notify_report_dequeue_timeout(const struct timespec *report_timeout, struct timespec *timeout) { return report_dequeue_timeout(&decision_workers[0], report_timeout, timeout); } /* * test_notify_report_timer_destroy - release report timer state after tests. * * Returns nothing. */ void test_notify_report_timer_destroy(void) { if (rpt_timer_fd != -1) { close(rpt_timer_fd); rpt_timer_fd = -1; } rpt_interval = 0; } /* * test_notify_report_timer_disable - run the report timer error cleanup. * * Returns nothing. */ void test_notify_report_timer_disable(void) { rpt_disable("test failure"); } /* * test_notify_report_timer_fd - return the active report timer descriptor. * * Returns -1 when report timing is disabled. */ int test_notify_report_timer_fd(void) { return rpt_timer_fd; } #endif /* * decision_worker_main - run policy decisions for one worker queue. * @arg: struct decision_worker pointer. * Returns NULL when the worker exits. */ static void *decision_worker_main(void *arg) { struct decision_worker *worker = arg; sigset_t sigs; int owns_reports = worker_owns_reports(worker); /* This is a worker thread. Don't handle external signals. */ sigemptyset(&sigs); sigaddset(&sigs, SIGTERM); sigaddset(&sigs, SIGHUP); sigaddset(&sigs, SIGUSR1); sigaddset(&sigs, SIGINT); sigaddset(&sigs, SIGQUIT); pthread_sigmask(SIG_SETMASK, &sigs, NULL); if (worker == NULL || worker->queue == NULL || worker->context == NULL) return NULL; atomic_store_explicit(&worker->tid, (int)syscall(SYS_gettid), memory_order_relaxed); worker_health_heartbeat(&worker->health); /* * The decision path uses decision_context_current() in cache, file, and * policy helpers. Bind this thread before any event or report work so * all mutable state stays private to this worker. */ decision_context_set_current(worker->context); // interval reporting state int rpt_is_stale = 0; struct timespec rpt_timeout; // if an interval was configured, reports are enabled if (owns_reports && rpt_interval) rpt_init(&rpt_timeout); // start with a fresh report if (owns_reports) atomic_store_explicit(&run_stats, true, memory_order_relaxed); while (!atomic_load_explicit(&decision_worker_pool_stopping, memory_order_acquire)) { int rc; decision_event_t event; /* * Apply asynchronous timing-control work on the decision * worker. SIGUSR1 handlers and overflow detection only set * atomic request flags; this call starts/stops manual timing, * restores queue-depth accounting, and writes any required * timing report outside signal context. */ decision_timing_process_requests(&config); // if an interval has been configured if (owns_reports && rpt_interval) { struct timespec timeout; int timed_for_defer, timed_out; /* * Never wait directly on rpt_timeout here. Deferred * permission events need their independent stale- * BUILDING recheck deadline. */ timed_for_defer = report_dequeue_timeout(worker, &rpt_timeout, &timeout); errno = 0; rc = q_timed_dequeue(worker->queue, &event, &timeout); if (rc == 0) { uint64_t expired = 0; timed_out = errno == ETIMEDOUT; worker_health_heartbeat(&worker->health); // check for timer expirations if (timed_out) { if (read(rpt_timer_fd, &expired, sizeof(uint64_t)) == -1) { // EAGAIN expected w/nonblocking // timer. Any other error is // unrecoverable. if (errno != EAGAIN) { rpt_disable( strerror(errno)); continue; } } } // timer expired or stats explicitly requested if (expired || atomic_load_explicit(&run_stats, memory_order_relaxed)) { bool stats_requested = atomic_exchange_explicit( &run_stats, false, memory_order_relaxed); // write a new report only when one of // 1. new events seen since last report // 2. explicitly requested w/run_stats if (rpt_is_stale || stats_requested) { state_report_write( state_report_reason_for_triggers( expired)); rpt_is_stale = 0; } // adjust the timed dequeue timeout to // a full interval from now if (clock_gettime(CLOCK_REALTIME, &rpt_timeout)) { // gettime errors are // unrecoverable rpt_disable("clock failure"); continue; } rpt_timeout.tv_sec += rpt_interval; } if (timed_for_defer && timed_out) release_ready_deferred_events( worker, &rpt_is_stale); continue; } if (rc < 0) continue; } else { int timed_for_defer = 0; struct timespec timeout; unsigned int timeout_sec = HEALTH_MONITOR_INTERVAL_SEC; if (decision_defer_has_events( &worker->context->defer_queue)) timeout_sec = DEFER_RECHECK_INTERVAL_SEC; if (clock_gettime(CLOCK_REALTIME, &timeout) == 0) { timeout.tv_sec += timeout_sec; errno = 0; rc = q_timed_dequeue(worker->queue, &event, &timeout); timed_for_defer = decision_defer_has_events( &worker->context->defer_queue); } else { rc = q_dequeue(worker->queue, &event); } if (rc == 0) { worker_health_heartbeat(&worker->health); if (owns_reports && atomic_exchange_explicit(&run_stats, false, memory_order_relaxed)) { state_report_write(STATE_REPORT_SIGNAL); } if (timed_for_defer && errno == ETIMEDOUT) release_ready_deferred_events( worker, &rpt_is_stale); continue; } if (rc < 0) continue; if (owns_reports && atomic_exchange_explicit(&run_stats, false, memory_order_relaxed)) { state_report_write(STATE_REPORT_SIGNAL); } } dispatch_dequeued_decision_event(worker, &event, &rpt_is_stale); } unsigned int queued = shutdown_queued_events(worker); unsigned int deferred = shutdown_deferred_events(worker); if (queued || deferred) msg(LOG_INFO, "Replied to %u queued and %u deferred fanotify events during shutdown", queued, deferred); msg(LOG_DEBUG, "Decision worker %u shutdown backlog: queued=%u deferred=%u", worker->id, queued, deferred); msg(LOG_DEBUG, "Exiting decision worker %u", worker->id); decision_context_set_current(NULL); return NULL; } fapolicyd-2.0/src/daemon/decision-worker.h000066400000000000000000000057531524071644000206510ustar00rootroot00000000000000/* * decision-worker.h - daemon decision worker pool interface * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef DECISION_WORKER_HEADER #define DECISION_WORKER_HEADER #include #include #include #include "conf.h" #include "decision-event.h" struct decision_worker_runtime { int fanotify_fd; const uint64_t *fanotify_mask; unsigned int report_interval; }; int decision_worker_pool_open(const conf_t *conf); int decision_worker_pool_start(const struct decision_worker_runtime *runtime); void decision_worker_pool_shutdown(void); void decision_worker_pool_close(void); void decision_worker_pool_discard(void); int decision_worker_pool_enqueue( const struct fanotify_event_metadata *metadata, unsigned int *worker_index); unsigned int decision_worker_pool_active_count(void); void decision_worker_pool_nudge(void); void decision_worker_pool_queue_report(FILE *f); void decision_worker_pool_queue_report_reset(FILE *f, int reset); void decision_worker_pool_queue_health_report(FILE *f); void decision_worker_pool_defer_config_report(FILE *f); void decision_worker_pool_defer_fallback_report(FILE *f); void decision_worker_pool_defer_age_report(FILE *f); void decision_worker_pool_defer_health_report(FILE *f); #ifdef TEST_SUBJECT_DEFER #include #include int test_notify_queue_reset(unsigned int entries); void test_notify_queue_destroy(void); int test_notify_queue_push(const decision_event_t *event); uint64_t test_notify_worker_heartbeat_ns(unsigned int worker_id); void test_notify_worker_set_heartbeat_ns(unsigned int worker_id, uint64_t heartbeat_ns); int test_notify_worker_dequeue_dispatch(unsigned int worker_id); unsigned int test_notify_shutdown_queued_events(void); int test_notify_defer_reset(unsigned int subj_cache_size); void test_notify_defer_destroy(void); int test_notify_defer_push(const decision_event_t *event); unsigned int test_notify_shutdown_deferred_events(void); unsigned int test_notify_worker_index(pid_t pid, unsigned int workers); int test_notify_worker_pool_reset(unsigned int workers, unsigned int entries); void test_notify_worker_pool_destroy(void); void test_notify_worker_pool_begin_shutdown(void); int test_notify_enqueue_pid_fd(pid_t pid, int event_fd); unsigned int test_notify_worker_queue_depth(unsigned int worker_id); unsigned int test_notify_worker_drain(unsigned int worker_id, pid_t *pids, int *fds, unsigned int max); int test_notify_report_timer_init(unsigned int interval, struct timespec *timeout, struct itimerspec *timer); int test_notify_report_dequeue_timeout(const struct timespec *report_timeout, struct timespec *timeout); void test_notify_report_timer_destroy(void); void test_notify_report_timer_disable(void); int test_notify_report_timer_fd(void); #endif #endif fapolicyd-2.0/src/daemon/fanotify-fs-error.c000066400000000000000000000445261524071644000211150ustar00rootroot00000000000000/* * fanotify-fs-error.c - FAN_FS_ERROR health monitoring * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ /* * Overview * -------- * * FAN_FS_ERROR events are filesystem health notifications delivered through a * fanotify notification group. They are not permission events: there is no file * descriptor to answer and no policy decision to make. The daemon opens a * second, notification-only fanotify group with FAN_REPORT_FID, marks each * watched filesystem for FAN_FS_ERROR, and polls that fd beside the normal * permission-event fd. * * When the health fd becomes readable, this module drains the fanotify records * and parses the variable-length info records attached to each metadata block. * The ERROR info record carries the errno-style failure and the kernel's * suppression count. FID records identify the affected object, but the daemon * currently counts them only so status output can show what kind of payload was * received. Malformed records are still counted as health events because the * kernel reported trouble even if user space could not parse every detail. * * Runtime handling is intentionally observe-only. Each event is recorded in the * shared failure-action counters, the most recent details are published for * state reports, and rate-limited log messages describe the failure. Recording * the failure wakes the report path so operators see the new health signal * promptly without mixing this notification-only path into the permission * queue handled by notify.c. * * Header and kernel support vary across supported build targets. Public entry * points stay available even when FAN_FS_ERROR symbols are absent or configure * disables this monitor; in that case initialization reports that monitoring is * unavailable and all other helpers become harmless no-ops. */ #include "config.h" /* Needed to get O_LARGEFILE definition */ #include #include #include #include #include #include #include #include #include #include #include "escape.h" #include "failure-action.h" #include "fanotify-fs-error.h" #include "gcc-attributes.h" #include "message.h" #include "notify.h" #define FANOTIFY_FS_ERROR_BUFFER_SIZE 8192 #define FS_ERROR_LOG_INTERVAL 60 #if defined(FAPOLICYD_ENABLE_FANOTIFY_FS_ERROR) && \ defined(FAN_FS_ERROR) && defined(FAN_REPORT_FID) && \ defined(FAN_MARK_FILESYSTEM) && \ defined(FAN_EVENT_INFO_TYPE_ERROR) && \ defined(FAN_EVENT_INFO_TYPE_FID) #define FAPOLICYD_HAVE_FANOTIFY_FS_ERROR 1 struct fanotify_fs_error_info_record { struct fanotify_event_info_header hdr; int32_t error; uint32_t error_count; }; #else #define FAPOLICYD_HAVE_FANOTIFY_FS_ERROR 0 #endif struct fanotify_fs_error_details { int valid; int has_error; int malformed; int error; unsigned int error_count; unsigned int info_records; unsigned int fid_records; uint32_t event_len; uint16_t metadata_len; pid_t pid; time_t when; }; extern atomic_bool stop; #if FAPOLICYD_HAVE_FANOTIFY_FS_ERROR extern atomic_bool run_stats; #endif static int fs_error_fd = -1; #if FAPOLICYD_HAVE_FANOTIFY_FS_ERROR static struct message_rate_limit fanotify_fs_error_log = MESSAGE_RATE_LIMIT_INIT(FS_ERROR_LOG_INTERVAL); #endif static pthread_mutex_t fs_error_lock = PTHREAD_MUTEX_INITIALIZER; static struct fanotify_fs_error_details last_fs_error; static const char *fs_error_status( const struct fanotify_fs_error_details *details); static const char *fs_error_code_text(int error); static const char *format_fs_error_time(time_t when, char *buf, size_t buf_size) __attr_access ((__write_only__, 2, 3)); #if FAPOLICYD_HAVE_FANOTIFY_FS_ERROR static int parse_fs_error_record( const struct fanotify_event_metadata *metadata, struct fanotify_fs_error_details *details); static void save_fs_error_details( const struct fanotify_fs_error_details *details); static void log_fs_error_event( const struct fanotify_fs_error_details *details, unsigned long total); static void record_fs_error_event( const struct fanotify_event_metadata *metadata); static void fanotify_fs_error_failure_action(void); static const char *escape_path_for_log(const char *path, char **escaped); #endif /* * getFanotifyFilesystemErrors - return FAN_FS_ERROR health event count. * Returns the number of FAN_FS_ERROR events reported by the kernel. */ unsigned long getFanotifyFilesystemErrors(void) { return failure_action_count(FAILURE_REASON_FANOTIFY_FS_ERROR); } /* * fs_error_status - return a parse status name for status output. * @details: most recent FAN_FS_ERROR details. * Returns a stable status string. */ static const char *fs_error_status( const struct fanotify_fs_error_details *details) { if (!details->valid) return "none"; if (details->malformed) return "malformed"; if (!details->has_error) return "missing_error_record"; return "ok"; } /* * fs_error_code_text - return printable text for a FAN_FS_ERROR errno. * @error: errno-style value reported by the kernel. * Returns strerror text for @error. */ static const char *fs_error_code_text(int error) { if (error < 0) error = -error; return strerror(error); } /* * format_fs_error_time - format a filesystem error timestamp. * @when: timestamp saved with the error details. * @buf: destination buffer. * @buf_size: destination size. * Returns @buf on success, or NULL when @buf cannot be initialized. */ static const char *format_fs_error_time(time_t when, char *buf, size_t buf_size) { struct tm tm; if (buf == NULL || buf_size < 11) return NULL; if (when == 0) { strncpy(buf, "never", buf_size - 1); buf[buf_size - 1] = 0; return buf; } if (localtime_r(&when, &tm) == NULL || strftime(buf, buf_size, "%Y-%m-%d %H:%M:%S %z", &tm) == 0) { strncpy(buf, "unavailable", buf_size - 1); buf[buf_size - 1] = 0; } return buf; } #if FAPOLICYD_HAVE_FANOTIFY_FS_ERROR /* * parse_fs_error_record - parse FAN_FS_ERROR info records. * @metadata: fanotify event metadata from the kernel. * @details: destination for recent error details. * Returns 0 when the event was well-formed and -1 when it was malformed. */ static int parse_fs_error_record( const struct fanotify_event_metadata *metadata, struct fanotify_fs_error_details *details) { const char *event = (const char *)metadata; size_t offset, end; details->event_len = metadata->event_len; details->metadata_len = metadata->metadata_len; details->pid = metadata->pid; details->when = time(NULL); if (details->when == (time_t)-1) details->when = 0; if (metadata->metadata_len < sizeof(*metadata) || metadata->metadata_len > metadata->event_len) { details->malformed = 1; return -1; } end = metadata->event_len; offset = metadata->metadata_len; while (offset + sizeof(struct fanotify_event_info_header) <= end) { const struct fanotify_event_info_header *info; info = (const struct fanotify_event_info_header *) (event + offset); if (info->len < sizeof(*info) || offset + info->len > end) { details->malformed = 1; return -1; } details->info_records++; switch (info->info_type) { case FAN_EVENT_INFO_TYPE_ERROR: if (info->len < sizeof(struct fanotify_fs_error_info_record)) { details->malformed = 1; return -1; } else { const struct fanotify_fs_error_info_record *err; err = (const struct fanotify_fs_error_info_record *) info; details->has_error = 1; details->error = err->error; details->error_count = err->error_count; } break; case FAN_EVENT_INFO_TYPE_FID: details->fid_records++; break; default: break; } offset += info->len; } if (offset != end) { details->malformed = 1; return -1; } if (!details->has_error) return -1; return 0; } /* * save_fs_error_details - publish recent filesystem error details. * @details: details parsed from the current kernel event. * Returns nothing. */ static void save_fs_error_details( const struct fanotify_fs_error_details *details) { pthread_mutex_lock(&fs_error_lock); last_fs_error = *details; pthread_mutex_unlock(&fs_error_lock); } /* * log_fs_error_event - log one rate-limited filesystem health event. * @details: parsed details from the kernel event. * @total: total FAN_FS_ERROR events observed. * Returns nothing. */ static void log_fs_error_event( const struct fanotify_fs_error_details *details, unsigned long total) { time_t now = details->when; if (now == 0) now = time(NULL); if (!message_rate_limit_allow(&fanotify_fs_error_log, now)) return; if (details->has_error) { msg(LOG_ERR, "Filesystem error reported by fanotify: error=%d (%s) " "suppressed=%u pid=%d status=%s " "(fanotify_filesystem_errors=%lu)", details->error, fs_error_code_text(details->error), details->error_count, details->pid, fs_error_status(details), total); } else { msg(LOG_ERR, "Filesystem error reported by fanotify without a " "parseable error record: status=%s event_len=%u " "metadata_len=%u (fanotify_filesystem_errors=%lu)", fs_error_status(details), details->event_len, details->metadata_len, total); } } /* * record_fs_error_event - count and publish a FAN_FS_ERROR health event. * @metadata: fanotify event metadata from the kernel. * Returns nothing. */ static void record_fs_error_event( const struct fanotify_event_metadata *metadata) { struct fanotify_fs_error_details details = { 0 }; unsigned long total; details.valid = 1; parse_fs_error_record(metadata, &details); total = failure_action_record(FAILURE_REASON_FANOTIFY_FS_ERROR); save_fs_error_details(&details); log_fs_error_event(&details, total); fanotify_fs_error_failure_action(); } #endif /* * fanotify_fs_error_report - write recent FAN_FS_ERROR details. * @f: report stream. * Returns nothing. */ void fanotify_fs_error_report(FILE *f) { struct fanotify_fs_error_details details; const char *when_text; char when[64]; if (f == NULL) return; pthread_mutex_lock(&fs_error_lock); details = last_fs_error; pthread_mutex_unlock(&fs_error_lock); fprintf(f, "Filesystem error last status: %s\n", fs_error_status(&details)); when_text = format_fs_error_time(details.when, when, sizeof(when)); if (when_text == NULL) when_text = "unavailable"; fprintf(f, "Filesystem error last seen: %s\n", when_text); if (!details.valid) return; if (details.has_error) { fprintf(f, "Filesystem error last errno: %d\n", details.error); fprintf(f, "Filesystem error last errno text: %s\n", fs_error_code_text(details.error)); fprintf(f, "Filesystem error last suppressed count: %u\n", details.error_count); } fprintf(f, "Filesystem error last pid: %d\n", details.pid); fprintf(f, "Filesystem error last info records: %u\n", details.info_records); fprintf(f, "Filesystem error last fid records: %u\n", details.fid_records); fprintf(f, "Filesystem error last event length: %u\n", details.event_len); fprintf(f, "Filesystem error last metadata length: %u\n", details.metadata_len); } #if FAPOLICYD_HAVE_FANOTIFY_FS_ERROR /* * fanotify_fs_error_failure_action - run the observe-only failure response. * Returns nothing. */ static void fanotify_fs_error_failure_action(void) { /* * FAN_FS_ERROR is a daemon health signal, not a policy decision. Wake * the report path so the signal becomes visible promptly. */ run_stats = true; nudge_queue(); } /* * escape_path_for_log - return a shell-escaped path for logging. * @path: path that may include control characters. * @escaped: optional output pointer to an allocated escaped buffer. * Returns escaped @path when needed, original @path when not needed, * or "" if escaping is needed but allocation fails. */ static const char *escape_path_for_log(const char *path, char **escaped) { size_t escaped_size; if (escaped) *escaped = NULL; escaped_size = check_escape_shell(path); if (escaped_size == 0) return path; if (escaped) *escaped = escape_shell(path, escaped_size); if (escaped && *escaped) return *escaped; return ""; } /* * fanotify_fs_error_close - close the filesystem error fanotify group. * Returns nothing. */ void fanotify_fs_error_close(void) { if (fs_error_fd >= 0) { close(fs_error_fd); fs_error_fd = -1; } } /* * fanotify_fs_error_mark - add one FAN_FS_ERROR filesystem mark. * @path: mount path whose filesystem should be monitored. * Returns 0 on success, -2 when FAN_FS_ERROR is unsupported, and -1 for * per-filesystem failures or disabled monitoring. */ int fanotify_fs_error_mark(const char *path) { char *escaped_path = NULL; const char *safe_path; int saved_errno; if (fs_error_fd < 0 || path == NULL) return -1; safe_path = escape_path_for_log(path, &escaped_path); if (fanotify_mark(fs_error_fd, FAN_MARK_ADD | FAN_MARK_FILESYSTEM, FAN_FS_ERROR, AT_FDCWD, path) == -1) { saved_errno = errno; switch (saved_errno) { case EINVAL: case ENOSYS: msg(LOG_INFO, "FAN_FS_ERROR marks unsupported by running kernel"); free(escaped_path); return -2; case ENODEV: #ifdef EOPNOTSUPP case EOPNOTSUPP: #endif case EXDEV: msg(LOG_DEBUG, "FAN_FS_ERROR monitoring unavailable for %s (%s)", safe_path, strerror(saved_errno)); break; default: msg(LOG_WARNING, "Error (%s) adding FAN_FS_ERROR mark for %s", strerror(saved_errno), safe_path); break; } free(escaped_path); return -1; } msg(LOG_DEBUG, "added %s filesystem error monitor", safe_path); free(escaped_path); return 0; } /* * fanotify_fs_error_unmark - flush the FAN_FS_ERROR mark for one path. * @path: mount path whose filesystem mark should be flushed. * Returns nothing. */ void fanotify_fs_error_unmark(const char *path) { char *escaped_path = NULL; const char *safe_path; if (fs_error_fd < 0 || path == NULL) return; safe_path = escape_path_for_log(path, &escaped_path); if (fanotify_mark(fs_error_fd, FAN_MARK_FLUSH | FAN_MARK_FILESYSTEM, 0, -1, path) == -1) msg(LOG_ERR, "Failed flushing FAN_FS_ERROR path %s (%s)", safe_path, strerror(errno)); free(escaped_path); } /* * fanotify_fs_error_init - initialize notification-only FS error monitoring. * @m: watched mount list. * Returns the fanotify fd when monitoring is active, or -1 when disabled. */ int fanotify_fs_error_init(mlist *m) { const char *path; unsigned int marked = 0; if (m == NULL) return -1; fs_error_fd = fanotify_init(FAN_CLOEXEC | FAN_CLASS_NOTIF | FAN_NONBLOCK | FAN_REPORT_FID, O_RDONLY | O_LARGEFILE | O_CLOEXEC | O_NOATIME); if (fs_error_fd < 0) { if (errno == EINVAL || errno == ENOSYS) msg(LOG_INFO, "FAN_FS_ERROR monitoring unsupported by running " "kernel; disabled"); else msg(LOG_WARNING, "Failed opening FAN_FS_ERROR fanotify fd (%s)", strerror(errno)); return -1; } path = mlist_first(m); while (path && fs_error_fd >= 0) { int rc = fanotify_fs_error_mark(path); if (rc == 0) marked++; else if (rc == -2) { fanotify_fs_error_close(); break; } path = mlist_next(m); } if (fs_error_fd >= 0 && marked == 0) { msg(LOG_INFO, "FAN_FS_ERROR monitoring disabled; no watched " "filesystems accepted error marks"); fanotify_fs_error_close(); } return fs_error_fd; } #else /* * fanotify_fs_error_close - no-op for builds without FAN_FS_ERROR headers. * Returns nothing. */ void fanotify_fs_error_close(void) { fs_error_fd = -1; } /* * fanotify_fs_error_mark - no-op for builds without FAN_FS_ERROR headers. * @path: unused path. * Returns -1 because monitoring is unavailable. */ int fanotify_fs_error_mark(const char *path) { (void)path; return -1; } /* * fanotify_fs_error_unmark - no-op for builds without FAN_FS_ERROR headers. * @path: unused path. * Returns nothing. */ void fanotify_fs_error_unmark(const char *path) { (void)path; } /* * fanotify_fs_error_init - report compile-time FAN_FS_ERROR unavailability. * @m: unused watched mount list. * Returns -1 because monitoring is unavailable. */ int fanotify_fs_error_init(mlist *m) { (void)m; #if defined(FAPOLICYD_ENABLE_FANOTIFY_FS_ERROR) msg(LOG_INFO, "FAN_FS_ERROR monitoring disabled; kernel headers do not provide " "the required fanotify info records"); #else msg(LOG_INFO, "FAN_FS_ERROR monitoring disabled"); #endif return -1; } #endif /* * fanotify_fs_error_fd - return filesystem error notification fd. * Returns a fanotify fd when monitoring is active, or -1 when disabled. */ int fanotify_fs_error_fd(void) { return fs_error_fd; } /* * fanotify_fs_error_handle_event - process one FAN_FS_ERROR metadata record. * @metadata: fanotify event metadata from the kernel. * Returns 1 when the event was consumed, 0 otherwise. */ int fanotify_fs_error_handle_event( const struct fanotify_event_metadata *metadata) { #if FAPOLICYD_HAVE_FANOTIFY_FS_ERROR if (metadata == NULL || (metadata->mask & FAN_FS_ERROR) == 0) return 0; record_fs_error_event(metadata); return 1; #else (void)metadata; return 0; #endif } /* * fanotify_fs_error_handle_events - read filesystem health fanotify events. * Returns nothing. */ void fanotify_fs_error_handle_events(void) { const struct fanotify_event_metadata *metadata; struct fanotify_event_metadata buf[FANOTIFY_FS_ERROR_BUFFER_SIZE]; ssize_t len = -2; if (fs_error_fd < 0) return; while (len < 0) { do { len = read(fs_error_fd, (void *)buf, sizeof(buf)); } while (len == -1 && errno == EINTR && stop == false); if (len == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) return; if (len == -1) { msg(LOG_ERR, "Error receiving fanotify_event (%s)", strerror(errno)); return; } if (stop) return; } metadata = (const struct fanotify_event_metadata *)buf; while (FAN_EVENT_OK(metadata, len)) { if (metadata->vers != FANOTIFY_METADATA_VERSION) { msg(LOG_ERR, "Mismatch of fanotify metadata version"); exit(1); } fanotify_fs_error_handle_event(metadata); metadata = FAN_EVENT_NEXT(metadata, len); } } #ifdef TEST_SUBJECT_DEFER /* * test_fanotify_fs_error_set_fd - install a synthetic fd for read-loop tests. * @test_fd: fd borrowed from the unit test, or -1 to disable reads. * * The test owns @test_fd and must close it after clearing this reference. * Returns nothing. */ void test_fanotify_fs_error_set_fd(int test_fd) { fs_error_fd = test_fd; } #endif fapolicyd-2.0/src/daemon/fanotify-fs-error.h000066400000000000000000000017421524071644000211130ustar00rootroot00000000000000/* * fanotify-fs-error.h - FAN_FS_ERROR health monitoring * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef FANOTIFY_FS_ERROR_HEADER #define FANOTIFY_FS_ERROR_HEADER #include #include #include "mounts.h" int fanotify_fs_error_init(mlist *m); int fanotify_fs_error_mark(const char *path); void fanotify_fs_error_unmark(const char *path); void fanotify_fs_error_close(void); int fanotify_fs_error_fd(void); void fanotify_fs_error_handle_events(void); int fanotify_fs_error_handle_event( const struct fanotify_event_metadata *metadata); unsigned long getFanotifyFilesystemErrors(void); void fanotify_fs_error_report(FILE *f); #ifdef TEST_SUBJECT_DEFER void test_fanotify_fs_error_set_fd(int test_fd); #endif #endif fapolicyd-2.0/src/daemon/fapolicyd.c000066400000000000000000001402311524071644000175010ustar00rootroot00000000000000/* * fapolicyd.c - Main file for the program * Copyright (c) 2016-26 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * Radovan Sroka */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include /* umask */ #include #include #include #include #include /* PATH_MAX */ #include #include #ifdef HAVE_MALLINFO2 #include #endif #include "fanotify-fs-error.h" #include "notify.h" #include "attr-lookup-metrics.h" #include "policy.h" #include "event.h" #include "escape.h" #include "fd-fgets.h" #include "file.h" #include "database.h" #include "message.h" #include "daemon-config.h" #include "decision-config.h" #include "decision-timing.h" #include "conf.h" #include "queue.h" #include "gcc-attributes.h" #include "avl.h" #include "paths.h" #include "string-util.h" #include "filter.h" #include "state-report.h" #include "systemd-notify.h" // Global program variables unsigned int debug_mode = 0; static unsigned int foreground_mode; const char* mounts = MOUNTS_FILE; // Signal handler notifications atomic_bool stop = false, hup = false, run_stats = false; atomic_uint signal_report_requests; atomic_uint signal_report_intent; atomic_uint signal_report_reset_requests; atomic_int signal_report_reset_request_pid = -1; atomic_int signal_report_reset_request_uid = -1; // Global configuration state conf_t config; // This holds info about all file systems to watch struct fs_avl { avl_tree_t index; }; // This is the data about a specific file system to watch typedef struct fs_data { avl_t avl; // This has to be first const char *fs_name; } fs_data_t; static struct fs_avl filesystems; static struct fs_avl ignored_mounts; // List of mounts being watched static mlist *m = NULL; static pthread_mutex_t mlist_lock = PTHREAD_MUTEX_INITIALIZER; static int pid_file_fd = -1; static bool pid_file_created; static bool pid_file_written; // Reconfiguration static atomic_bool reconfig_running = false; // Mount handling static atomic_bool mounts_running = false; static __noreturn void usage(void); /* * install_syscall_filter - install daemon seccomp restrictions. * * Package trust snapshots can be rebuilt after startup. The RPM database * uses SQLite locks that are scoped to a process ID. Loading rpmdb in * fapolicyd, or in one of its threads, lets a close of an rpmdb descriptor * release that process's locks and can corrupt rpmdb during an RPM * transaction. The fixed libexec RPM loader therefore runs in a separate * process and returns a memfd snapshot. * * posix_spawn children inherit the daemon's seccomp filter, so builds with * a package loader must keep execve available for that fixed loader. * * Returns nothing. */ static void install_syscall_filter(void) { scmp_filter_ctx ctx; int rc = -1; ctx = seccomp_init(SCMP_ACT_ALLOW); if (ctx == NULL) goto err_out; #if !defined(USE_RPM) && !defined(USE_DEB) rc = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EACCES), SCMP_SYS(execve), 0); if (rc < 0) goto err_out; # ifdef HAVE_FEXECVE # ifdef __NR_fexecve rc = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EACCES), SCMP_SYS(fexecve), 0); if (rc < 0) goto err_out; # endif # endif #endif rc = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EIO), SCMP_SYS(sendfile), 0); if (rc < 0) goto err_out; rc = seccomp_load(ctx); err_out: if (rc < 0) msg(LOG_ERR, "Failed installing seccomp filter"); seccomp_release(ctx); } static int cmp_fs(void *a, void *b) { return strcmp(((fs_data_t *)a)->fs_name, ((fs_data_t *)b)->fs_name); } /* * free_filesystem - release storage for a single AVL entry. * @s: filesystem data being destroyed. * Returns nothing. */ static void free_filesystem(fs_data_t *s) { free((void *)s->fs_name); free((void *)s); } /* * destroy_filesystem - remove the first node from an AVL tree. * @list: tree holding filesystem style strings. * Returns nothing. */ static void destroy_filesystem(struct fs_avl *list) { avl_t *cur = list->index.root; fs_data_t *tmp =(fs_data_t *)avl_remove(&list->index, cur); if ((avl_t *)tmp != cur) msg(LOG_DEBUG, "string list: removal of invalid node"); free_filesystem(tmp); } /* * destroy_fs_list - free every node from an AVL tree. * @list: tree that should be cleared. * Returns nothing. */ static void destroy_fs_list(struct fs_avl *list) { while (list->index.root) destroy_filesystem(list); } /* * add_filesystem - insert a string into an AVL tree. * @list: tree receiving the entry. * @f: filesystem data to insert. * Returns: 0 on failure, 1 if the item is in the tree. */ static int add_filesystem(struct fs_avl *list, fs_data_t *f) { fs_data_t *tmp=(fs_data_t *)avl_insert(&list->index,(avl_t *)(f)); if (tmp) { if (tmp != f) { // already in the tree, delete the current item msg(LOG_DEBUG, "fs_list: duplicate filesystem found"); free_filesystem(f); } return 1; } return 0; } /* * new_filesystem - allocate and add a string into an AVL tree. * @list: tree receiving the new entry. * @fs: string representing the filesystem or mount point. * Returns 1 on success and 0 on failure. */ static int new_filesystem(struct fs_avl *list, const char *fs) { fs_data_t *tmp = malloc(sizeof(fs_data_t)); if (tmp) { tmp->fs_name = fs ? strdup(fs) : strdup(""); if (tmp->fs_name == NULL) { free(tmp); return 0; } if (add_filesystem(list, tmp) == 0) { free((void *)tmp->fs_name); free(tmp); return 0; } return 1; } return 0; } /* * find_filesystem - search the supplied AVL tree for a string. * @list: tree that should be searched. * @f: string to locate. * Returns a pointer to the stored entry or NULL if not found. */ static fs_data_t *find_filesystem(struct fs_avl *list, const char *f) { fs_data_t tmp; tmp.fs_name = f; return (fs_data_t *)avl_search(&list->index, (avl_t *) &tmp); } /* * add_ignore_mount_entry - callback that validates and stores ignored mounts. * @mount: trimmed ignore_mounts entry. * @unused: unused context pointer. * Returns 0 to continue iterating entries. */ static int add_ignore_mount_entry(const char *mount, void *unused __attribute__ ((unused))) { const char *warning; int rc; rc = check_ignore_mount_warning(mounts, mount, &warning); if (warning) msg(LOG_ERR, warning, mount, mounts); if (rc == 1) { if (!new_filesystem(&ignored_mounts, mount)) msg(LOG_ERR, "Cannot store ignore_mounts entry %s", mount); } return 0; } /* * init_ignore_mounts - create the ignored mount AVL tree. * @ignore_list: comma separated list of mount points to ignore. * Returns nothing. */ static void init_ignore_mounts(const char *ignore_list) { avl_init(&ignored_mounts.index, cmp_fs); if (ignore_list == NULL) return; if (iterate_ignore_mounts(ignore_list, add_ignore_mount_entry, NULL)) { msg(LOG_ERR, "Cannot duplicate ignore_mounts list"); return; } if (ignored_mounts.index.root == NULL) { free((void *)config.ignore_mounts); config.ignore_mounts = NULL; } } /* * mount_is_ignored - check if a mount point is in the ignore list. * @point: mount point path. * Returns 1 when the mount point should be skipped and 0 otherwise. */ static int mount_is_ignored(const char *point) { return find_filesystem(&ignored_mounts, point) ? 1 : 0; } /* * init_fs_list - create the filesystem type AVL tree. * @watch_fs: comma separated list of filesystem types to watch. * Returns 0 on success or 1 when the list cannot be fully initialized. */ static int init_fs_list(const char *watch_fs) { char *ptr, *saved, *tmp; if (watch_fs == NULL) { msg(LOG_ERR, "File systems to watch is empty"); return 1; } avl_init(&filesystems.index, cmp_fs); // Now parse up list and push into avl tmp = strdup(watch_fs); if (tmp == NULL) { msg(LOG_ERR, "Cannot duplicate watch_fs list"); return 1; } ptr = strtok_r(tmp, ",", &saved); while (ptr) { if (!new_filesystem(&filesystems, ptr)) { msg(LOG_ERR, "Cannot store watch_fs entry %s", ptr); free(tmp); destroy_fs_list(&filesystems); return 1; } ptr = strtok_r(NULL, ",", &saved); } free(tmp); if (filesystems.index.root == NULL) { msg(LOG_ERR, "File systems to watch is empty"); return 1; } return 0; } static void term_handler(int sig __attribute__((unused))) { stop = true; /* * Let the dispatcher finish its current nonblocking batch before * workers are woken for shutdown. Otherwise a worker can drain and * exit while a signal-interrupted handle_events() resumes and * enqueues a later record. * * Reconfiguration checks stop between reload phases. This best-effort * guard makes a deliberately sequenced SIGHUP/SIGTERM from a signal- * authorized process abandon later work, but cannot close a * permission-open race after a check. The normal shutdown path therefore * closes fanotify before it waits for helpers. */ } /* * coredump_handler - release fanotify waiters before fatal termination. * @sig: fatal signal that selected this handler. * * Hardware faults are delivered to the thread that caused them, and abort() * raises SIGABRT in the calling thread. They cannot safely be redirected to * the main thread: the interrupted thread may hold allocator or daemon locks, * and blocking synchronous SIGSEGV/SIGBUS/SIGILL/SIGFPE is undefined. * * Every thread therefore executes this same minimal handler. Closing the last * permission-group descriptor removes its marks and allows outstanding * permission events, preventing unrelated processes from remaining blocked * while the daemon terminates or writes a core. * * No allocation, logging, locking, formatted output, or shared data-structure * traversal is permitted here. After closing the group, restore the default * disposition and raise the signal in this thread so normal fatal-signal and * core-dump processing occurs. _exit() is only a fallback if that fails. * * Returns only after successfully queuing @sig for default handling. */ static void coredump_handler(int sig) { struct sigaction action = { .sa_handler = SIG_DFL, }; fanotify_close_on_fatal_signal(); sigemptyset(&action.sa_mask); if (sigaction(sig, &action, NULL) || raise(sig)) _exit(128 + sig); } static void hup_handler(int sig __attribute__((unused))) { hup = true; } /* * reload_configuration - refresh runtime configuration settings. * @void: no arguments are required. * Returns 0 when the configuration was reloaded, non-zero otherwise. */ static int reload_configuration(void) { conf_t new_config; reset_strategy_t reset_strategy; timing_collection_t timing_collection; if (load_daemon_config(&new_config)) { free_daemon_config(&new_config); msg(LOG_ERR, "Failed reloading daemon configuration"); return 1; } if (decision_config_publish(&new_config)) { free_daemon_config(&new_config); msg(LOG_ERR, "Failed publishing decision configuration"); return 1; } __atomic_store_n(&config.permissive, new_config.permissive, __ATOMIC_RELAXED); if (setpriority(PRIO_PROCESS, 0, -(int)new_config.nice_val) == -1) msg(LOG_WARNING, "Couldn't adjust priority (%s)", strerror(errno)); config.nice_val = new_config.nice_val; config.do_stat_report = new_config.do_stat_report; config.detailed_report = new_config.detailed_report; reset_strategy = __atomic_load_n(&config.reset_strategy, __ATOMIC_RELAXED); if (new_config.reset_strategy != reset_strategy) { __atomic_store_n(&config.reset_strategy, new_config.reset_strategy, __ATOMIC_RELAXED); state_report_log_reset_strategy(new_config.reset_strategy); } timing_collection = __atomic_load_n(&config.timing_collection, __ATOMIC_RELAXED); if (new_config.timing_collection != timing_collection) { __atomic_store_n(&config.timing_collection, new_config.timing_collection, __ATOMIC_RELAXED); decision_timing_apply_config(new_config.timing_collection); // Let the decision worker restore timing-owned queue metrics. if (new_config.timing_collection == TIMING_COLLECTION_OFF) nudge_queue(); } if (new_config.integrity != config.integrity) { config.integrity = new_config.integrity; msg(LOG_INFO, "fapolicyd integrity is %u", config.integrity); } if (new_config.syslog_format && (!config.syslog_format || strcmp(new_config.syslog_format, config.syslog_format) != 0)) { char *new_syslog = strdup(new_config.syslog_format); if (new_syslog) { char *old_syslog; lock_rule(); old_syslog = (char *)config.syslog_format; config.syslog_format = new_syslog; unlock_rule(); free(old_syslog); } else msg(LOG_ERR, "Failed replacing syslog_format string"); } config.rpm_sha256_only = new_config.rpm_sha256_only; if (new_config.trust && !stop) { lock_update_thread(); if (!config.trust || strcmp(new_config.trust, config.trust) != 0) { char *new_trust = strdup(new_config.trust); if (new_trust) { char *old_trust = (char *)config.trust; config.trust = new_trust; free(old_trust); } else msg(LOG_ERR, "Failed replacing trust backend list"); } unlock_update_thread(); } /* * Remaining daemon_config fields require restart-time changes: * q_size, decision_threads, subj_cache_size, and obj_cache_size are * consumed when the event queue and caches are created. uid/gid, * allow_filesystem_mark, watch_fs, and ignore_mounts are applied while * fanotify marks are installed. db_max_size fixes the LMDB map when the * database opens, and report_interval is bound to the decision worker's * timer. None of these components support resizing in-place yet, so their * configuration stays static. */ free_daemon_config(&new_config); return 0; } static void reconfigure(void) { if (stop) return; if (reload_configuration()) msg(LOG_WARNING, "Continuing with previous configuration settings"); if (stop) return; filter_destroy(); if (filter_init()) msg(LOG_ERR, "Failed initializing filter configuration"); else if (filter_load_file(NULL)) msg(LOG_ERR, "Failed reloading filter configuration"); if (stop) return; set_reload_rules(); set_reload_trust_database(); } /* * Reconfiguration reload overview * =============================== * * SIGHUP does not replace all runtime state directly in this detached * thread. Lightweight daemon configuration fields are refreshed here, then * filter, rule, and trust reloads are requested through their normal owners. * * Rule reload is transactional. The update thread opens the rule file and * builds a complete policy snapshot while the current policy remains active. * Readers hold references to the snapshot they started with, so reload can * parse slow macro/set based policies without parking the decision worker. * The new snapshot is published only after rule and syslog parsing succeed. * If any step fails, the previous policy snapshot stays active so decisions * and syslog fields do not fall back to empty, partial, or stale candidate * state. * * Trust database reload remains independent of policy reload. A failed trust * reload is reported by the database layer and does not imply that policy * rules were changed. */ /* * reconfigure_thread_main - run configuration reload outside event loop. * @arg: unused pointer. * Returns NULL. */ static void *reconfigure_thread_main(void *arg __attribute__ ((unused))) { sigset_t sigs; /* This is a worker thread. Don't handle external signals. */ sigemptyset(&sigs); sigaddset(&sigs, SIGTERM); sigaddset(&sigs, SIGHUP); sigaddset(&sigs, SIGUSR1); sigaddset(&sigs, SIGINT); sigaddset(&sigs, SIGQUIT); pthread_sigmask(SIG_SETMASK, &sigs, NULL); reconfigure(); atomic_store(&reconfig_running, false); return NULL; } /* * maybe_start_reconfigure_thread - start a reconfigure thread when requested * and prevent a second one running until first finishes. * @void: no arguments are required. * Returns nothing. */ static void maybe_start_reconfigure_thread(void) { int rc; bool expected = false; // Make sure one is not runnning since it is detached if (!atomic_compare_exchange_strong(&reconfig_running, &expected, true)) return; // OK to start up the thread pthread_t tid; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); rc = pthread_create(&tid, &attr, reconfigure_thread_main, NULL); if (rc) { msg(LOG_ERR, "Failed starting reconfigure thread (%s)", strerror(rc)); atomic_store(&reconfig_running, false); } pthread_attr_destroy(&attr); } // This is a workaround for https://bugzilla.redhat.com/show_bug.cgi?id=643031 #define UNUSED(x) (void)(x) #ifdef USE_RPM extern int rpmsqEnable (int signum, void *handler); int rpmsqEnable (int signum, void *handler) { UNUSED(signum); UNUSED(handler); return 0; } #endif /* * remove_pid_file - remove the pid file and release this daemon's lock. * * The advisory lock must remain held until shutdown has completed so a new * daemon cannot create a second pid file while this process still runs. * * Returns nothing. */ static void remove_pid_file(void) { if (pid_file_fd == -1) return; if ((pid_file_created || pid_file_written) && unlink(pidfile) && errno != ENOENT) msg(LOG_WARNING, "Unable to remove pidfile (%s)", strerror(errno)); close(pid_file_fd); pid_file_fd = -1; pid_file_created = false; pid_file_written = false; } /* * write_pid_file - write this process ID to an already locked pid file. * * Returns 0 on success or 1 after releasing the lock on write failure. */ static int write_pid_file(void) { int len; char val[16]; size_t written = 0; len = snprintf(val, sizeof(val), "%u\n", getpid()); if (len <= 0 || (size_t)len >= sizeof(val)) { msg(LOG_ERR, "Pid error (%s)", strerror(errno)); remove_pid_file(); return 1; } if (pid_file_fd == -1) { msg(LOG_ERR, "Pidfile lock was not acquired"); return 1; } if (lseek(pid_file_fd, 0, SEEK_SET) == (off_t)-1 || ftruncate(pid_file_fd, 0)) { msg(LOG_ERR, "Unable to write pidfile (%s)", strerror(errno)); goto err_out; } while (written < (size_t)len) { ssize_t rc = write(pid_file_fd, val + written, (size_t)len - written); if (rc > 0) { written += (size_t)rc; continue; } if (rc < 0 && errno == EINTR) continue; if (rc == 0) errno = EIO; msg(LOG_ERR, "Unable to write pidfile (%s)", strerror(errno)); goto err_out; } pid_file_written = true; return 0; err_out: remove_pid_file(); return 1; } static int become_daemon(void) { int fd; pid_t pid; pid = fork(); switch (pid) { case 0: // Child fd = open("/dev/null", O_RDWR); if (fd < 0) return -1; if (dup2(fd, 0) < 0) { close(fd); return -1; } if (dup2(fd, 1) < 0) { close(fd); close(0); return -1; } if (dup2(fd, 2) < 0) { close(fd); close(0); close(1); return -1; } if (fd > 2) close(fd); chdir("/"); if (setsid() < 0) return -1; break; case -1: return -1; break; default: // Parent _exit(0); break; } return 0; } // Returns 1 if we care about the entry and 0 if we do not static int check_mount_entry(const char *point, const char *type) { // Skip entries explicitly ignored by configuration if (mount_is_ignored(point)) return 0; // Some we know we don't want if (strncmp(point, "/sys/", 5) == 0) return 0; if (find_filesystem(&filesystems, type)) return 1; else return 0; } /* * parse_mount_entry - extract the mount point and filesystem type. * @buf: line from the mounts file to parse in place. * @point: where to store the mount point token. * @type: where to store the filesystem type token. * * Returns 0 on success or 1 when the line does not contain the first * 3 whitespace-delimited fields expected from /proc/mounts. */ static int parse_mount_entry(char *buf, char **point, char **type) { char *s = buf; size_t len; int field = 0; *point = NULL; *type = NULL; while (*s && field < 3) { // Skip over any leading field separators before trying // to read the next token. s += strspn(s, " \t"); if (*s == '\0' || *s == '\n') break; // Scan for anything not a space, tab, or newline len = strcspn(s, " \t\n"); if (len == 0) break; if (field == 1) *point = s; else if (field == 2) *type = s; // If we hit the end, we are done if (s[len] == '\0' || s[len] == '\n') { s[len] = '\0'; break; } // terminate the field and increment s[len] = '\0'; s += len + 1; field++; } return (*point && *type) ? 0 : 1; } /* * read_mount_list - read a complete replacement list of watched mounts. * @fd: file descriptor for the mounts file. * @next: empty list that receives matching mount paths. * * The active list is left untouched until this read and every required * allocation succeeds. This avoids applying a partial /proc/mounts snapshot. * * Returns 0 on success or -1 on a read or allocation failure. */ static int read_mount_list(int fd, mlist *next) { char buf[PATH_MAX * 2]; char *point, *type; fd_fgets_state_t *st; int rc; if (lseek(fd, 0, SEEK_SET) == (off_t)-1) { msg(LOG_ERR, "Cannot rewind mounts file (%s)", strerror(errno)); return -1; } st = fd_fgets_init(); if (!st) { msg(LOG_ERR, "Cannot allocate mounts reader (%s)", strerror(errno)); return -1; } for (;;) { rc = fd_fgets_r(st, buf, sizeof(buf), fd); // Get a line if (rc > 0) { // We only need the mount point and filesystem type. // Note, the returned pointers point into buf. if (parse_mount_entry(buf, &point, &type)) { msg(LOG_WARNING, "Skipping malformed mount (%s)", buf); continue; } if (unescape_shell(point, sizeof(buf) - (size_t)(point - buf))) { msg(LOG_WARNING, "Skipping unterminated mount point"); continue; } // Is this one that we care about? if (check_mount_entry(point, type)) { if (mlist_append(next, point)) { msg(LOG_ERR, "Cannot store mount point %s", point); goto err_out; } } } else if (rc < 0) { msg(LOG_ERR, "Cannot read mounts file (%s)", strerror(errno)); goto err_out; } else if (!fd_fgets_eof_r(st)) { msg(LOG_ERR, "Mounts file read made no progress"); goto err_out; } if (fd_fgets_eof_r(st)) break; } fd_fgets_destroy(st); return 0; err_out: fd_fgets_destroy(st); return -1; } /* * handle_mounts - read mount entries and refresh the watched mount list. * @fd: file descriptor for the mounts file. * * This function serializes access to the global mount list and the * subsequent fanotify mark update by holding mlist_lock across the * read/parse/update cycle. Callers may invoke it from the main loop or * the detached mount thread, but they must pass a descriptor that can be * rewound and read while the lock is held. * * Returns 0 on success or -1 when the snapshot cannot be applied safely. */ static int handle_mounts(int fd) { mlist next; int rc = -1; mlist_create(&next); pthread_mutex_lock(&mlist_lock); if (read_mount_list(fd, &next)) goto out; if (m == NULL) { m = malloc(sizeof(*m)); if (m == NULL) { msg(LOG_ERR, "Cannot allocate mount list (%s)", strerror(errno)); goto out; } *m = next; mlist_create(&next); } else mlist_reconcile(m, &next); // update marks fanotify_update(m); rc = 0; out: mlist_clear(&next); pthread_mutex_unlock(&mlist_lock); return rc; } /* * handle_mounts_thread_main - run mount processing outside event loop. * @arg: pointer to a heap-allocated file descriptor integer. * Returns NULL. */ static void *handle_mounts_thread_main(void *arg) { int fd; sigset_t sigs; /* This is a worker thread. Don't handle external signals. */ sigemptyset(&sigs); sigaddset(&sigs, SIGTERM); sigaddset(&sigs, SIGHUP); sigaddset(&sigs, SIGUSR1); sigaddset(&sigs, SIGINT); sigaddset(&sigs, SIGQUIT); pthread_sigmask(SIG_SETMASK, &sigs, NULL); if (arg == NULL) { atomic_store(&mounts_running, false); return NULL; } fd = *((int *)arg); free(arg); if (handle_mounts(fd)) msg(LOG_ERR, "Mount list update failed; retaining current marks"); atomic_store(&mounts_running, false); return NULL; } /* * maybe_start_mounts_thread - start a detached mounts thread when needed. * @fd: /proc/mounts file descriptor. * Returns nothing. */ static void maybe_start_mounts_thread(int fd) { int rc; bool expected = false; int *arg; // Make sure one is not runnning since it is detached if (!atomic_compare_exchange_strong(&mounts_running, &expected, true)) return; arg = malloc(sizeof(int)); if (arg == NULL) { msg(LOG_ERR, "Failed allocating mount thread arg"); atomic_store(&mounts_running, false); return; } *arg = fd; pthread_t tid; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); rc = pthread_create(&tid, &attr, handle_mounts_thread_main, arg); if (rc) { msg(LOG_ERR, "Failed starting mount thread (%s)", strerror(rc)); atomic_store(&mounts_running, false); free(arg); } pthread_attr_destroy(&attr); } /* * wait_for_mounts_thread - wait for active mount processing to complete. * Returns nothing. */ static void wait_for_mounts_thread(void) { while (atomic_load(&mounts_running)) usleep(1000); } static void wait_for_reconfigure_thread(void) { while (atomic_load(&reconfig_running)) usleep(1000); } static void usage(void) { fprintf(stderr, "Usage: fapolicyd [--debug|--debug-deny|--foreground] [--permissive] " "[--no-details] [--version]\n"); exit(1); } #ifdef HAVE_MALLINFO2 static struct mallinfo2 last_mi; /* * memory_use_report - write current glibc allocator utilization. * @f: report stream. * Returns nothing. */ static void memory_use_report(FILE *f) { struct mallinfo2 mi = mallinfo2(); fprintf(f, "glibc arena (total memory) is: %zu KiB, was: %zu KiB\n", (size_t)mi.arena/1024, (size_t)last_mi.arena/1024); fprintf(f, "glibc uordblks (in use memory) is: %zu KiB, was: %zu KiB\n", (size_t)mi.uordblks/1024,(size_t)last_mi.uordblks/1024); fprintf(f,"glibc fordblks (total free space) is: %zu KiB, was: %zu KiB\n", (size_t)mi.fordblks/1024,(size_t)last_mi.fordblks/1024); memcpy(&last_mi, &mi, sizeof(struct mallinfo2)); } static void close_memory_report(void) { struct mallinfo2 mi = mallinfo2(); msg(LOG_DEBUG, "total memory: %zu KiB, was: %zu KiB", (size_t)mi.arena/1024, (size_t)last_mi.arena/1024); msg(LOG_DEBUG, "in use memory: %zu KiB, was: %zu KiB", (size_t)mi.uordblks/1024,(size_t)last_mi.uordblks/1024); msg(LOG_DEBUG,"total free memory: %zu KiB, was: %zu KiB", (size_t)mi.fordblks/1024,(size_t)last_mi.fordblks/1024); } #endif void do_stat_report(FILE *f, int shutdown) { do_stat_report_reset(f, shutdown, 0); } /* * do_state_report - write health and configuration state. * @f: report stream. * @shutdown: non-zero when writing final shutdown report. * Returns nothing. */ void do_state_report(FILE *f, int shutdown) { const struct decision_config *decision_config; const char *ptr; reset_strategy_t strategy; failure_action_metrics_t failures; decision_metrics_t decisions; const char *reset_ptr; struct state_report_operating_mode mode; if (f == NULL) return; decision_config = decision_config_current(); ptr = lookup_integrity(decision_config_integrity(decision_config)); strategy = __atomic_load_n(&config.reset_strategy, __ATOMIC_RELAXED); reset_ptr = lookup_reset_strategy(strategy); failure_action_snapshot(&failures, 0); getDecisionMetrics(&decisions); mode.permissive = decision_config_permissive(decision_config); mode.integrity = ptr; mode.reset_strategy = reset_ptr; mode.config_generation = decision_config_generation(decision_config); mode.ruleset_generation = decisions.ruleset_generation; mode.config_effective_since = decision_config_effective_since(decision_config); mode.ruleset_effective_since = decisions.ruleset_effective_since; database_generation_snapshot(&mode.trust_db); mode.config = &config; state_report_operating_mode(f, &mode); fprintf(f, "\nHeadline activity:\n"); fprintf(f, "Allowed accesses: %lu\n", getAllowed()); fprintf(f, "Denied accesses: %lu\n", getDenied()); fprintf(f, "\nResource configuration:\n"); fprintf(f, "Daemon PID: %ld\n", (long)getpid()); fprintf(f, "CPU cores: %ld\n", sysconf(_SC_NPROCESSORS_ONLN)); fprintf(f, "decision_threads: %u\n", config.decision_threads); fprintf(f, "Per worker q_size: %u\n", config.q_size); fanotify_defer_config_report(f); do_cache_config_report(f); database_config_report(f); fprintf(f, "\nResource utilization:\n"); database_utilization_report(f, &config); do_cache_utilization_report(f); #ifdef HAVE_MALLINFO2 memory_use_report(f); #endif fprintf(f, "\nHealth indicators:\n"); fprintf(f, "Kernel queue overflow: %lu\n", failure_action_metrics_count(&failures, FAILURE_REASON_KERNEL_QUEUE_OVERFLOW)); fprintf(f, "Filesystem errors: %lu\n", failure_action_metrics_count(&failures, FAILURE_REASON_FANOTIFY_FS_ERROR)); fanotify_fs_error_report(f); fprintf(f, "Reply errors: %lu\n", failure_action_metrics_count(&failures, FAILURE_REASON_RESPONSE_WRITE_FAILURE)); fanotify_queue_health_report(f); fanotify_defer_fallback_report(f); do_cache_health_report(f); fanotify_defer_age_report(f); decision_failure_action_report(f, &failures); fprintf(f, "\nWatched mounts:\n"); // Report mounts under fanotify watch. pthread_mutex_lock(&mlist_lock); if (m) { const char *path = mlist_first(m); while (path) { fprintf(f, "watching mount: %s\n", path); path = mlist_next(m); } } pthread_mutex_unlock(&mlist_lock); // Keep repeated report output separated when generated in a loop. fputs("\n", f); } /* * do_metrics_report_reset - write resettable runtime metrics. * @f: report stream. * @reset: non-zero resets counters after snapshotting them. * Returns nothing. */ void do_metrics_report_reset(FILE *f, int reset) { if (f == NULL) return; decision_report_metrics_reset(f, reset); fanotify_metrics_report_reset(f, reset); do_cache_metrics_report_reset(f, reset); database_metrics_report_reset(f, reset); fputs("\n", f); fputs("Rule hit counts:\n", f); policy_rule_hits_report_reset(f, reset); fputs("\n", f); attr_lookup_metrics_report(f, reset); // Keep repeated report output separated when generated in a loop. fputs("\n", f); } /* * do_stat_report_reset - write state report and optionally reset metrics. * @f: output stream. * @shutdown: non-zero when writing final shutdown report. * @reset: non-zero resets interval counters after copying them. * * Reset only affects operational counters. Configuration and state identity * values such as queue size, integrity mode, database size, and ruleset * generation remain unchanged. */ void do_stat_report_reset(FILE *f, int shutdown, int reset) { do_state_report(f, shutdown); do_metrics_report_reset(f, reset); } /* * already_running - lock the pid file and reject a live daemon instance. * * The successful caller retains the advisory lock until final shutdown. That * makes the stale-pid check and later write one lifecycle operation instead of * a check-then-create race between concurrent starts. * * Returns 1 when another daemon may be running or the pid file is unsafe to * inspect, otherwise returns 0 with pid_file_fd locked for write_pid_file(). */ int already_running(void) { fd_fgets_state_t *st = NULL; char pid_buf[16], exe_buf[80], my_path[80]; int created = 0; int read_len; int pid; int result = 1; if (pid_file_fd != -1) return 1; pid_file_created = false; pid_file_written = false; for (;;) { pid_file_fd = open(pidfile, O_NOFOLLOW | O_RDWR | O_CLOEXEC); if (pid_file_fd >= 0) break; if (errno != ENOENT) { msg(LOG_ERR, "Unable to open pidfile (%s)", strerror(errno)); return 1; } /* * flock permits an exclusive lock through a read-only descriptor. * Keep the pid file private so an unprivileged process cannot hold * a stale inode locked after an unclean daemon shutdown. */ pid_file_fd = open(pidfile, O_CREAT | O_EXCL | O_NOFOLLOW | O_RDWR | O_CLOEXEC, 0600); if (pid_file_fd >= 0) { created = 1; break; } if (errno != EEXIST) { msg(LOG_ERR, "Unable to create pidfile (%s)", strerror(errno)); return 1; } } pid_file_created = created; if (flock(pid_file_fd, LOCK_EX | LOCK_NB)) { msg(LOG_ERR, "Unable to lock pidfile (%s)", strerror(errno)); goto out; } if (created) { result = 0; goto out; } if (lseek(pid_file_fd, 0, SEEK_SET) == (off_t)-1) { msg(LOG_ERR, "Unable to read pidfile (%s)", strerror(errno)); goto out; } st = fd_fgets_init(); if (st == NULL) { msg(LOG_ERR, "Unable to allocate pidfile reader (%s)", strerror(errno)); goto out; } read_len = fd_fgets_r(st, pid_buf, sizeof(pid_buf), pid_file_fd); if (read_len < 0) { msg(LOG_ERR, "fapolicyd pid file could not be read (%s)", strerror(errno)); goto out; } if (read_len == 0) { /* * A crash can leave the file empty after creation or truncation. * Holding its exclusive lock proves no live fapolicyd owns it. */ msg(LOG_WARNING, "Replacing empty stale fapolicyd pid file"); result = 0; goto out; } // Get our path if (get_program_from_pid(getpid(), sizeof(exe_buf), my_path) == NULL) goto out; // shouldn't happen, but be safe // convert pidfile to integer errno = 0; pid = strtoul(pid_buf, NULL, 10); if (errno) goto out; // shouldn't happen, but be safe // verify it really is fapolicyd if (get_program_from_pid(pid, sizeof(exe_buf), exe_buf) == NULL) result = 0; // pid doesn't exist, we're OK else if (strstr(exe_buf, "fapolicyd") == NULL) result = 0; // a different program owns the stale pid else if (strcmp(exe_buf, my_path) != 0 && pid == getpid()) result = 0; // unexpected path but this process owns the pid out: if (st) fd_fgets_destroy(st); if (result) remove_pid_file(); return result; } int main(int argc, const char *argv[]) { struct pollfd pfd[4]; struct sigaction sa; struct rlimit limit; sigset_t control_signals; nfds_t pfd_count = 3; bool ready_pending = false; int exit_status = 0; int rc; setlocale(LC_TIME, ""); for (int i=1; i < argc; i++) { if (strcmp(argv[i], "--help") == 0) usage(); else if (strcmp(argv[i], "--version") == 0) { printf("fapolicyd %s\n", VERSION); return 0; } } set_message_mode(MSG_STDERR, debug_mode); if (load_daemon_config(&config)) { free_daemon_config(&config); msg(LOG_ERR, "Exiting due to bad configuration"); return 1; } // set the debug flags for (int i=1; i < argc; i++) { if (strcmp(argv[i], "--debug") == 0) { debug_mode = 1; set_message_mode(MSG_STDERR, DBG_YES); } else if (strcmp(argv[i], "--debug-deny") == 0) { debug_mode = 2; set_message_mode(MSG_STDERR, DBG_YES); } } // process remaining flags for (int i=1; i < argc; i++) { if (strcmp(argv[i], "--permissive") == 0) { __atomic_store_n(&config.permissive, 1, __ATOMIC_RELAXED); } else if (strcmp(argv[i], "--foreground") == 0) { foreground_mode = 1; } else if (strcmp(argv[i], "--no-details") == 0) { config.detailed_report = 0; } else if (strncmp(argv[i], "--mounts", 8) == 0) { if (!debug_mode) { msg(LOG_ERR, "the mounts flag can only be" " used in debug mode"); return 1; } // require an equals and at least one char long path if (strlen(argv[i]) < 10 || argv[i][8] != '=') { msg(LOG_ERR, "the mounts flag requires a file" " path: --mounts=/tmp/mounts.txt"); return 1; } // ensure we have specified a regular file struct stat sb; const char *tmp = argv[i] + 9; if (stat(tmp, &sb) != 0) { msg(LOG_ERR, "cannot stat mounts file %s, %s", tmp, strerror(errno)); return 1; } if (!S_ISREG(sb.st_mode)) { msg(LOG_ERR, "mounts path %s is not a regular file", tmp); return 1; } msg(LOG_INFO, "Overriding mounts file: %s", tmp); mounts = tmp; } else if (strcmp(argv[i], "--debug") == 0 || strcmp(argv[i], "--debug-deny") == 0 || strcmp(argv[i], "--foreground") == 0) { // nop; debug flags already set } else { msg(LOG_ERR, "unknown command option:%s\n", argv[i]); free_daemon_config(&config); usage(); } } /* * Debug runs are manually launched and should not trust inherited * NOTIFY_SOCKET/WATCHDOG_* values. --foreground remains notify-capable * because it is the production systemd service mode. */ systemd_notify_set_enabled(!debug_mode); if (already_running()) { msg(LOG_ERR, "fapolicyd is already running"); exit(1); } /* Register first so LIFO cleanup releases the lock after later teardown. */ if (atexit(remove_pid_file)) { remove_pid_file(); msg(LOG_ERR, "Cannot set pidfile exit function"); exit(1); } if (decision_config_publish(&config)) { remove_pid_file(); free_daemon_config(&config); msg(LOG_ERR, "Exiting due to bad decision configuration"); return 1; } // Set a couple signal handlers sa.sa_flags = 0; sigemptyset(&sa.sa_mask); sa.sa_handler = hup_handler; sigaction(SIGHUP, &sa, NULL); sa.sa_handler = coredump_handler; sigaction(SIGSEGV, &sa, NULL); sigaction(SIGABRT, &sa, NULL); sigaction(SIGBUS, &sa, NULL); sigaction(SIGFPE, &sa, NULL); sigaction(SIGILL, &sa, NULL); sigaction(SIGSYS, &sa, NULL); sigaction(SIGTRAP, &sa, NULL); sigaction(SIGXCPU, &sa, NULL); sigaction(SIGXFSZ, &sa, NULL); sigaction(SIGQUIT, &sa, NULL); sa.sa_flags = SA_SIGINFO; sa.sa_sigaction = usr1_handler; sigaction(SIGUSR1, &sa, NULL); sa.sa_flags = 0; /* These need to be last since they are used later */ sa.sa_handler = term_handler; sigaction(SIGTERM, &sa, NULL); sigaction(SIGINT, &sa, NULL); // Bump up resources limit.rlim_cur = RLIM_INFINITY; limit.rlim_max = RLIM_INFINITY; setrlimit(RLIMIT_FSIZE, &limit); getrlimit(RLIMIT_NOFILE, &limit); if (limit.rlim_max >= DAEMON_CONFIG_MIN_NOFILE) limit.rlim_cur = limit.rlim_max; else limit.rlim_max = limit.rlim_cur = DAEMON_CONFIG_MIN_NOFILE; if (setrlimit(RLIMIT_NOFILE, &limit)) msg(LOG_WARNING, "Can't increase file number rlimit - %s", strerror(errno)); else msg(LOG_INFO,"Can handle %lu file descriptors", limit.rlim_cur); // get more time slices because everything is waiting on us errno = 0; nice(-config.nice_val); if (errno) msg(LOG_WARNING, "Couldn't adjust priority (%s)", strerror(errno)); // Load the rule configuration if (load_rules(&config)) { remove_pid_file(); exit(1); } if (!debug_mode) { if (!foreground_mode) { if (become_daemon() < 0) { remove_pid_file(); msg(LOG_ERR, "Exiting due to failure daemonizing"); exit(1); } } set_message_mode(MSG_SYSLOG, DBG_NO); openlog("fapolicyd", LOG_PID, LOG_DAEMON); } /* Keep this before unlink_fifo so shutdown retains its existing order. * message_async_stop() is a safe no-op until the logger starts below. */ if (atexit(message_async_stop)) { remove_pid_file(); msg(LOG_ERR, "cannot register async log exit handler"); exit(1); } state_report_log_reset_strategy(config.reset_strategy); decision_timing_apply_config(config.timing_collection); // Set the exit function so there is always a fifo cleanup if (atexit(unlink_fifo)) { remove_pid_file(); msg(LOG_ERR, "Cannot set exit function"); exit(1); } // Setup filesystem to watch list init_ignore_mounts(config.ignore_mounts); if (init_fs_list(config.watch_fs)) { remove_pid_file(); destroy_rules(); destroy_fs_list(&filesystems); destroy_fs_list(&ignored_mounts); decision_config_destroy(); free_daemon_config(&config); exit(1); } /* The lock acquired above remains held across daemonization. */ if (write_pid_file()) { destroy_rules(); destroy_fs_list(&filesystems); destroy_fs_list(&ignored_mounts); decision_config_destroy(); free_daemon_config(&config); exit(1); } // Set strict umask (void) umask( 0117 ); if (preconstruct_fifo(&config)) { remove_pid_file(); msg(LOG_ERR, "Cannot construct a pipe"); exit(1); } // If we are not going to be root, then setup necessary capabilities if (config.uid != 0) { capng_clear(CAPNG_SELECT_BOTH); capng_updatev(CAPNG_ADD, CAPNG_EFFECTIVE|CAPNG_PERMITTED, CAP_DAC_OVERRIDE, CAP_SYS_ADMIN, CAP_SYS_PTRACE, CAP_SYS_NICE, CAP_SYS_RESOURCE, CAP_AUDIT_WRITE, -1); if (capng_change_id(config.uid, config.gid, CAPNG_DROP_SUPP_GRP)) { msg(LOG_ERR, "Cannot change to uid %d", config.uid); exit(1); } else msg(LOG_DEBUG, "Changed to uid %d", config.uid); } // Install seccomp filter to prevent escalation install_syscall_filter(); // Start after daemonizing and installing seccomp. This create a new // thread. When seccomp is involved, it must be started after the // filter is installed so that it inherits the filter. rc = message_async_start(); if (rc) { msg(LOG_ERR, "failed starting async logging thread (%s)", strerror(rc)); exit(1); } // Setup lru caches if (init_event_system(&config)) { msg(LOG_ERR, "Cannot initialize event caches"); destroy_rules(); destroy_fs_list(&filesystems); destroy_fs_list(&ignored_mounts); free_daemon_config(&config); remove_pid_file(); exit(1); } // Init the database if (init_database(&config)) { destroy_event_system(); destroy_rules(); destroy_fs_list(&filesystems); destroy_fs_list(&ignored_mounts); free_daemon_config(&config); remove_pid_file(); exit(1); } // Init the file test libraries if (file_init()) { // file_init cleans up on failure - skip destroy_event_system(); destroy_rules(); destroy_fs_list(&filesystems); destroy_fs_list(&ignored_mounts); free_daemon_config(&config); remove_pid_file(); exit(1); } // Initialize the file watch system pfd[0].fd = open(mounts, O_RDONLY); if (pfd[0].fd < 0) { msg(LOG_ERR, "Cannot open mounts file %s (%s)", mounts, strerror(errno)); file_close(); close_database(); destroy_event_system(); destroy_rules(); destroy_fs_list(&filesystems); destroy_fs_list(&ignored_mounts); free_daemon_config(&config); remove_pid_file(); exit(1); } pfd[0].events = POLLPRI; if (handle_mounts(pfd[0].fd)) { msg(LOG_ERR, "Cannot initialize watched mount list"); close(pfd[0].fd); file_close(); close_database(); destroy_event_system(); destroy_rules(); destroy_fs_list(&filesystems); destroy_fs_list(&ignored_mounts); free_daemon_config(&config); remove_pid_file(); exit(1); } pfd[1].fd = init_fanotify(&config, m); pfd[1].events = POLLIN; /* * Without a signal fd, SIGTERM can run its handler after main tests * stop but before the untimed poll begins. No polled descriptor then * becomes ready, so shutdown deadlocks until unrelated filesystem * activity occurs. SIGHUP has the same race and can leave reload * pending. A signalfd makes either request visible to poll regardless * of whether it arrives before or during the system call. */ sigemptyset(&control_signals); sigaddset(&control_signals, SIGTERM); sigaddset(&control_signals, SIGINT); sigaddset(&control_signals, SIGHUP); pfd[2].fd = signalfd(-1, &control_signals, SFD_CLOEXEC | SFD_NONBLOCK); if (pfd[2].fd >= 0) { rc = pthread_sigmask(SIG_BLOCK, &control_signals, NULL); if (rc) { close(pfd[2].fd); pfd[2].fd = -1; errno = rc; } } if (pfd[2].fd < 0) { msg(LOG_ERR, "Cannot create control signal fd (%s)", strerror(errno)); exit_status = 1; goto shutdown; } pfd[2].events = POLLIN; #ifdef FAPOLICYD_ENABLE_FANOTIFY_FS_ERROR pfd[3].fd = fanotify_fs_error_fd(); if (pfd[3].fd >= 0) { pfd[3].events = POLLIN; pfd_count = 4; } #endif msg(LOG_INFO, "Starting to listen for events"); if (systemd_notify_ready()) { if (errno == EAGAIN || errno == EWOULDBLOCK) ready_pending = true; else msg(LOG_WARNING, "Cannot notify systemd that startup is complete"); } while (!stop) { if (hup) { hup = false; msg(LOG_DEBUG, "Got SIGHUP"); maybe_start_reconfigure_thread(); } rc = poll(pfd, pfd_count, ready_pending ? 250 : -1); #ifdef DEBUG msg(LOG_DEBUG, "Main poll interrupted"); #endif if (rc < 0) { if (errno == EINTR) continue; else { int poll_errno = errno; stop = true; /* * The main loop is the only reader of the fanotify * permission group. The update thread can open watched * files during a trust reload; those same-process opens are * allowed only after handle_events() reads their permission * events. Once poll fails, joining that thread while the * group is still live can therefore wait forever. * * Close the group before any shutdown work that may wait. * Closing it removes the marks and allows all outstanding * permission events. Unmarking alone is insufficient because * an event already queued still needs a reply or a group * close. nudge_queue() only wakes decision workers, while * close_database() joins the update thread, so neither call * may precede this early close. */ fanotify_close_for_shutdown(); nudge_queue(); close_database(); msg(LOG_ERR, "Poll error (%s)\n", strerror(poll_errno)); exit(1); } } else if (rc > 0) { int signal_fd_error = 0; if (pfd[2].revents & (POLLERR | POLLHUP | POLLNVAL)) { signal_fd_error = EIO; } else if (pfd[2].revents & POLLIN) { struct signalfd_siginfo info; ssize_t len; do { len = read(pfd[2].fd, &info, sizeof(info)); } while (len < 0 && errno == EINTR); if (len != (ssize_t)sizeof(info)) signal_fd_error = len < 0 ? errno : EIO; else if (info.ssi_signo == SIGHUP) hup_handler(info.ssi_signo); else term_handler(info.ssi_signo); } if (signal_fd_error) { msg(LOG_ERR, "Control signal fd failed (%s)", strerror(signal_fd_error)); exit_status = 1; stop = true; } if (stop) continue; if (pfd[1].revents & POLLIN) { handle_events(); } if (pfd_count > 3 && pfd[3].revents & POLLIN) { fanotify_fs_error_handle_events(); } if (pfd[0].revents & POLLPRI) { msg(LOG_DEBUG, "Mount change detected"); maybe_start_mounts_thread(pfd[0].fd); } // This will always need to be here as long as we // link against librpm. Turns out that librpm masks // signals to prevent corrupted databases during an // update. Since we only do read access, we can turn // them back on. #ifdef USE_RPM sigaction(SIGTERM, &sa, NULL); sigaction(SIGINT, &sa, NULL); #endif } /* Retry only after pending permission events have been answered. */ if (ready_pending) { rc = systemd_notify_ready(); if (rc == 0) ready_pending = false; else if (errno != EAGAIN && errno != EWOULDBLOCK) { ready_pending = false; msg(LOG_WARNING, "Cannot notify systemd that startup is complete"); } } } shutdown: msg(LOG_INFO, "shutting down..."); /* * The main loop was the only reader that allowed same-process fanotify * events. Close the group before waiting for any worker or helper that * may already be blocked in a watched open. Moving this close after a * wait can make shutdown deadlock with the close needed to release it. */ fanotify_close_for_shutdown(); if (systemd_notify_stopping()) msg(LOG_WARNING, "Cannot notify systemd that shutdown started"); wait_for_reconfigure_thread(); wait_for_mounts_thread(); shutdown_fanotify(); close(pfd[0].fd); file_close(); close_database(); #ifdef HAVE_MALLINFO2 close_memory_report(); #endif // Reinstate the strict umask in case rpm messed with it (void) umask( 0237 ); if (config.do_stat_report) { FILE *f = fopen(REPORT, "w"); if (f == NULL) msg(LOG_WARNING, "Cannot create usage report"); else { do_stat_report(f, 1); run_usage_report(&config, f); fclose(f); } } mlist_clear(m); // removes mounts free(m); destroy_event_system(); // clears lru caches destroy_rules(); destroy_fs_list(&filesystems); destroy_fs_list(&ignored_mounts); decision_config_destroy(); free_daemon_config(&config); remove_pid_file(); if (pfd[2].fd >= 0) close(pfd[2].fd); return exit_status; } fapolicyd-2.0/src/daemon/mounts.c000066400000000000000000000073041524071644000170570ustar00rootroot00000000000000/* * mounts.c - Minimal linked list set of mount points * Copyright (c) 2019 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #include "config.h" #include #include #include #include #include #include #include #include "mounts.h" void mlist_create(mlist *m) { m->head = NULL; m->cur = NULL; } static void mlist_last(mlist *m) { register mnode* window; if (m->head == NULL) return; window = m->head; while (window->next) window = window->next; m->cur = window; } // Returns 0 on success and 1 on error int mlist_append(mlist *m, const char *p) { mnode* newnode; if (m && p) { newnode = malloc(sizeof(mnode)); if (newnode == NULL) return 1; newnode->path = strdup(p); if (newnode->path == NULL) { free(newnode); return 1; } newnode->status = MNT_ADD; } else return 1; newnode->next = NULL; mlist_last(m); // if we are at top, fix this up if (m->head == NULL) m->head = newnode; else // Otherwise add pointer to newnode m->cur->next = newnode; // make newnode current m->cur = newnode; return 0; } /* * mlist_reconcile - merge a complete replacement list into the active list. * @m: active mount list whose status fields drive fanotify updates. * @next: fully parsed mount list that transfers new node ownership to @m. * * Parsing into @next first keeps @m unchanged if reading /proc/mounts or an * allocation fails. Matching paths retain their marks, missing paths become * deletions, and new nodes are moved without another allocation. * * Returns nothing. */ void mlist_reconcile(mlist *m, mlist *next) { mnode *node; if (m == NULL || next == NULL) return; mlist_mark_all_deleted(m); node = next->head; while (node) { mnode *following = node->next; node->next = NULL; if (mlist_find(m, node->path)) { m->cur->status = MNT_NO_CHANGE; free((void *)node->path); free(node); } else { mlist_last(m); if (m->head == NULL) m->head = node; else m->cur->next = node; m->cur = node; } node = following; } next->head = NULL; next->cur = NULL; m->cur = m->head; } const char *mlist_first(mlist *m) { m->cur = m->head; if (m->cur == NULL) return NULL; return m->cur->path; } const char *mlist_next(mlist *m) { if (m->cur == NULL) return NULL; m->cur = m->cur->next; if (m->cur == NULL) return NULL; return m->cur->path; } void mlist_mark_all_deleted(mlist *m) { register mnode *n = m->head; while (n) { n->status = MNT_DELETE; n = n->next; } } int mlist_find(mlist *m, const char *p) { register mnode *n = m->head; while (n) { if (strcmp(p, n->path) == 0) { m->cur = n; return 1; } n = n->next; } return 0; } void mlist_clear(mlist *m) { mnode* nextnode; register mnode* current; current = m->head; while (current) { nextnode=current->next; free((void *)current->path); free((void *)current); current=nextnode; } m->head = NULL; m->cur = NULL; } fapolicyd-2.0/src/daemon/mounts.h000066400000000000000000000027471524071644000170720ustar00rootroot00000000000000/* * mounts.h - Header file for mounts.c * Copyright (c) 2019 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef MOUNTS_HEADER #define MOUNTS_HEADER typedef enum { MNT_NO_CHANGE, MNT_ADD, MNT_DELETE } change_t; typedef struct _mnode{ const char *path; change_t status; struct _mnode *next; // Next node pointer } mnode; typedef struct { mnode *head; // List head mnode *cur; // Pointer to current node } mlist; void mlist_create(mlist *m); const char *mlist_first(mlist *m); const char *mlist_next(mlist *m); void mlist_mark_all_deleted(mlist *l); int mlist_find(mlist *m, const char *p); int mlist_append(mlist *m, const char *p); void mlist_reconcile(mlist *m, mlist *next); void mlist_clear(mlist *m); #endif fapolicyd-2.0/src/daemon/notify.c000066400000000000000000000410401524071644000170350ustar00rootroot00000000000000/* * notify.c - daemon fanotify permission group boundary * Copyright (c) 2016-18,2022-24 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * Radovan Sroka */ /* * Overview * -------- * * notify.c owns the daemon's fanotify permission group and the top-level * exported API used by fapolicyd.c: init_fanotify(), handle_events(), * shutdown_fanotify(), mark updates, public report entry points, and kernel * fanotify event reading. It is the boundary between kernel records and the * decision worker subsystem. * * The state owned here is limited to the permission fanotify fd, current mark * mask/mark mode, the daemon pid used for self-event bypass, and the kernel * queue-overflow rate limiter. This file must not own decision worker queues, * worker health timestamps, worker thread lifecycle, defer arrays, or watchdog * decisions. Those belong to decision-worker.c and worker-health.c. * * The ownership handoff remains explicit: * * kernel fanotify fd * -> handle_events() fanotify dispatcher * -> decision_worker_pool_enqueue() * -> decision-worker.c queue/defer/worker processing * -> make_policy_decision() * -> reply_event() * * Before a successful enqueue, notify.c still owns the permission fd and must * answer it on local errors such as invalid masks or queue-full fallback. Once * decision_worker_pool_enqueue() succeeds, the worker pool owns the copied * metadata fd and is the only path that may answer or close it, including * deferred-event and shutdown cleanup. */ #include "config.h" /* Needed to get O_LARGEFILE definition */ #include #include #include #include #include #include #include #include #include #include #include #include "conf.h" #include "decision-config.h" #include "decision-worker.h" #include "escape.h" #include "failure-action.h" #include "fanotify-fs-error.h" #include "message.h" #include "mounts.h" #include "notify.h" #include "policy.h" #define FANOTIFY_BUFFER_SIZE 8192 #define KERNEL_OVERFLOW_LOG_INTERVAL 60 // External variables extern atomic_bool stop, run_stats; extern conf_t config; // Local variables static pid_t our_pid; _Static_assert(ATOMIC_INT_LOCK_FREE == 2, "fatal signal cleanup requires lock-free atomic int"); static atomic_int fd = ATOMIC_VAR_INIT(-1); static uint64_t mask; static unsigned int mark_flag; static struct message_rate_limit kernel_queue_overflow_log = MESSAGE_RATE_LIMIT_INIT(KERNEL_OVERFLOW_LOG_INTERVAL); // Local functions static void fanotify_failure_action(failure_reason_t reason); static const char *escape_path_for_log(const char *path, char **escaped); static int ignore_mounts_configured(const char *list); /* * getKernelQueueOverflow - return kernel fanotify overflow count. * Returns the number of FAN_Q_OVERFLOW events reported by the kernel. */ unsigned long getKernelQueueOverflow(void) { return failure_action_count(FAILURE_REASON_KERNEL_QUEUE_OVERFLOW); } /* * fanotify_failure_action - run the daemon-local failure response. * @reason: failure condition that was already recorded. * Returns nothing. */ static void fanotify_failure_action(failure_reason_t reason) { (void)reason; /* * The current action is observe-only. Wake the report path so serious * reliability failures are visible before the next interval report. */ run_stats = true; nudge_queue(); } /* * handle_kernel_event - process fanotify metadata without a file descriptor. * @metadata: fanotify event metadata from the kernel. * Returns 1 when the event was consumed, 0 when normal event handling should * continue. */ int handle_kernel_event(const struct fanotify_event_metadata *metadata) { unsigned long total; time_t now; if (metadata->mask & FAN_Q_OVERFLOW) { total = failure_action_record( FAILURE_REASON_KERNEL_QUEUE_OVERFLOW); now = time(NULL); if (message_rate_limit_allow(&kernel_queue_overflow_log, now)) msg(LOG_CRIT, "Kernel fanotify queue overflow; events were lost " "(kernel_queue_overflow=%lu)", total); fanotify_failure_action(FAILURE_REASON_KERNEL_QUEUE_OVERFLOW); return 1; } return 0; } /* * fanotify_active_worker_count - return workers currently receiving events. * Returns zero before fanotify initialization, otherwise the active count. */ unsigned int fanotify_active_worker_count(void) { return decision_worker_pool_active_count(); } /* * escape_path_for_log - return a shell-escaped path for logging. * @path: path that may include control characters. * @escaped: optional output pointer to an allocated escaped buffer. * Returns escaped @path when needed, original @path when not needed, * or "" if escaping is needed but allocation fails. */ static const char *escape_path_for_log(const char *path, char **escaped) { size_t escaped_size; if (escaped) *escaped = NULL; escaped_size = check_escape_shell(path); if (escaped_size == 0) return path; if (escaped) *escaped = escape_shell(path, escaped_size); if (escaped && *escaped) return *escaped; return ""; } /* * ignore_mounts_configured - determine whether ignore_mounts has entries. * @list: configuration string describing ignored mount points. * Returns 1 when at least one entry is configured and 0 otherwise. */ static int ignore_mounts_configured(const char *list) { if (list == NULL) return 0; while (*list) { if (!isspace((unsigned char)*list) && *list != ',') return 1; list++; } return 0; } int init_fanotify(const conf_t *conf, mlist *m) { const char *path; struct decision_worker_runtime runtime; int ignore_mounts_enabled; int rc; if (decision_worker_pool_open(conf)) exit(1); our_pid = getpid(); fd = fanotify_init(FAN_CLOEXEC | FAN_CLASS_CONTENT | #ifdef USE_AUDIT FAN_ENABLE_AUDIT | #endif FAN_NONBLOCK, O_RDONLY | O_LARGEFILE | O_CLOEXEC | O_NOATIME); #ifdef USE_AUDIT // We will retry without the ENABLE_AUDIT to see if THAT is supported if (fd < 0 && errno == EINVAL) { fd = fanotify_init(FAN_CLOEXEC | FAN_CLASS_CONTENT | FAN_NONBLOCK, O_RDONLY | O_LARGEFILE | O_CLOEXEC | O_NOATIME); if (fd >= 0) policy_no_audit(); } #endif if (fd < 0) { msg(LOG_ERR, "Failed opening fanotify fd (%s)", strerror(errno)); decision_worker_pool_discard(); exit(1); } if (reply_event_init(fd)) { fanotify_close_on_fatal_signal(); decision_worker_pool_discard(); exit(1); } runtime.fanotify_fd = fd; runtime.fanotify_mask = &mask; runtime.report_interval = conf->report_interval; rc = decision_worker_pool_start(&runtime); if (rc) { fanotify_close_on_fatal_signal(); decision_worker_pool_discard(); exit(1); } mask = FAN_OPEN_PERM | FAN_OPEN_EXEC_PERM; ignore_mounts_enabled = ignore_mounts_configured(conf->ignore_mounts); if (ignore_mounts_enabled && conf->allow_filesystem_mark) { msg(LOG_ERR, "ignore_mounts conflicts with allow_filesystem_mark - disable filesystem marks"); exit(1); } #if defined HAVE_DECL_FAN_MARK_FILESYSTEM && HAVE_DECL_FAN_MARK_FILESYSTEM != 0 if (conf->allow_filesystem_mark) mark_flag = FAN_MARK_FILESYSTEM; else mark_flag = FAN_MARK_MOUNT; #else if (conf->allow_filesystem_mark) msg(LOG_ERR, "allow_filesystem_mark is unsupported for this kernel - ignoring"); mark_flag = FAN_MARK_MOUNT; #endif // Iterate through the mount points and add a mark path = mlist_first(m); while (path) { char *escaped_path = NULL; const char *safe_path; safe_path = escape_path_for_log(path, &escaped_path); retry_mark: if (fanotify_mark(fd, FAN_MARK_ADD | mark_flag, mask, -1, path) == -1) { /* * The FAN_OPEN_EXEC_PERM mask is not supported by * all kernel releases prior to 5.0. Retry setting * up the mark using only the legacy FAN_OPEN_PERM * mask. */ if (errno == EINVAL && mask & FAN_OPEN_EXEC_PERM) { msg(LOG_INFO, "Kernel doesn't support OPEN_EXEC_PERM"); mask = FAN_OPEN_PERM; goto retry_mark; } msg(LOG_ERR, "Error (%s) adding fanotify mark for %s", strerror(errno), safe_path); free(escaped_path); exit(1); } msg(LOG_DEBUG, "added %s mount point", safe_path); free(escaped_path); path = mlist_next(m); } fanotify_fs_error_init(m); return fd; } /* * fanotify_close_on_fatal_signal - release the permission fanotify group. * * The lock-free exchange gives ownership of the descriptor to at most one * concurrent fatal signal handler. close() is async-signal-safe, releases the * group's marks, and allows its outstanding permission events. This function * must not grow logging, allocation, locking, or other cleanup dependencies. * * Returns nothing. */ void fanotify_close_on_fatal_signal(void) { int group_fd = atomic_exchange_explicit(&fd, -1, memory_order_relaxed); if (group_fd >= 0) (void)close(group_fd); } /* * fanotify_close_for_shutdown - release permission waiters before thread waits. * * FAN_MARK_FLUSH prevents future events but does not answer permission events * already queued. Once the main reader exits, a daemon worker or helper can be * blocked in its own watched open, so no join or helper wait may precede this * synchronized group close. reply_event_close_group() also prevents workers * from writing through the retired descriptor after the kernel reuses it. * * Returns nothing. */ void fanotify_close_for_shutdown(void) { reply_event_close_group(&fd); } void fanotify_update(mlist *m) { // Make sure fanotify_init has run if (fd < 0) return; if (m->head == NULL) return; mnode *cur = m->head, *prev = NULL, *temp; while (cur) { char *escaped_path = NULL; const char *safe_path = escape_path_for_log(cur->path, &escaped_path); if (cur->status == MNT_ADD) { // We will trust that the mask was set correctly if (fanotify_mark(fd, FAN_MARK_ADD | mark_flag, mask, -1, cur->path) == -1) { msg(LOG_ERR, "Error (%s) adding fanotify mark for %s", strerror(errno), safe_path); } else { msg(LOG_DEBUG, "Added %s mount point", safe_path); } fanotify_fs_error_mark(cur->path); } // Now remove the deleted mount point - NOTE: the kernel // cleans up the mark itself when umount ran. All we do // here is update the bookkeeping. if (cur->status == MNT_DELETE) { msg(LOG_DEBUG, "Deleted %s mount point", safe_path); temp = cur->next; if (cur == m->head) m->head = temp; else prev->next = temp; free((void *)cur->path); free((void *)cur); cur = temp; } else { prev = cur; cur = cur->next; } free(escaped_path); } m->cur = m->head; // Leave cur pointing to something valid } /* * shutdown_fanotify - join and release workers after permission group close. * Returns nothing. */ void shutdown_fanotify(void) { decision_worker_pool_shutdown(); decision_worker_pool_close(); fanotify_fs_error_close(); // Report results msg(LOG_DEBUG, "Allowed accesses: %lu", getAllowed()); msg(LOG_DEBUG, "Denied accesses: %lu", getDenied()); } void nudge_queue(void) { decision_worker_pool_nudge(); } /* * fanotify_queue_report - write fanotify queue metrics. * @f: output stream. * Returns nothing. */ void fanotify_queue_report(FILE *f) { decision_worker_pool_queue_report(f); } /* * fanotify_queue_report_reset - write fanotify queue metrics. * @f: output stream. * @reset: non-zero resets interval counters after copying them. * Returns nothing. */ void fanotify_queue_report_reset(FILE *f, int reset) { decision_worker_pool_queue_report_reset(f, reset); } /* * fanotify_queue_health_report - write per-worker queue health indicators. * @f: report stream. * Returns nothing. */ void fanotify_queue_health_report(FILE *f) { decision_worker_pool_queue_health_report(f); } /* * fanotify_defer_config_report - write defer capacity sized at startup. * @f: report stream. * Returns nothing. */ void fanotify_defer_config_report(FILE *f) { decision_worker_pool_defer_config_report(f); } /* * fanotify_defer_fallback_report - write defer fallback health indicator. * @f: report stream. * Returns nothing. */ void fanotify_defer_fallback_report(FILE *f) { decision_worker_pool_defer_fallback_report(f); } /* * fanotify_defer_age_report - write oldest deferred event age. * @f: report stream. * Returns nothing. */ void fanotify_defer_age_report(FILE *f) { decision_worker_pool_defer_age_report(f); } /* * fanotify_defer_health_report - write defer health indicators. * @f: report stream. * Returns nothing. */ void fanotify_defer_health_report(FILE *f) { decision_worker_pool_defer_health_report(f); } /* * fanotify_metrics_report_reset - write queue and defer activity metrics. * @f: report stream. * @reset: non-zero resets counters after snapshotting them. * Returns nothing. */ void fanotify_metrics_report_reset(FILE *f, int reset) { if (f == NULL) return; fprintf(f, "\nInter-thread queue & defer activity:\n"); fanotify_queue_report_reset(f, reset); } /* * handle_events - read policy permission fanotify events. * Returns nothing. */ void handle_events(void) { const struct fanotify_event_metadata *metadata; struct fanotify_event_metadata buf[FANOTIFY_BUFFER_SIZE]; ssize_t len = -2; if (fd < 0) return; while (len < 0) { do { len = read(fd, (void *) buf, sizeof(buf)); } while (len == -1 && errno == EINTR && stop == false); if (len == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) return; if (len == -1) { // If we get this, we have no access to the file. We // cannot formulate a reply either to deny it because // we have nothing to work with. msg(LOG_ERR, "Error receiving fanotify_event (%s)", strerror(errno)); return; } if (stop) return; } metadata = (const struct fanotify_event_metadata *)buf; while (FAN_EVENT_OK(metadata, len)) { if (metadata->vers != FANOTIFY_METADATA_VERSION) { msg(LOG_ERR, "Mismatch of fanotify metadata version"); exit(1); } if (handle_kernel_event(metadata)) { metadata = FAN_EVENT_NEXT(metadata, len); continue; } if (metadata->fd >= 0) { if (metadata->mask & mask) { if (metadata->pid == our_pid) reply_event(fd, metadata, FAN_ALLOW, NULL); else { unsigned int worker_index = DECISION_EVENT_NO_WORKER; if (decision_worker_pool_enqueue( metadata, &worker_index)) { int enqueue_errno = errno; int decision = FAN_DENY; if (enqueue_errno != ESHUTDOWN) failure_action_record( FAILURE_REASON_QUEUE_FULL); if (decision_config_permissive(NULL)) decision = FAN_ALLOW; /* * Answer before syslog: * journald may be the blocked * opener, and logging first * can create a circular wait * during journal rotation. */ reply_event(fd, metadata, decision, NULL); if (enqueue_errno == ESHUTDOWN) msg(LOG_DEBUG, "Worker pool stopped before " "event for PID %d could be " "queued; answered directly", metadata->pid); else msg(LOG_ERR, "Failed to enqueue event " "for PID %d to worker %u: " "queue is full, please " "consider tuning q_size if " "issue happens often", metadata->pid, worker_index); } } } else { // This should never happen. Reply with deny // which releases the descriptor and kernel // memory. Continue processing what was read. reply_event(fd, metadata, FAN_DENY, NULL); } } metadata = FAN_EVENT_NEXT(metadata, len); } } #ifdef TEST_SUBJECT_DEFER /* * test_notify_set_fanotify_fd - install a synthetic fd for read-loop tests. * @test_fd: fd borrowed from the unit test, or -1 to disable reads. * * The test owns @test_fd and must close it after clearing this reference. * Returns nothing. */ void test_notify_set_fanotify_fd(int test_fd) { atomic_store_explicit(&fd, test_fd, memory_order_relaxed); } #endif fapolicyd-2.0/src/daemon/notify.h000066400000000000000000000035231524071644000170460ustar00rootroot00000000000000/* * notify.h - Header file for notify.c * Copyright (c) 2016,2018 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef NOTIFY_HEADER #define NOTIFY_HEADER #include "conf.h" #include #include #include "mounts.h" int init_fanotify(const conf_t *config, mlist *m); void fanotify_close_for_shutdown(void); void fanotify_close_on_fatal_signal(void); void fanotify_update(mlist *m); void shutdown_fanotify(void); void fanotify_queue_report(FILE *f); void fanotify_queue_report_reset(FILE *f, int reset); void fanotify_queue_health_report(FILE *f); void fanotify_defer_config_report(FILE *f); void fanotify_defer_fallback_report(FILE *f); void fanotify_defer_age_report(FILE *f); void fanotify_defer_health_report(FILE *f); void fanotify_metrics_report_reset(FILE *f, int reset); void handle_events(void); int handle_kernel_event(const struct fanotify_event_metadata *metadata); unsigned long getKernelQueueOverflow(void); unsigned int fanotify_active_worker_count(void); void nudge_queue(void); #ifdef TEST_SUBJECT_DEFER void test_notify_set_fanotify_fd(int test_fd); #endif #endif fapolicyd-2.0/src/daemon/state-report.c000066400000000000000000000445251524071644000201710ustar00rootroot00000000000000/* * state-report.c - daemon state report coordination * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #include "config.h" #include #include #include #include #include #include #include #include #include "attr-lookup-metrics.h" #include "daemon-config.h" #include "decision-config.h" #include "decision-timing.h" #include "failure-action.h" #include "fanotify-fs-error.h" #include "gcc-attributes.h" #include "message.h" #include "notify.h" #include "paths.h" #include "policy.h" #include "state-report.h" extern atomic_bool run_stats; extern atomic_uint signal_report_requests; extern atomic_uint signal_report_intent; extern atomic_uint signal_report_reset_requests; extern atomic_int signal_report_reset_request_pid; extern atomic_int signal_report_reset_request_uid; extern conf_t config; static time_t last_metrics_reset; static const char *format_report_time(time_t when, char *buf, size_t buf_size, const char *zero_text) __attr_access ((__write_only__, 2, 3)); static const char *format_metrics_reset_time(char *buf, size_t buf_size) __attr_access ((__write_only__, 1, 2)); static void write_generation_line(FILE *f, const char *label, unsigned long generation, time_t effective_since); static void write_trust_database_generation(FILE *f, const database_generation_report_t *report); /* * state_report_operating_mode - write health and control state. * @f: report stream. * @mode: operating mode snapshot and active timing configuration. * Returns nothing. */ void state_report_operating_mode(FILE *f, const struct state_report_operating_mode *mode) { if (f == NULL || mode == NULL) return; fprintf(f, "Operating mode:\n"); fprintf(f, "Permissive: %s\n", mode->permissive ? "true" : "false"); fprintf(f, "Integrity: %s\n", mode->integrity ? mode->integrity : "unknown"); fprintf(f, "reset_strategy: %s\n", mode->reset_strategy ? mode->reset_strategy : "unknown"); write_generation_line(f, "Config generation", mode->config_generation, mode->config_effective_since); write_generation_line(f, "Ruleset generation", mode->ruleset_generation, mode->ruleset_effective_since); write_trust_database_generation(f, &mode->trust_db); decision_timing_control_report(f, mode->config); decision_timing_history_report(f); } /* * usr1_handler - request work from SIGUSR1. * @sig: signal number. * @info: sender identity supplied by sigaction. * @context: unused signal context. * Returns nothing. */ void usr1_handler(int sig __attribute__((unused)), siginfo_t *info, void *context __attribute__((unused))) { if (info && info->si_code == SI_QUEUE) { report_intent_t intent = info->si_value.sival_int; if (intent == REPORT_INTENT_TIMING_ARM || intent == REPORT_INTENT_TIMING_STOP) { decision_timing_signal_request(intent, info->si_pid, info->si_uid); nudge_queue(); return; } if (intent == REPORT_INTENT_RESET_METRICS) { atomic_store_explicit(&signal_report_reset_request_pid, info->si_pid, memory_order_relaxed); atomic_store_explicit(&signal_report_reset_request_uid, info->si_uid, memory_order_relaxed); atomic_fetch_add_explicit( &signal_report_reset_requests, 1, memory_order_relaxed); } if (intent == REPORT_INTENT_STATUS || intent == REPORT_INTENT_METRICS || intent == REPORT_INTENT_RESET_METRICS) atomic_store_explicit(&signal_report_intent, intent, memory_order_relaxed); } atomic_fetch_add_explicit(&signal_report_requests, 1, memory_order_relaxed); run_stats = true; nudge_queue(); } /* * format_report_time - format a report timestamp. * @when: timestamp to format. * @buf: destination buffer. * @buf_size: destination size. * @zero_text: text to use when @when is not available. * Returns @buf on success, or NULL when @buf cannot be initialized. */ static const char *format_report_time(time_t when, char *buf, size_t buf_size, const char *zero_text) { struct tm tm; if (buf == NULL || buf_size == 0) return NULL; if (when <= 0) { strncpy(buf, zero_text ? zero_text : "unknown", buf_size - 1); buf[buf_size - 1] = 0; return buf; } if (localtime_r(&when, &tm) == NULL || strftime(buf, buf_size, "%Y-%m-%d %H:%M:%S %z", &tm) == 0) { strncpy(buf, "unavailable", buf_size - 1); buf[buf_size - 1] = 0; } return buf; } /* * format_metrics_reset_time - format the last metric reset timestamp. * @buf: destination buffer. * @buf_size: destination size. * Returns @buf on success, or NULL when @buf cannot be initialized. */ static const char *format_metrics_reset_time(char *buf, size_t buf_size) { return format_report_time(last_metrics_reset, buf, buf_size, "never"); } /* * write_generation_line - write a generation with its active timestamp. * @f: report stream. * @label: generation label without trailing colon. * @generation: generation number to report. * @effective_since: time the generation became active. * Returns nothing. */ static void write_generation_line(FILE *f, const char *label, unsigned long generation, time_t effective_since) { char since[64]; const char *since_text; if (f == NULL || label == NULL) return; since_text = format_report_time(effective_since, since, sizeof(since), "unknown"); if (since_text == NULL) since_text = "unknown"; fprintf(f, "%s: %lu (effective since %s)\n", label, generation, since_text); } /* * write_trust_database_generation - write trust DB generation identity. * @f: report stream. * @report: trust DB generation snapshot. * * Returns nothing. */ static void write_trust_database_generation(FILE *f, const database_generation_report_t *report) { if (f == NULL || report == NULL) return; write_generation_line(f, "Trust database generation", report->generation, report->publish_time); write_generation_line(f, "LMDB environment generation", report->lmdb_generation, report->lmdb_publish_time); fprintf(f, "Trust database entries: %ld\n", report->entries); } /* * state_report_log_reset_strategy - record how runtime metric resets work. * @strategy: configured reset strategy. * Returns nothing. */ void state_report_log_reset_strategy(reset_strategy_t strategy) { switch (strategy) { case RESET_NEVER: msg(LOG_INFO, "Metrics resets disabled; counters grow for daemon lifetime"); break; case RESET_AUTO: msg(LOG_INFO, "Metrics resets will occur only by interval timer reports"); break; case RESET_MANUAL: msg(LOG_INFO, "Metrics resets will occur only by privileged signal reports"); break; } } /* * open_report_file - open a report file for overwrite without symlinks. * @path: report path to open. * Return codes: * >= 0 - writable file descriptor * -1 - open or validation failed (errno set) */ static int open_report_file(const char *path) { struct stat st; int sfd; sfd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0640); if (sfd < 0) return -1; if (fstat(sfd, &st) == -1 || !S_ISREG(st.st_mode)) { close(sfd); errno = EINVAL; return -1; } return sfd; } /* * open_report_stream - open a report stream and log failures. * @path: report path to open. * Returns a FILE stream, or NULL on failure. */ static FILE *open_report_stream(const char *path) { int fd = open_report_file(path); FILE *f; if (fd < 0) { msg(LOG_WARNING, "cannot open %s: %s", path, strerror(errno)); return NULL; } f = fdopen(fd, "w"); if (!f) { msg(LOG_WARNING, "cannot fdopen %s: %s", path, strerror(errno)); close(fd); return NULL; } return f; } /* * decision_report - write policy decision metrics. * @f: output stream. * Returns nothing. */ void decision_report(FILE *f) { decision_report_reset(f, 0); } /* * decision_report_reset - write policy and failure metrics. * @f: output stream. * @reset: non-zero resets interval counters after copying them. * Returns nothing. */ void decision_report_reset(FILE *f, int reset) { failure_action_metrics_t failures; if (f == NULL) return; failure_action_snapshot(&failures, reset); decision_report_reset_with_failures(f, reset, &failures); decision_failure_action_report(f, &failures); } /* * decision_report_metrics_reset - write decision outcome metrics. * @f: output stream. * @reset: non-zero resets counters after copying them. * Returns nothing. */ void decision_report_metrics_reset(FILE *f, int reset) { decision_metrics_t metrics; database_generation_report_t trust_db; const char *reset_text; char reset_time[64]; if (f == NULL) return; getDecisionMetricsReset(&metrics, reset); reset_text = format_metrics_reset_time(reset_time, sizeof(reset_time)); if (reset_text == NULL) reset_text = "unavailable"; fprintf(f, "Last metrics reset: %s\n", reset_text); write_generation_line(f, "Config generation", decision_config_active_generation(), decision_config_active_effective_since()); write_generation_line(f, "Ruleset generation", metrics.ruleset_generation, metrics.ruleset_effective_since); if (database_generation_snapshot(&trust_db) == 0) write_trust_database_generation(f, &trust_db); fprintf(f, "\nDecision outcomes:\n"); fprintf(f, "Allowed accesses: %lu\n", getAllowedReset(reset)); fprintf(f, "Denied accesses: %lu\n", getDeniedReset(reset)); fprintf(f, "Allowed by rule: %lu\n", metrics.allowed_by_rule); fprintf(f, "Allowed by fallthrough: %lu\n", metrics.allowed_by_fallthrough); if (metrics.allowed_by_fallthrough) { fprintf(f, "Allowed by fallthrough open: %lu\n", metrics.fallthrough_open); fprintf(f, "Allowed by fallthrough execute: %lu\n", metrics.fallthrough_execute); fprintf(f, "Allowed by fallthrough trusted: %lu\n", metrics.fallthrough_trusted); fprintf(f, "Allowed by fallthrough untrusted: %lu\n", metrics.fallthrough_untrusted); fprintf(f, "Allowed by fallthrough trust unknown: %lu\n", metrics.fallthrough_trust_unknown); fprintf(f, "Allowed by fallthrough executable: %lu\n", metrics.fallthrough_executable); fprintf(f, "Allowed by fallthrough programmatic: %lu\n", metrics.fallthrough_programmatic); fprintf(f, "Allowed by fallthrough sharedlib: %lu\n", metrics.fallthrough_sharedlib); fprintf(f, "Allowed by fallthrough unknown ftype: %lu\n", metrics.fallthrough_unknown_ftype); fprintf(f, "Allowed by fallthrough other ftype: %lu\n", metrics.fallthrough_other_ftype); } } /* * decision_report_reset_with_failures - write policy metrics. * @f: output stream. * @reset: non-zero resets interval counters after copying them. * @failures: failure action metrics snapshot for reliability counters. * Returns nothing. */ void decision_report_reset_with_failures(FILE *f, int reset, const failure_action_metrics_t *failures) { decision_metrics_t metrics; if (f == NULL || failures == NULL) return; getDecisionMetricsReset(&metrics, reset); // Report results fprintf(f, "Kernel Queue Overflow: %lu\n", failure_action_metrics_count(failures, FAILURE_REASON_KERNEL_QUEUE_OVERFLOW)); fprintf(f, "Filesystem Errors: %lu\n", failure_action_metrics_count(failures, FAILURE_REASON_FANOTIFY_FS_ERROR)); fprintf(f, "Reply Errors: %lu\n", failure_action_metrics_count(failures, FAILURE_REASON_RESPONSE_WRITE_FAILURE)); fprintf(f, "Allowed accesses: %lu\n", getAllowedReset(reset)); fprintf(f, "Denied accesses: %lu\n", getDeniedReset(reset)); fprintf(f, "Allowed by rule: %lu\n", metrics.allowed_by_rule); fprintf(f, "Allowed by fallthrough: %lu\n", metrics.allowed_by_fallthrough); if (metrics.allowed_by_fallthrough) { fprintf(f, "Allowed by fallthrough open: %lu\n", metrics.fallthrough_open); fprintf(f, "Allowed by fallthrough execute: %lu\n", metrics.fallthrough_execute); fprintf(f, "Allowed by fallthrough trusted: %lu\n", metrics.fallthrough_trusted); fprintf(f, "Allowed by fallthrough untrusted: %lu\n", metrics.fallthrough_untrusted); fprintf(f, "Allowed by fallthrough trust unknown: %lu\n", metrics.fallthrough_trust_unknown); fprintf(f, "Allowed by fallthrough executable: %lu\n", metrics.fallthrough_executable); fprintf(f, "Allowed by fallthrough programmatic: %lu\n", metrics.fallthrough_programmatic); fprintf(f, "Allowed by fallthrough sharedlib: %lu\n", metrics.fallthrough_sharedlib); fprintf(f, "Allowed by fallthrough unknown ftype: %lu\n", metrics.fallthrough_unknown_ftype); fprintf(f, "Allowed by fallthrough other ftype: %lu\n", metrics.fallthrough_other_ftype); } write_generation_line(f, "Ruleset generation", metrics.ruleset_generation, metrics.ruleset_effective_since); policy_rule_hits_report_reset(f, reset); attr_lookup_metrics_report(f, reset); } /* * decision_failure_action_report - write failure action metrics. * @f: output stream. * @failures: failure action metrics snapshot to report. * Returns nothing. */ void decision_failure_action_report(FILE *f, const failure_action_metrics_t *failures) { if (f == NULL || failures == NULL) return; failure_action_metrics_report(f, failures); } /* * metric_reset_allowed - decide whether a report should reset counters. * @reason: why the report is being generated. * @reset_requests: number of pending signal-based reset requests. * Returns 1 when counters should be reset after this report snapshot. */ static int metric_reset_allowed(enum state_report_reason reason, unsigned int reset_requests) { reset_strategy_t strategy; int uid; strategy = __atomic_load_n(&config.reset_strategy, __ATOMIC_RELAXED); if (strategy == RESET_AUTO && reason == STATE_REPORT_INTERVAL) return 1; uid = atomic_load_explicit(&signal_report_reset_request_uid, memory_order_relaxed); if (strategy == RESET_MANUAL && reason == STATE_REPORT_SIGNAL && reset_requests && uid == 0) return 1; return 0; } /* * log_manual_metric_reset - log a manual reset request from SIGUSR1. * @reset_requests: number of requests consumed by this report. * @reset: non-zero when the request reset counters. * Returns nothing. */ static void log_manual_metric_reset(unsigned int reset_requests, int reset) { reset_strategy_t strategy; int pid, uid; if (reset_requests == 0) return; strategy = __atomic_load_n(&config.reset_strategy, __ATOMIC_RELAXED); if (strategy != RESET_MANUAL) return; pid = atomic_load_explicit(&signal_report_reset_request_pid, memory_order_relaxed); uid = atomic_load_explicit(&signal_report_reset_request_uid, memory_order_relaxed); if (pid > 0) msg(LOG_INFO, "Manual metrics reset requested by pid=%d uid=%d " "(requests=%u): %s", pid, uid, reset_requests, reset ? "resetting counters after metrics report" : "not resetting counters"); else msg(LOG_INFO, "Manual metrics reset requested (requests=%u): %s", reset_requests, reset ? "resetting counters after metrics report" : "not resetting counters"); if (!reset) { if (uid != 0) msg(LOG_INFO, "Manual metrics reset ignored because uid=%d " "is not privileged", uid); else msg(LOG_INFO, "Manual metrics reset ignored because report was not " "signal based"); } } /* * state_report_intent_for_write - consume the pending report intent. * @reason: report trigger used to decide the default. * @requests: number of signal report requests consumed. * Returns the report intent to write. */ static report_intent_t state_report_intent_for_write( enum state_report_reason reason, unsigned int requests) { unsigned int intent; if (reason == STATE_REPORT_INTERVAL || requests == 0) return REPORT_INTENT_STATUS; intent = atomic_exchange_explicit(&signal_report_intent, REPORT_INTENT_STATUS, memory_order_relaxed); switch (intent) { case REPORT_INTENT_STATUS: case REPORT_INTENT_METRICS: case REPORT_INTENT_RESET_METRICS: return intent; case REPORT_INTENT_TIMING_ARM: case REPORT_INTENT_TIMING_STOP: break; } return REPORT_INTENT_STATUS; } /* * write_state_report_file - write the daemon state report. * Returns 0 on success, non-zero on open failure. */ static int write_state_report_file(void) { FILE *f = open_report_stream(STAT_REPORT); if (!f) return -1; do_state_report(f, 0); fclose(f); return 0; } /* * write_metrics_report_file - write the daemon metrics report. * @reset: non-zero resets metrics after snapshotting them. * Returns 0 on success, non-zero on open failure. */ static int write_metrics_report_file(int reset) { FILE *f = open_report_stream(METRICS_REPORT); if (!f) return -1; do_metrics_report_reset(f, reset); fclose(f); return 0; } /* * record_metrics_reset - remember a successful metrics reset time. * Returns nothing. */ static void record_metrics_reset(void) { time_t now = time(NULL); if (now != (time_t)-1) last_metrics_reset = now; } /* * state_report_write - write a state report to the standard location. * @reason: report trigger used to apply reset_strategy. * Returns nothing. */ void state_report_write(enum state_report_reason reason) { unsigned int reset_requests; unsigned int report_requests; report_intent_t intent; int reset; report_requests = atomic_exchange_explicit(&signal_report_requests, 0, memory_order_relaxed); reset_requests = atomic_exchange_explicit(&signal_report_reset_requests, 0, memory_order_relaxed); reset = metric_reset_allowed(reason, reset_requests); log_manual_metric_reset(reset_requests, reset); if (reason == STATE_REPORT_INTERVAL) { write_state_report_file(); if (write_metrics_report_file(reset) == 0 && reset) record_metrics_reset(); return; } intent = state_report_intent_for_write(reason, report_requests); if (intent == REPORT_INTENT_METRICS || intent == REPORT_INTENT_RESET_METRICS) { if (write_metrics_report_file(reset) == 0 && reset) record_metrics_reset(); return; } write_state_report_file(); } /* * state_report_reason_for_triggers - classify the pending report trigger. * @expired: non-zero when the interval timer expired. * Returns the trigger to use when applying reset_strategy. */ enum state_report_reason state_report_reason_for_triggers(int expired) { if (atomic_load_explicit(&signal_report_requests, memory_order_relaxed)) return STATE_REPORT_SIGNAL; return expired ? STATE_REPORT_INTERVAL : STATE_REPORT_SIGNAL; } fapolicyd-2.0/src/daemon/state-report.h000066400000000000000000000034351524071644000201710ustar00rootroot00000000000000/* * state-report.h - daemon state report coordination * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef STATE_REPORT_HEADER #define STATE_REPORT_HEADER #include "conf.h" #include "database.h" #include "failure-action.h" #include #include #include #include enum state_report_reason { STATE_REPORT_SIGNAL, STATE_REPORT_INTERVAL, }; struct state_report_operating_mode { bool permissive; const char *integrity; const char *reset_strategy; unsigned int config_generation; unsigned int ruleset_generation; time_t config_effective_since; time_t ruleset_effective_since; database_generation_report_t trust_db; const conf_t *config; }; void usr1_handler(int sig, siginfo_t *info, void *context); void state_report_log_reset_strategy(reset_strategy_t strategy); enum state_report_reason state_report_reason_for_triggers(int expired); void state_report_write(enum state_report_reason reason); void state_report_operating_mode(FILE *f, const struct state_report_operating_mode *mode); void do_state_report(FILE *f, int shutdown); void do_stat_report(FILE *f, int shutdown); void do_metrics_report_reset(FILE *f, int reset); void do_stat_report_reset(FILE *f, int shutdown, int reset); void decision_report(FILE *f); void decision_report_reset(FILE *f, int reset); void decision_report_metrics_reset(FILE *f, int reset); void decision_report_reset_with_failures(FILE *f, int reset, const failure_action_metrics_t *failures); void decision_failure_action_report(FILE *f, const failure_action_metrics_t *failures); #endif fapolicyd-2.0/src/daemon/systemd-notify.c000066400000000000000000000132701524071644000205270ustar00rootroot00000000000000/* * systemd-notify.c - minimal systemd notify/watchdog integration * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include "systemd-notify.h" #ifndef MSG_NOSIGNAL #define MSG_NOSIGNAL 0 #endif #ifndef SOCK_CLOEXEC #define SOCK_CLOEXEC 0 #endif #ifdef FAPOLICYD_ENABLE_SYSTEMD_WATCHDOG static atomic_int notify_enabled = ATOMIC_VAR_INIT(1); /* * notify_runtime_enabled - check whether runtime mode allows notification. * Returns non-zero when sd_notify/watchdog environment may be consumed. */ static int notify_runtime_enabled(void) { return atomic_load_explicit(¬ify_enabled, memory_order_relaxed) != 0; } /* * notify_address - build the AF_UNIX destination from NOTIFY_SOCKET. * @path: NOTIFY_SOCKET value from systemd. * @addr: destination socket address. * @addr_len: destination address length. * * Systemd accepts either filesystem paths or abstract namespace sockets where * the environment value starts with '@'. Returns 0 on success and -1 with errno * set on invalid input. */ static int notify_address(const char *path, struct sockaddr_un *addr, socklen_t *addr_len) { size_t len; if (path == NULL || *path == 0 || addr == NULL || addr_len == NULL) { errno = EINVAL; return -1; } memset(addr, 0, sizeof(*addr)); addr->sun_family = AF_UNIX; if (path[0] == '@') { len = strlen(path + 1); if (len + 1 > sizeof(addr->sun_path)) { errno = ENAMETOOLONG; return -1; } addr->sun_path[0] = 0; memcpy(addr->sun_path + 1, path + 1, len); *addr_len = offsetof(struct sockaddr_un, sun_path) + 1 + len; return 0; } len = strlen(path); if (len >= sizeof(addr->sun_path)) { errno = ENAMETOOLONG; return -1; } memcpy(addr->sun_path, path, len + 1); *addr_len = offsetof(struct sockaddr_un, sun_path) + len + 1; return 0; } /* * notify_send - send one systemd notification datagram. * @state: newline-separated sd_notify state fields. * Returns 0 when notification is disabled or the datagram was sent, -1 on * local socket/send failure. */ static int notify_send(const char *state) { const char *path; struct sockaddr_un addr; socklen_t addr_len; int fd, saved_errno; if (!notify_runtime_enabled() || state == NULL) return 0; path = getenv("NOTIFY_SOCKET"); if (path == NULL || *path == 0) return 0; if (notify_address(path, &addr, &addr_len)) return -1; fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0); if (fd < 0) return -1; if (sendto(fd, state, strlen(state), MSG_NOSIGNAL | MSG_DONTWAIT, (const struct sockaddr *)&addr, addr_len) < 0) { saved_errno = errno; close(fd); errno = saved_errno; return -1; } close(fd); return 0; } /* * watchdog_pid_matches - check whether systemd targeted this process. * Returns 1 when WATCHDOG_PID is unset or matches getpid(), 0 otherwise. */ static int watchdog_pid_matches(void) { const char *pid_env = getenv("WATCHDOG_PID"); char *end = NULL; unsigned long pid; if (pid_env == NULL || *pid_env == 0) return 1; errno = 0; pid = strtoul(pid_env, &end, 10); if (errno || end == pid_env || *end != 0) return 0; return pid == (unsigned long)getpid(); } #endif /* * systemd_notify_set_enabled - enable or disable systemd notification. * @enabled: non-zero enables notify/watchdog environment consumption. * Returns nothing. */ void systemd_notify_set_enabled(int enabled) { #ifdef FAPOLICYD_ENABLE_SYSTEMD_WATCHDOG atomic_store_explicit(¬ify_enabled, enabled ? 1 : 0, memory_order_relaxed); #else (void)enabled; #endif } /* * systemd_notify_ready - tell systemd startup completed. * Returns 0 when disabled or sent, -1 on local notification failure. */ int systemd_notify_ready(void) { #ifdef FAPOLICYD_ENABLE_SYSTEMD_WATCHDOG return notify_send("READY=1\nSTATUS=Ready"); #else return 0; #endif } /* * systemd_notify_stopping - tell systemd shutdown started. * Returns 0 when disabled or sent, -1 on local notification failure. */ int systemd_notify_stopping(void) { #ifdef FAPOLICYD_ENABLE_SYSTEMD_WATCHDOG return notify_send("STOPPING=1\nSTATUS=Stopping"); #else return 0; #endif } /* * systemd_watchdog_ping - refresh the systemd watchdog deadline. * Returns 0 when disabled or sent, -1 on local notification failure. */ int systemd_watchdog_ping(void) { #ifdef FAPOLICYD_ENABLE_SYSTEMD_WATCHDOG return notify_send("WATCHDOG=1"); #else return 0; #endif } /* * systemd_watchdog_interval_usec - return configured watchdog interval. * Returns WATCHDOG_USEC when systemd enabled the watchdog for this process, or * zero when notification/watchdog support is disabled. */ uint64_t systemd_watchdog_interval_usec(void) { #ifdef FAPOLICYD_ENABLE_SYSTEMD_WATCHDOG const char *usec_env; char *end = NULL; unsigned long long usec; if (!notify_runtime_enabled()) return 0; if (getenv("NOTIFY_SOCKET") == NULL || !watchdog_pid_matches()) return 0; usec_env = getenv("WATCHDOG_USEC"); if (usec_env == NULL || *usec_env == 0) return 0; errno = 0; usec = strtoull(usec_env, &end, 10); if (errno || end == usec_env || *end != 0 || usec == 0) return 0; return (uint64_t)usec; #else return 0; #endif } /* * systemd_watchdog_enabled - check whether systemd watchdog pings are active. * Returns 1 when systemd supplied a usable watchdog interval, 0 otherwise. */ int systemd_watchdog_enabled(void) { return systemd_watchdog_interval_usec() != 0; } fapolicyd-2.0/src/daemon/systemd-notify.h000066400000000000000000000012461524071644000205340ustar00rootroot00000000000000/* * systemd-notify.h - minimal systemd notify/watchdog integration * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef SYSTEMD_NOTIFY_HEADER #define SYSTEMD_NOTIFY_HEADER #include void systemd_notify_set_enabled(int enabled); int systemd_notify_ready(void); int systemd_notify_stopping(void); int systemd_watchdog_ping(void); uint64_t systemd_watchdog_interval_usec(void); int systemd_watchdog_enabled(void); #endif fapolicyd-2.0/src/daemon/worker-health.c000066400000000000000000000354031524071644000203070ustar00rootroot00000000000000/* * worker-health.c - decision worker health and watchdog monitor * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ /* * Overview * -------- * * worker-health.c owns the health timestamps embedded in each decision worker * and the monitor thread that interprets those timestamps. Its charter is to * record worker loop progress, distinguish idle workers from workers inside a * policy decision, detect the existing queue-backed stall condition, refresh * the systemd watchdog when all workers are healthy, and execute the * worker_stall failure action when a stall is observed. * * The state owned here is deliberately small: struct worker_health fields, * the monitor thread handle, the monitor callback wiring, and watchdog log * rate limiting. This file must not own fanotify descriptors, worker queues, * worker routing, decision contexts, or shutdown draining. Those are worker * pool responsibilities. * * The monitor sees neighboring state through worker_health_view snapshots * supplied by the worker pool. That keeps queue metrics and thread IDs in the * worker module while still letting this module make the same health decision * as the former notify.c implementation. systemd-notify.c remains the * low-level adapter; this module only calls its public watchdog helpers and * never reads systemd environment variables directly. */ #include "config.h" #include #include #include #include #include #include #include #include #include "daemon-config.h" #include "failure-action.h" #include "message.h" #include "queue.h" #include "string-util.h" #include "systemd-notify.h" #include "worker-health.h" #define HEALTH_MONITOR_INTERVAL_SEC 3 #define HEALTH_QUEUE_BACKLOG_THRESHOLD 5 #define NSEC_PER_SEC 1000000000ULL #define USEC_TO_NSEC 1000ULL #define SYSTEMD_WATCHDOG_LOG_INTERVAL 60 extern atomic_bool stop, run_stats; enum worker_health_state { WORKER_HEALTH_IDLE, WORKER_HEALTH_BUSY, }; struct worker_health_snapshot { enum worker_health_state state; uint64_t heartbeat_age_ns; uint64_t current_event_age_ns; }; struct worker_health_monitor { pthread_t thread; worker_health_collect_fn collect; worker_health_nudge_fn nudge; void *ctx; int running; }; static struct worker_health_monitor monitor; static struct message_rate_limit systemd_watchdog_log = MESSAGE_RATE_LIMIT_INIT(SYSTEMD_WATCHDOG_LOG_INTERVAL); static void *health_monitor_thread_main(void *arg); /* * health_now_ns - read monotonic time for worker health checks. * Returns monotonic nanoseconds, or zero if the clock cannot be read. */ static uint64_t health_now_ns(void) { struct timespec ts; if (clock_gettime(CLOCK_MONOTONIC, &ts)) return 0; return (uint64_t)ts.tv_sec * NSEC_PER_SEC + (uint64_t)ts.tv_nsec; } /* * health_age_ns - compute an age from a monotonic timestamp. * @now: current monotonic time in nanoseconds. * @timestamp: older monotonic timestamp, or zero when unset. * Returns age in nanoseconds, or zero when the timestamp is unset or invalid. */ static uint64_t health_age_ns(uint64_t now, uint64_t timestamp) { if (timestamp == 0 || now < timestamp) return 0; return now - timestamp; } /* * worker_health_state_name - convert a worker health state to report text. * @state: health state to describe. * Returns a static string for reports and logs. */ static const char *worker_health_state_name(enum worker_health_state state) { switch (state) { case WORKER_HEALTH_IDLE: return "idle"; case WORKER_HEALTH_BUSY: return "busy"; } return "unknown"; } /* * worker_health_init - initialize health timestamps for one worker slot. * @health: worker health state being initialized. * Returns nothing. */ void worker_health_init(struct worker_health *health) { uint64_t now; if (health == NULL) return; now = health_now_ns(); atomic_store_explicit(&health->heartbeat_ns, now, memory_order_relaxed); atomic_store_explicit(&health->current_event_started_ns, 0, memory_order_relaxed); atomic_store_explicit(&health->last_completed_event_ns, 0, memory_order_relaxed); atomic_store_explicit(&health->stall_reported, false, memory_order_relaxed); } /* * worker_health_heartbeat - record worker loop progress. * @health: worker health state that made progress. * Returns nothing. */ void worker_health_heartbeat(struct worker_health *health) { if (health == NULL) return; atomic_store_explicit(&health->heartbeat_ns, health_now_ns(), memory_order_relaxed); } /* * worker_health_event_begin - mark the start of a policy decision. * @health: worker health state processing the event. * Returns nothing. */ void worker_health_event_begin(struct worker_health *health) { uint64_t now; if (health == NULL) return; now = health_now_ns(); atomic_store_explicit(&health->current_event_started_ns, now, memory_order_release); atomic_store_explicit(&health->heartbeat_ns, now, memory_order_relaxed); } /* * worker_health_event_end - mark a completed policy decision. * @health: worker health state that finished processing. * Returns nothing. */ void worker_health_event_end(struct worker_health *health) { uint64_t now; if (health == NULL) return; now = health_now_ns(); atomic_store_explicit(&health->last_completed_event_ns, now, memory_order_relaxed); atomic_store_explicit(&health->current_event_started_ns, 0, memory_order_release); atomic_store_explicit(&health->heartbeat_ns, now, memory_order_relaxed); atomic_store_explicit(&health->stall_reported, false, memory_order_relaxed); } /* * worker_health_snapshot - copy one worker's health timestamps. * @health: worker health state to inspect. * @snapshot: destination snapshot. * @now: current monotonic time in nanoseconds. * Returns nothing. */ static void worker_health_snapshot(const struct worker_health *health, struct worker_health_snapshot *snapshot, uint64_t now) { uint64_t heartbeat, current; if (health == NULL || snapshot == NULL) return; heartbeat = atomic_load_explicit(&health->heartbeat_ns, memory_order_relaxed); current = atomic_load_explicit(&health->current_event_started_ns, memory_order_acquire); if (current != 0) snapshot->state = WORKER_HEALTH_BUSY; else snapshot->state = WORKER_HEALTH_IDLE; snapshot->heartbeat_age_ns = health_age_ns(now, heartbeat); snapshot->current_event_age_ns = health_age_ns(now, current); } /* * worker_health_report - write one compact decision-worker health line. * @f: report stream. * @worker_id: worker identifier for report text. * @health: worker health state to describe. */ void worker_health_report(FILE *f, unsigned int worker_id, const struct worker_health *health) { struct worker_health_snapshot snapshot = { 0 }; char heartbeat[32], current[32]; const char *state; uint64_t now; if (f == NULL || health == NULL) return; now = health_now_ns(); worker_health_snapshot(health, &snapshot, now); fapolicyd_format_ns(snapshot.heartbeat_age_ns, heartbeat, sizeof(heartbeat)); if (snapshot.state == WORKER_HEALTH_IDLE) snprintf(current, sizeof(current), "idle"); else fapolicyd_format_ns(snapshot.current_event_age_ns, current, sizeof(current)); if (atomic_load_explicit(&health->stall_reported, memory_order_relaxed)) state = "stalled"; else state = worker_health_state_name(snapshot.state); fprintf(f, " Decision worker %u health: state=%s heartbeat=%s " "current_event=%s\n", worker_id, state, heartbeat, current); } /* * health_monitor_interval_ns - choose the monitor wake interval. * Returns half the systemd watchdog interval when enabled, otherwise the * legacy three-second monitor cadence. */ static uint64_t health_monitor_interval_ns(void) { uint64_t watchdog = systemd_watchdog_interval_usec(); if (watchdog) return (watchdog * USEC_TO_NSEC) / 2; return HEALTH_MONITOR_INTERVAL_SEC * NSEC_PER_SEC; } /* * health_stall_timeout_ns - choose the worker stall deadline. * Returns the systemd watchdog interval when enabled, otherwise the legacy * three-second stall deadline. */ static uint64_t health_stall_timeout_ns(void) { uint64_t watchdog = systemd_watchdog_interval_usec(); if (watchdog) return watchdog * USEC_TO_NSEC; return HEALTH_MONITOR_INTERVAL_SEC * NSEC_PER_SEC; } /* * health_monitor_sleep - sleep in short chunks so shutdown is not delayed. * @interval_ns: requested sleep interval in nanoseconds. * Returns nothing. */ static void health_monitor_sleep(uint64_t interval_ns) { while (!atomic_load_explicit(&stop, memory_order_relaxed) && interval_ns > 0) { struct timespec ts; uint64_t chunk = interval_ns; if (chunk > NSEC_PER_SEC) chunk = NSEC_PER_SEC; ts.tv_sec = (time_t)(chunk / NSEC_PER_SEC); ts.tv_nsec = (long)(chunk % NSEC_PER_SEC); while (nanosleep(&ts, &ts) && errno == EINTR) ; interval_ns -= chunk; } } /* * worker_health_is_stalled - decide whether one worker stopped progressing. * @snapshot: worker health timestamp snapshot. * @metrics: queue metrics for the worker. * @timeout_ns: stall deadline in nanoseconds. * * A single slow current decision can be legitimate storage latency, so the * compatibility action requires visible queued pressure before treating it as * a daemon-level stall. If no decision is active, an old queued event plus a * stale heartbeat means the worker is not draining its queue. * * Returns 1 when the worker is stalled, 0 otherwise. */ static int worker_health_is_stalled( const struct worker_health_snapshot *snapshot, const struct queue_metrics *metrics, uint64_t timeout_ns) { if (snapshot == NULL || metrics == NULL || timeout_ns == 0) return 0; if (snapshot->state != WORKER_HEALTH_IDLE && snapshot->current_event_age_ns >= timeout_ns && (metrics->current_depth > HEALTH_QUEUE_BACKLOG_THRESHOLD || metrics->oldest_age_ns >= timeout_ns)) return 1; if (snapshot->state == WORKER_HEALTH_IDLE && metrics->current_depth > 0 && metrics->oldest_age_ns >= timeout_ns && snapshot->heartbeat_age_ns >= timeout_ns) return 1; return 0; } /* * health_monitor_log_stall - record and report one worker stall. * @view: snapshot of the stalled worker supplied by the worker pool. * @snapshot: worker health snapshot. * Returns nothing. */ static void health_monitor_log_stall( const struct worker_health_view *view, const struct worker_health_snapshot *snapshot) { char heartbeat[32], current[32], oldest[32]; unsigned long total; if (view == NULL || snapshot == NULL || view->health == NULL) return; if (atomic_exchange_explicit(&view->health->stall_reported, true, memory_order_relaxed)) return; total = failure_action_record(FAILURE_REASON_WORKER_STALL); fapolicyd_format_ns(snapshot->heartbeat_age_ns, heartbeat, sizeof(heartbeat)); if (snapshot->state == WORKER_HEALTH_IDLE) snprintf(current, sizeof(current), "idle"); else fapolicyd_format_ns(snapshot->current_event_age_ns, current, sizeof(current)); fapolicyd_format_ns(view->metrics.oldest_age_ns, oldest, sizeof(oldest)); msg(LOG_ERR, "Health monitor detected stalled decision worker %u: TID=%d " "state=%s heartbeat=%s current_event=%s " "queue_depth=%u oldest_queued=%s action=%s " "worker_stall=%lu", view->id, view->tid, "stalled", heartbeat, current, view->metrics.current_depth, oldest, failure_action_name(failure_reason_action( FAILURE_REASON_WORKER_STALL)), total); atomic_store_explicit(&run_stats, true, memory_order_relaxed); if (monitor.nudge) monitor.nudge(monitor.ctx); } /* * health_monitor_check_workers - check every worker once. * @timeout_ns: stall deadline in nanoseconds. * Returns 1 when all workers are healthy, 0 when at least one is stalled. */ static int health_monitor_check_workers(uint64_t timeout_ns) { struct worker_health_view views[DAEMON_CONFIG_DECISION_THREADS_MAX]; unsigned int count, i; uint64_t now = health_now_ns(); int healthy = 1; if (monitor.collect == NULL) return healthy; count = monitor.collect(monitor.ctx, views, DAEMON_CONFIG_DECISION_THREADS_MAX); for (i = 0; i < count; i++) { struct worker_health_snapshot snapshot = { 0 }; int stalled; if (views[i].health == NULL) continue; worker_health_snapshot(views[i].health, &snapshot, now); stalled = worker_health_is_stalled(&snapshot, &views[i].metrics, timeout_ns); if (stalled) { healthy = 0; health_monitor_log_stall(&views[i], &snapshot); failure_action_execute(FAILURE_REASON_WORKER_STALL); } else { atomic_store_explicit(&views[i].health->stall_reported, false, memory_order_relaxed); } } return healthy; } /* * health_monitor_thread_main - monitor worker progress and watchdog state. * @arg: unused pthread argument. * Returns NULL when the monitor exits. */ static void *health_monitor_thread_main(void *arg) { sigset_t sigs; (void)arg; /* This is a worker thread. Don't handle external signals. */ sigemptyset(&sigs); sigaddset(&sigs, SIGTERM); sigaddset(&sigs, SIGHUP); sigaddset(&sigs, SIGUSR1); sigaddset(&sigs, SIGINT); sigaddset(&sigs, SIGQUIT); pthread_sigmask(SIG_SETMASK, &sigs, NULL); do { uint64_t interval_ns = health_monitor_interval_ns(); uint64_t timeout_ns = health_stall_timeout_ns(); int healthy = health_monitor_check_workers(timeout_ns); if (healthy && systemd_watchdog_enabled() && systemd_watchdog_ping()) { time_t now = time(NULL); if (message_rate_limit_allow(&systemd_watchdog_log, now)) msg(LOG_WARNING, "Cannot refresh systemd watchdog deadline"); } health_monitor_sleep(interval_ns); } while (!stop); return NULL; } /* * worker_health_monitor_start - start decision worker health monitoring. * @collect: callback that snapshots worker health inputs. * @nudge: callback used to wake worker queues after a stall. * @ctx: callback context. * * Returns zero on success or the pthread_create error code. */ int worker_health_monitor_start(worker_health_collect_fn collect, worker_health_nudge_fn nudge, void *ctx) { int rc; monitor.collect = collect; monitor.nudge = nudge; monitor.ctx = ctx; monitor.running = 0; rc = pthread_create(&monitor.thread, NULL, health_monitor_thread_main, NULL); if (rc) { monitor.collect = NULL; monitor.nudge = NULL; monitor.ctx = NULL; return rc; } monitor.running = 1; return 0; } /* * worker_health_monitor_join - wait for the monitor thread after shutdown. * Returns nothing. */ void worker_health_monitor_join(void) { if (!monitor.running) return; pthread_join(monitor.thread, NULL); monitor.running = 0; monitor.collect = NULL; monitor.nudge = NULL; monitor.ctx = NULL; } fapolicyd-2.0/src/daemon/worker-health.h000066400000000000000000000026321524071644000203120ustar00rootroot00000000000000/* * worker-health.h - decision worker health tracking * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef WORKER_HEALTH_HEADER #define WORKER_HEALTH_HEADER #include #include #include #include "queue.h" struct worker_health { atomic_ullong heartbeat_ns; atomic_ullong current_event_started_ns; atomic_ullong last_completed_event_ns; atomic_bool stall_reported; }; struct worker_health_view { unsigned int id; int tid; struct worker_health *health; struct queue_metrics metrics; }; typedef unsigned int (*worker_health_collect_fn)(void *ctx, struct worker_health_view *views, unsigned int max); typedef void (*worker_health_nudge_fn)(void *ctx); void worker_health_init(struct worker_health *health); void worker_health_heartbeat(struct worker_health *health); void worker_health_event_begin(struct worker_health *health); void worker_health_event_end(struct worker_health *health); void worker_health_report(FILE *f, unsigned int worker_id, const struct worker_health *health); int worker_health_monitor_start(worker_health_collect_fn collect, worker_health_nudge_fn nudge, void *ctx); void worker_health_monitor_join(void); #endif fapolicyd-2.0/src/handler/000077500000000000000000000000001524071644000155345ustar00rootroot00000000000000fapolicyd-2.0/src/handler/fapolicyd-deb-loader.c000066400000000000000000000066461524071644000216620ustar00rootroot00000000000000/* * fapolicyd-deb-loader.c - Debian loader tool for fapolicyd * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "conf.h" #include "fapolicyd-backend.h" #include "message.h" atomic_bool stop = 0; // Library needs this unsigned int debug_mode = 0; // Library needs this conf_t config; // Library needs this int do_deb_init_backend(void); int do_deb_load_list(const conf_t *conf, int memfd); int do_deb_destroy_backend(void); extern backend deb_backend; // fd 3 is installed by the daemon-side backend before exec. int sock_fd = 3; int main(int argc, char * const argv[]) { /* The daemon ignores SIGCHLD only for its direct package loaders. */ signal(SIGCHLD, SIG_DFL); set_message_mode(MSG_SYSLOG, DBG_NO); openlog("fapolicyd-deb-loader", LOG_PID, LOG_DAEMON); int memfd = memfd_create("deb_snapshot", MFD_CLOEXEC|MFD_ALLOW_SEALING); if (memfd < 0) { msg(LOG_ERR, "memfd_create failed"); return 1; } if (do_deb_init_backend()) { msg(LOG_ERR, "Failed to initialize deb loader backend"); close(memfd); return 1; } if (do_deb_load_list(&config, memfd)) { msg(LOG_ERR, "Failed to populate deb backend snapshot"); do_deb_destroy_backend(); close(memfd); return 1; } msg(LOG_INFO, "Loaded files %ld", deb_backend.entries); /* Seal the snapshot so readers see a stable view. */ if (fcntl(memfd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE) == -1) // Not a fatal error msg(LOG_WARNING, "Failed to seal deb backend memfd (%s)", strerror(errno)); lseek(memfd, 0, SEEK_SET); /* * Sending the sealed memfd commits success to the daemon. Finish all * package cleanup first so the daemon never needs to wait for our exit. */ do_deb_destroy_backend(); // Send the snapshot fd to the daemon. struct msghdr _msg = {0}; struct iovec iov = { .iov_base = (char[1]){0}, .iov_len = 1 }; union { struct cmsghdr align; char buf[CMSG_SPACE(sizeof(int))]; } cmsgbuf; _msg.msg_iov = &iov; _msg.msg_iovlen = 1; _msg.msg_control = cmsgbuf.buf; _msg.msg_controllen = sizeof cmsgbuf.buf; struct cmsghdr *c = CMSG_FIRSTHDR(&_msg); c->cmsg_level = SOL_SOCKET; c->cmsg_type = SCM_RIGHTS; c->cmsg_len = CMSG_LEN(sizeof(int)); memcpy(CMSG_DATA(c), &memfd, sizeof(int)); if (sendmsg(sock_fd, &_msg, 0) < 0) { char err_buff[256]; msg(LOG_ERR, "sendmsg failed (%s)", strerror_r(errno, err_buff, sizeof(err_buff))); close(sock_fd); close(memfd); return 1; } close(sock_fd); close(memfd); return 0; } fapolicyd-2.0/src/handler/fapolicyd-rpm-loader.c000066400000000000000000000075341524071644000217230ustar00rootroot00000000000000/* * fapolicy-rpm-loader.c - loader tool for fapolicyd * Copyright (c) 2025-26 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "backend-manager.h" #include "daemon-config.h" #include "message.h" #include "fd-fgets.h" #include "paths.h" atomic_bool stop = 0; // Library needs this unsigned int debug_mode = 0; // Library needs this conf_t config; // Library needs this int do_rpm_init_backend(void); int do_rpm_load_list(const conf_t * conf, int memfd); int do_rpm_destroy_backend(void); extern backend rpm_backend; // fetch the socket FD number – defaults to 3 if env not set int sock_fd = 3; // same number dup2’ed by parent int main(int argc, char * const argv[]) { /* The daemon ignores SIGCHLD only for its direct package loaders. */ signal(SIGCHLD, SIG_DFL); set_message_mode(MSG_SYSLOG, DBG_NO); openlog("fapolicyd-rpm-loader", LOG_PID, LOG_DAEMON); if (load_daemon_config(&config)) { free_daemon_config(&config); msg(LOG_ERR, "Exiting due to bad configuration"); return 1; } int memfd = memfd_create("rpm_snapshot", MFD_CLOEXEC|MFD_ALLOW_SEALING); if (memfd < 0) { msg(LOG_ERR, "memfd_create failed"); exit(1); } if (do_rpm_init_backend()) { msg(LOG_ERR, "Failed to initialize rpm loader backend"); exit(1); } if (do_rpm_load_list(&config, memfd)) { msg(LOG_ERR, "Failed to populate rpm backend snapshot"); exit(1); } msg(LOG_INFO, "Loaded files %ld", rpm_backend.entries); /* Seal the snapshot so readers see a stable view. */ if (fcntl(memfd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE) == -1) // Not a fatal error msg(LOG_WARNING, "Failed to seal rpm backend memfd (%s)", strerror(errno)); lseek(memfd, 0, SEEK_SET); /* rewind – not strictly needed */ /* * Sending the sealed memfd commits success to the daemon. Finish all * package cleanup first so the daemon never needs to wait for our exit. */ do_rpm_destroy_backend(); free_daemon_config(&config); // send the FD struct msghdr _msg = {0}; struct iovec iov = { .iov_base = (char[1]){0}, .iov_len = 1 }; union { struct cmsghdr align; char buf[CMSG_SPACE(sizeof(int))]; } cmsgbuf; _msg.msg_iov = &iov; _msg.msg_iovlen = 1; _msg.msg_control = cmsgbuf.buf; _msg.msg_controllen = sizeof cmsgbuf.buf; struct cmsghdr *c = CMSG_FIRSTHDR(&_msg); c->cmsg_level = SOL_SOCKET; c->cmsg_type = SCM_RIGHTS; c->cmsg_len = CMSG_LEN(sizeof(int)); memcpy(CMSG_DATA(c), &memfd, sizeof(int)); if (sendmsg(sock_fd, &_msg, 0) < 0) { char err_buff[256]; msg(LOG_ERR, "sendmsg failed (%s)", strerror_r(errno, err_buff, sizeof(err_buff))); exit(1); } close(sock_fd); // closes the channel; parent gets EOF close(memfd); // parent has its own refcount return 0; } fapolicyd-2.0/src/library/000077500000000000000000000000001524071644000155635ustar00rootroot00000000000000fapolicyd-2.0/src/library/attr-lookup-metrics.c000066400000000000000000000303151524071644000216560ustar00rootroot00000000000000/* * attr-lookup-metrics.c - subject/object attribute lookup counters * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #include "config.h" #include #include #include "attr-lookup-metrics.h" #define ATTR_LOOKUP_METRICS_MAX_WORKERS 32 #define ATTR_LOOKUP_LABEL_WIDTH 13 #define ATTR_LOOKUP_NAME_WIDTH 9 #define ATTR_LOOKUP_REQUEST_NARROW_WIDTH 10 #define ATTR_LOOKUP_REQUEST_WIDE_WIDTH 20 #define ATTR_LOOKUP_REQUEST_NARROW_MAX UINT32_MAX struct attr_lookup_counter { atomic_ullong requests; atomic_ullong lookups; }; struct attr_lookup_report_row { const char *label; const char *name; const struct attr_lookup_metric_snapshot *snapshot; }; /* * Keep counters in worker-local blocks so a future decision worker pool can * update attributes without bouncing one shared cache line. */ struct attr_lookup_worker_block { struct attr_lookup_counter subjects[SUBJ_COUNT]; struct attr_lookup_counter objects[OBJ_COUNT]; } __attribute__((aligned(64))); static struct attr_lookup_worker_block workers[ATTR_LOOKUP_METRICS_MAX_WORKERS]; static atomic_int attr_lookup_request_width = ATOMIC_VAR_INIT(ATTR_LOOKUP_REQUEST_NARROW_WIDTH); static __thread unsigned int attr_lookup_worker; /* * attr_lookup_metrics_set_worker - select the caller's metric block. * @worker_id: decision worker identifier. * Returns nothing. */ void attr_lookup_metrics_set_worker(unsigned int worker_id) { attr_lookup_worker = worker_id; } /* * subject_index - convert a real subject attribute to a counter index. * @type: subject attribute. * @index: destination for the array index. * Return codes: * 0 - index populated. * 1 - pseudo attribute or invalid attribute. */ static int subject_index(subject_type_t type, unsigned int *index) { if (type <= ALL_SUBJ || type == PATTERN || type > SUBJ_END) return 1; *index = type - SUBJ_START; return 0; } /* * object_index - convert a real object attribute to a counter index. * @type: object attribute. * @index: destination for the array index. * Return codes: * 0 - index populated. * 1 - pseudo attribute or invalid attribute. */ static int object_index(object_type_t type, unsigned int *index) { if (type <= ALL_OBJ || type > OBJ_END) return 1; *index = type - OBJ_START; return 0; } /* * counter_increment - add one to a relaxed metric counter. * @counter: counter to update. * Returns nothing. */ static void counter_increment(atomic_ullong *counter) { atomic_fetch_add_explicit(counter, 1, memory_order_relaxed); } /* * counter_snapshot - read one metric counter and optionally clear it. * @counter: counter to read. * @reset: non-zero clears the counter after copying. * Returns the copied counter value. */ static unsigned long long counter_snapshot(atomic_ullong *counter, int reset) { if (reset) return atomic_exchange_explicit(counter, 0, memory_order_relaxed); return atomic_load_explicit(counter, memory_order_relaxed); } /* * write_attr_lookup_metric - write one aligned attribute lookup row. * @f: output stream. * @row: report row metadata and metric snapshot. * @request_width: field width for request counts. * Returns nothing. */ static void write_attr_lookup_metric(FILE *f, const struct attr_lookup_report_row *row, int request_width) { fprintf(f, "%-*s %-*s requests=%-*llu lookups=%llu\n", ATTR_LOOKUP_LABEL_WIDTH, row->label, ATTR_LOOKUP_NAME_WIDTH, row->name ? row->name : "unknown", request_width, row->snapshot->requests, row->snapshot->lookups); } /* * attr_lookup_metrics_count_subject_request - count a subject attr request. * @type: requested subject attribute. * Returns nothing. */ void attr_lookup_metrics_count_subject_request(subject_type_t type) { unsigned int index, worker = attr_lookup_worker; if (worker >= ATTR_LOOKUP_METRICS_MAX_WORKERS) return; if (subject_index(type, &index)) return; counter_increment(&workers[worker].subjects[index].requests); } /* * attr_lookup_metrics_count_subject_lookup - count a subject attr lookup. * @type: requested subject attribute missing from the event cache. * Returns nothing. */ void attr_lookup_metrics_count_subject_lookup(subject_type_t type) { unsigned int index, worker = attr_lookup_worker; if (worker >= ATTR_LOOKUP_METRICS_MAX_WORKERS) return; if (subject_index(type, &index)) return; counter_increment(&workers[worker].subjects[index].lookups); } /* * attr_lookup_metrics_count_object_request - count an object attr request. * @type: requested object attribute. * Returns nothing. */ void attr_lookup_metrics_count_object_request(object_type_t type) { unsigned int index, worker = attr_lookup_worker; if (worker >= ATTR_LOOKUP_METRICS_MAX_WORKERS) return; if (object_index(type, &index)) return; counter_increment(&workers[worker].objects[index].requests); } /* * attr_lookup_metrics_count_object_lookup - count an object attr lookup. * @type: requested object attribute missing from the event cache. * Returns nothing. */ void attr_lookup_metrics_count_object_lookup(object_type_t type) { unsigned int index, worker = attr_lookup_worker; if (worker >= ATTR_LOOKUP_METRICS_MAX_WORKERS) return; if (object_index(type, &index)) return; counter_increment(&workers[worker].objects[index].lookups); } /* * attr_lookup_metrics_subject_snapshot - copy one subject counter. * @type: subject attribute to snapshot. * @snapshot: destination for aggregated counters. * @reset: non-zero resets counters after copying. * Return codes: * 0 - snapshot populated. * 1 - invalid argument or pseudo attribute. */ int attr_lookup_metrics_subject_snapshot(subject_type_t type, struct attr_lookup_metric_snapshot *snapshot, int reset) { unsigned int index, worker; if (snapshot == NULL || subject_index(type, &index)) return 1; snapshot->requests = 0; snapshot->lookups = 0; for (worker = 0; worker < ATTR_LOOKUP_METRICS_MAX_WORKERS; worker++) { snapshot->requests += counter_snapshot( &workers[worker].subjects[index].requests, reset); snapshot->lookups += counter_snapshot( &workers[worker].subjects[index].lookups, reset); } return 0; } /* * attr_lookup_metrics_object_snapshot - copy one object counter. * @type: object attribute to snapshot. * @snapshot: destination for aggregated counters. * @reset: non-zero resets counters after copying. * Return codes: * 0 - snapshot populated. * 1 - invalid argument or pseudo attribute. */ int attr_lookup_metrics_object_snapshot(object_type_t type, struct attr_lookup_metric_snapshot *snapshot, int reset) { unsigned int index, worker; if (snapshot == NULL || object_index(type, &index)) return 1; snapshot->requests = 0; snapshot->lookups = 0; for (worker = 0; worker < ATTR_LOOKUP_METRICS_MAX_WORKERS; worker++) { snapshot->requests += counter_snapshot( &workers[worker].objects[index].requests, reset); snapshot->lookups += counter_snapshot( &workers[worker].objects[index].lookups, reset); } return 0; } /* * snapshot_subject_attrs - copy all subject attribute counters. * @snapshots: destination array indexed by subject attribute offset. * @reset: non-zero resets counters after copying. * Returns nothing. */ static void snapshot_subject_attrs( struct attr_lookup_metric_snapshot snapshots[SUBJ_COUNT], int reset) { unsigned int type; for (type = SUBJ_START; type <= SUBJ_END; type++) attr_lookup_metrics_subject_snapshot(type, &snapshots[type - SUBJ_START], reset); } /* * snapshot_object_attrs - copy all object attribute counters. * @snapshots: destination array indexed by object attribute offset. * @reset: non-zero resets counters after copying. * Returns nothing. */ static void snapshot_object_attrs( struct attr_lookup_metric_snapshot snapshots[OBJ_COUNT], int reset) { unsigned int type; for (type = OBJ_START; type <= OBJ_END; type++) attr_lookup_metrics_object_snapshot(type, &snapshots[type - OBJ_START], reset); } /* * request_width_check_snapshot - widen request column if needed. * @snapshot: metric snapshot to inspect. * @width: current request column width. * Returns the request column width to use. */ static int request_width_check_snapshot( const struct attr_lookup_metric_snapshot *snapshot, int width) { if (width == ATTR_LOOKUP_REQUEST_WIDE_WIDTH) return width; if (snapshot->requests > ATTR_LOOKUP_REQUEST_NARROW_MAX) return ATTR_LOOKUP_REQUEST_WIDE_WIDTH; return width; } /* * request_width_for_report - choose request field width for this report. * @subjects: subject attribute snapshots. * @objects: object attribute snapshots. * * Width 10 keeps normal counters compact and covers UINT32_MAX. Once a wider * request counter is seen, future reports stay wide until a reset report. * * Returns the request column width to use. */ static int request_width_for_report( const struct attr_lookup_metric_snapshot subjects[SUBJ_COUNT], const struct attr_lookup_metric_snapshot objects[OBJ_COUNT]) { unsigned int type; int width; width = atomic_load_explicit(&attr_lookup_request_width, memory_order_relaxed); if (width == ATTR_LOOKUP_REQUEST_WIDE_WIDTH) return width; for (type = SUBJ_START; type <= SUBJ_END; type++) { if (type == ALL_SUBJ || type == PATTERN) continue; width = request_width_check_snapshot( &subjects[type - SUBJ_START], width); if (width == ATTR_LOOKUP_REQUEST_WIDE_WIDTH) goto out; } for (type = OBJ_START; type <= OBJ_END; type++) { if (type == ALL_OBJ) continue; width = request_width_check_snapshot( &objects[type - OBJ_START], width); if (width == ATTR_LOOKUP_REQUEST_WIDE_WIDTH) goto out; } out: if (width == ATTR_LOOKUP_REQUEST_WIDE_WIDTH) atomic_store_explicit(&attr_lookup_request_width, width, memory_order_relaxed); return width; } /* * report_subject_attrs - write all subject attribute lookup counters. * @f: output stream. * @snapshots: subject attribute counter snapshots. * @request_width: field width for request counts. * Returns nothing. */ static void report_subject_attrs(FILE *f, const struct attr_lookup_metric_snapshot snapshots[SUBJ_COUNT], int request_width) { unsigned int type; fprintf(f, "Subject attribute lookups:\n"); for (type = SUBJ_START; type <= SUBJ_END; type++) { const char *name = subj_val_to_name(type, RULE_FMT_COLON); const struct attr_lookup_metric_snapshot *snapshot = &snapshots[type - SUBJ_START]; struct attr_lookup_report_row row = { .label = "Subject attr:", .name = name, .snapshot = snapshot, }; if (type == ALL_SUBJ || type == PATTERN) continue; write_attr_lookup_metric(f, &row, request_width); } } /* * report_object_attrs - write all object attribute lookup counters. * @f: output stream. * @snapshots: object attribute counter snapshots. * @request_width: field width for request counts. * Returns nothing. */ static void report_object_attrs(FILE *f, const struct attr_lookup_metric_snapshot snapshots[OBJ_COUNT], int request_width) { unsigned int type; fprintf(f, "\nObject attribute lookups:\n"); for (type = OBJ_START; type <= OBJ_END; type++) { const char *name = obj_val_to_name(type); const struct attr_lookup_metric_snapshot *snapshot = &snapshots[type - OBJ_START]; struct attr_lookup_report_row row = { .label = "Object attr:", .name = name, .snapshot = snapshot, }; if (type == ALL_OBJ) continue; write_attr_lookup_metric(f, &row, request_width); } } /* * attr_lookup_metrics_report - write subject/object attribute counters. * @f: output stream. * @reset: non-zero resets counters after copying. * Returns nothing. */ void attr_lookup_metrics_report(FILE *f, int reset) { struct attr_lookup_metric_snapshot subjects[SUBJ_COUNT] = { 0 }; struct attr_lookup_metric_snapshot objects[OBJ_COUNT] = { 0 }; int request_width; if (f == NULL) return; snapshot_subject_attrs(subjects, reset); snapshot_object_attrs(objects, reset); request_width = request_width_for_report(subjects, objects); report_subject_attrs(f, subjects, request_width); report_object_attrs(f, objects, request_width); if (reset) atomic_store_explicit(&attr_lookup_request_width, ATTR_LOOKUP_REQUEST_NARROW_WIDTH, memory_order_relaxed); } fapolicyd-2.0/src/library/attr-lookup-metrics.h000066400000000000000000000023251524071644000216630ustar00rootroot00000000000000/* * attr-lookup-metrics.h - subject/object attribute lookup counters * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef ATTR_LOOKUP_METRICS_HEADER #define ATTR_LOOKUP_METRICS_HEADER #include #include "object-attr.h" #include "subject-attr.h" struct attr_lookup_metric_snapshot { unsigned long long requests; unsigned long long lookups; }; void attr_lookup_metrics_set_worker(unsigned int worker_id); void attr_lookup_metrics_count_subject_request(subject_type_t type); void attr_lookup_metrics_count_subject_lookup(subject_type_t type); void attr_lookup_metrics_count_object_request(object_type_t type); void attr_lookup_metrics_count_object_lookup(object_type_t type); int attr_lookup_metrics_subject_snapshot(subject_type_t type, struct attr_lookup_metric_snapshot *snapshot, int reset); int attr_lookup_metrics_object_snapshot(object_type_t type, struct attr_lookup_metric_snapshot *snapshot, int reset); void attr_lookup_metrics_report(FILE *f, int reset); #endif fapolicyd-2.0/src/library/attr-sets.c000066400000000000000000000304051524071644000176570ustar00rootroot00000000000000/* * attr-sets.c - Attribute sets dynamic data structure * * Copyright (c) 2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka */ /* * Overview * -------- * * Attribute sets are AVL-backed collections used by rule parsing and process * status caching. Each attr_sets_entry_t is individually allocated and all * callers keep direct entry pointers. A registry, attr_sets_t, owns a growing * array of those pointers so named policy sets can be found while parsing. * * Registry growth only reallocates the pointer array. It never moves the set * entries themselves, so rule attributes can store attr_sets_entry_t pointers * immediately without a second index-to-pointer regeneration pass after rule * parsing. * * Named policy sets and temporary UID/GID sets use the same attr_set_create() * and attr_set_destroy() API. The difference is only ownership: registry sets * are released by attr_sets_destroy(), while standalone process-cache sets are * released directly by attr_set_destroy(). */ #include #include #include #include "attr-sets.h" #include "message.h" #define RESIZE_BY 2 #define DEFAULT_CAPACITY 100 struct attr_sets { size_t capacity; size_t size; size_t resize_factor; attr_sets_entry_t **array; }; /* * strcmp_cb - Compare two string AVL nodes. * @a: first avl_str_data_t node * @b: second avl_str_data_t node * * Returns: 0 when equal, negative when @a sorts before @b, and positive when * @a sorts after @b. */ static int strcmp_cb(void *a, void *b) { return strcmp(((avl_str_data_t *)a)->str, ((avl_str_data_t *)b)->str); } /* * intcmp_cb - Compare two signed integer AVL nodes. * @a: first avl_int_data_t node * @b: second avl_int_data_t node * * Returns: 0 when equal, -1 when @a is less than @b, and 1 when @a is greater * than @b. */ static int intcmp_cb(void *a, void *b) { int64_t va = ((avl_int_data_t *)a)->num; int64_t vb = ((avl_int_data_t *)b)->num; if (va > vb) return 1; if (va < vb) return -1; return 0; } /* * unsigned_cmp_cb - Compare two unsigned integer AVL nodes. * @a: first avl_int_data_t node * @b: second avl_int_data_t node * * Returns: 0 when equal, -1 when @a is less than @b, and 1 when @a is greater * than @b. */ static int unsigned_cmp_cb(void *a, void *b) { uint64_t va = (uint64_t)((avl_int_data_t *)a)->num; uint64_t vb = (uint64_t)((avl_int_data_t *)b)->num; if (va > vb) return 1; if (va < vb) return -1; return 0; } /* * strncmp_cb - Check if a set string is a prefix of a lookup string. * @a: avl_str_data_t node from a set * @b: avl_str_data_t lookup value * * Returns: -1 when the prefix matches so traversal stops, or 0 when it does * not match. */ static int strncmp_cb(void *a, void *b) { return strncmp(((avl_str_data_t *)a)->str, ((avl_str_data_t *)b)->str, ((avl_str_data_t *)a)->len) ? 0 : -1; } /* * attr_set_init_tree - Initialize the AVL tree for one set type. * @set: set entry to initialize * @type: STRING, SIGNED, or UNSIGNED * * Returns: 0 on success and 1 when @type is not supported. */ static int attr_set_init_tree(attr_sets_entry_t *set, int type) { if (type == STRING) avl_init(&set->tree, strcmp_cb); else if (type == SIGNED) avl_init(&set->tree, intcmp_cb); else if (type == UNSIGNED) avl_init(&set->tree, unsigned_cmp_cb); else return 1; return 0; } /* * attr_sets_resize - Grow a registry pointer array. * @sets: registry to grow * * Returns: 0 on success and 1 on allocation failure. */ static int attr_sets_resize(attr_sets_t *sets) { size_t new_capacity = sets->capacity * sets->resize_factor; attr_sets_entry_t **tmp = realloc(sets->array, sizeof(attr_sets_entry_t *) * new_capacity); if (!tmp) return 1; sets->capacity = new_capacity; sets->array = tmp; return 0; } /* * attr_sets_create - Allocate an empty attribute set registry. * * Returns: pointer to a new registry or NULL on allocation failure. */ attr_sets_t *attr_sets_create(void) { attr_sets_t *sets = malloc(sizeof(attr_sets_t)); if (!sets) return NULL; sets->resize_factor = RESIZE_BY; sets->size = 0; sets->capacity = DEFAULT_CAPACITY; sets->array = calloc(sets->capacity, sizeof(attr_sets_entry_t *)); if (!sets->array) { free(sets); return NULL; } return sets; } /* * attr_sets_add - Add an existing set entry to a registry. * @sets: registry that takes ownership of @set * @set: set entry to append * * Returns: 0 on success and 1 on invalid input or allocation failure. */ int attr_sets_add(attr_sets_t *sets, attr_sets_entry_t *set) { if (!sets || !set) return 1; if (sets->size == sets->capacity) if (attr_sets_resize(sets)) return 1; sets->array[sets->size] = set; sets->size++; return 0; } /* * attr_sets_find - Find a named set in a registry. * @sets: registry to search * @name: set name without the leading percent character * * Returns: matching set entry, or NULL when no matching name exists. */ attr_sets_entry_t *attr_sets_find(const attr_sets_t *sets, const char *name) { if (!sets || !name) return NULL; for (size_t i = 0 ; i < sets->size ; i++) { attr_sets_entry_t *set = sets->array[i]; if (set && set->name && strcmp(set->name, name) == 0) return set; } return NULL; } /* * attr_sets_destroy - Free a registry and every set it owns. * @sets: registry to destroy */ void attr_sets_destroy(attr_sets_t *sets) { if (!sets) return; for (size_t i = 0 ; i < sets->size ; i++) attr_set_destroy(sets->array[i]); free(sets->array); free(sets); } /* * attr_set_create - Allocate one attribute set entry. * @name: optional set name, copied when provided * @type: STRING, SIGNED, or UNSIGNED * * Returns: pointer to a new set entry or NULL on allocation failure or invalid * @type. */ attr_sets_entry_t *attr_set_create(const char *name, const int type) { attr_sets_entry_t *set = malloc(sizeof(attr_sets_entry_t)); if (!set) return NULL; memset(set, 0, sizeof(attr_sets_entry_t)); set->type = type; if (attr_set_init_tree(set, type)) { free(set); return NULL; } if (name) { set->name = strdup(name); if (!set->name) { free(set); return NULL; } } return set; } /* * attr_set_append_int - Add an integer value to a set. * @set: target SIGNED or UNSIGNED set * @num: value to insert * * Returns: 0 on success and 1 on invalid input, duplicate value, or allocation * failure. */ int attr_set_append_int(attr_sets_entry_t *set, const int64_t num) { avl_int_data_t *data; avl_t *ret; if (!set) return 1; if (set->type != SIGNED && set->type != UNSIGNED) return 1; if (set->type == UNSIGNED && num < 0) return 1; data = malloc(sizeof(avl_int_data_t)); if (!data) return 1; data->num = num; ret = avl_insert(&set->tree, (avl_t *)data); if (ret != (avl_t *)data) { free(data); return 1; } return 0; } /* * attr_set_append_str - Add a string value to a set. * @set: target STRING set * @str: value to copy and insert * * Returns: 0 on success and 1 on invalid input, duplicate value, or allocation * failure. */ int attr_set_append_str(attr_sets_entry_t *set, const char *str) { avl_str_data_t *data; avl_t *ret; if (!set || !str) return 1; if (set->type != STRING) return 1; data = malloc(sizeof(avl_str_data_t)); if (!data) return 1; data->str = strdup(str); if (!data->str) { free(data); return 1; } data->len = strlen(str); ret = avl_insert(&set->tree, (avl_t *)data); if (ret != (avl_t *)data) { free((void *)data->str); free(data); return 1; } /* Metacharacters are literal unless the rule explicitly opts into globbing. */ if (strncmp(str, ATTR_SET_GLOB_PREFIX, ATTR_SET_GLOB_PREFIX_LEN) == 0) set->has_glob = true; return 0; } /* * attr_set_empty - Determine if a set has no members. * @set: set to check * * Returns: true when @set is NULL or has no entries, false otherwise. */ bool attr_set_empty(attr_sets_entry_t *set) { if (!set) return true; return set->tree.root == NULL; } /* * attr_set_check_int - Check if an integer set contains a value. * @set: SIGNED or UNSIGNED set to search * @num: value to search for * * Returns: 1 when found and 0 when not found or input is invalid. */ int attr_set_check_int(attr_sets_entry_t *set, const int64_t num) { avl_int_data_t data; if (!set || (set->type != SIGNED && set->type != UNSIGNED)) return 0; if (set->type == UNSIGNED && num < 0) return 0; data.num = num; return avl_search(&set->tree, (avl_t *)(&data)) ? 1 : 0; } /* * attr_set_check_str - Check if a string set contains a value. * @set: STRING set to search * @str: value to search for * * Returns: 1 when found and 0 when not found or input is invalid. */ int attr_set_check_str(attr_sets_entry_t *set, const char *str) { avl_str_data_t data; if (!set || set->type != STRING || !str) return 0; data.str = str; return avl_search(&set->tree, (avl_t *)(&data)) ? 1 : 0; } /* * attr_set_check_pstr - Check if any set entry prefixes a string. * @set: STRING set containing possible prefixes * @str: value to test against the set * * Returns: 1 when a prefix matches and 0 when no prefix matches or input is * invalid. */ int attr_set_check_pstr(attr_sets_entry_t *set, const char *str) { avl_str_data_t data; int ret; if (!set || set->type != STRING || !str) return 0; data.str = str; ret = avl_traverse(&set->tree, strncmp_cb, (void *)&data); if (ret == -1) return 1; return 0; } /* * print_str - Print one string set entry. * @entry: avl_str_data_t entry to print * @data: unused traversal callback data * * Returns: 0 to continue traversal. */ static int print_str(void *entry, void *data) { (void)data; const char *str = ((avl_str_data_t *)entry)->str; msg(LOG_DEBUG, "%s", str); return 0; } /* * print_signed - Print one signed integer set entry. * @entry: avl_int_data_t entry to print * @data: unused traversal callback data * * Returns: 0 to continue traversal. */ static int print_signed(void *entry, void *data) { (void)data; int64_t num = ((avl_int_data_t *)entry)->num; msg(LOG_DEBUG, "%lld", (long long)num); return 0; } /* * print_unsigned - Print one unsigned integer set entry. * @entry: avl_int_data_t entry to print * @data: unused traversal callback data * * Returns: 0 to continue traversal. */ static int print_unsigned(void *entry, void *data) { (void)data; uint64_t num = (uint64_t)((avl_int_data_t *)entry)->num; msg(LOG_DEBUG, "%llu", (unsigned long long)num); return 0; } /* * attr_set_print - Print one set for debugging. * @set: set to print */ void attr_set_print(attr_sets_entry_t *set) { if (!set) return; msg(LOG_DEBUG, "Set: %s", set->name ? set->name : "(anonymous)"); if (set->type == STRING) avl_traverse(&set->tree, print_str, NULL); else if (set->type == SIGNED) avl_traverse(&set->tree, print_signed, NULL); else if (set->type == UNSIGNED) avl_traverse(&set->tree, print_unsigned, NULL); msg(LOG_DEBUG, "--------------"); } /* * attr_sets_print - Print all registry-owned sets for debugging. * @sets: registry to print */ void attr_sets_print(const attr_sets_t *sets) { if (!sets) return; for (size_t i = 0 ; i < sets->size ; i++) attr_set_print(sets->array[i]); } /* * attr_set_destroy - Free one set entry and all member values. * @set: set entry to destroy */ void attr_set_destroy(attr_sets_entry_t *set) { avl_t *cur; if (!set) return; free(set->name); while ((cur = set->tree.root) != NULL) { void *tmp = (void *)avl_remove(&set->tree, cur); if ((avl_t *)tmp != cur) msg(LOG_DEBUG, "attr_set_entry: removal of invalid node"); if (set->type == STRING) free((void *)((avl_str_data_t *)tmp)->str); free(tmp); } free(set); } fapolicyd-2.0/src/library/attr-sets.h000066400000000000000000000046571524071644000176760ustar00rootroot00000000000000/* * attr-sets.h - Header file for attribute sets * Copyright (c) 2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka */ #ifndef ATTR_SETS_H #define ATTR_SETS_H #include "stddef.h" #include #include #include "avl.h" #define ATTR_SET_GLOB_PREFIX "glob:" #define ATTR_SET_GLOB_PREFIX_LEN (sizeof(ATTR_SET_GLOB_PREFIX) - 1) typedef struct _avl_str_data { avl_t avl; size_t len; const char * str; } avl_str_data_t; typedef struct _avl_int_data { avl_t avl; int64_t num; } avl_int_data_t; typedef struct attr_sets_entry { // optional char * name; // STRING, SIGNED, or UNSIGNED from DATA_TYPES int type; // Lookup hint only; rule attributes validate whether globs are allowed. bool has_glob; avl_tree_t tree; } attr_sets_entry_t; typedef struct attr_sets attr_sets_t; typedef enum _types { STRING = 1, SIGNED, UNSIGNED, } DATA_TYPES; attr_sets_t *attr_sets_create(void); void attr_sets_destroy(attr_sets_t *sets); int attr_sets_add(attr_sets_t *sets, attr_sets_entry_t *set); attr_sets_entry_t *attr_sets_find(const attr_sets_t *sets, const char *name); attr_sets_entry_t *attr_set_create(const char *name, const int type); void attr_set_destroy(attr_sets_entry_t *set); int attr_set_append_int(attr_sets_entry_t *set, const int64_t num); int attr_set_append_str(attr_sets_entry_t *set, const char *str); int attr_set_check_int(attr_sets_entry_t *set, const int64_t num); int attr_set_check_str(attr_sets_entry_t *set, const char *str); int attr_set_check_pstr(attr_sets_entry_t *set, const char *str); bool attr_set_empty(attr_sets_entry_t *set); void attr_sets_print(const attr_sets_t *sets); void attr_set_print(attr_sets_entry_t *set); #endif // ATTR_SETS_H fapolicyd-2.0/src/library/avl.c000066400000000000000000000311261524071644000165140ustar00rootroot00000000000000/* * avl.c - AVL tree helpers * Copyright (c) 2019-22,2026 Red Hat Inc. * * This file contains code adapted from Netdata's AVL implementation and * libavl. Those AVL routines are licensed under the GNU Lesser General * Public License (LGPL). See the provenance note below. */ //#include "config.h" #include // for NULL #include "avl.h" // Note: this file is based on this: // https://github.com/firehol/netdata/blob/master/src/avl.c // c63bdb5 on Oct 23, 2017 // // which has been moved to here (05/23/20): // https://github.com/netdata/netdata/blob/master/libnetdata/avl/avl.c // // However, its been modified to remove pthreads as this application will // only use it from a single thread. /* ------------------------------------------------------------------------- */ /* * avl_insert(), avl_remove() and avl_search() * are adaptations (by Costa Tsaousis) of the AVL algorithm found in libavl * v2.0.3, so that they do not use any memory allocations and their memory * footprint is optimized (by eliminating non-necessary data members). * * libavl - library for manipulation of binary trees. * Copyright (C) 1998, 1999, 2000, 2001, 2002, 2004 Free Software * Foundation, Inc. * GNU Lesser General Public License */ /* Search |tree| for an item matching |item|, and return it if found. Otherwise return |NULL|. */ avl_t *avl_search(const avl_tree_t *tree, avl_t *item) { avl_t *p; // assert (tree != NULL && item != NULL); for (p = tree->root; p != NULL; ) { int cmp = tree->compar(item, p); if (cmp < 0) p = p->avl_link[0]; else if (cmp > 0) p = p->avl_link[1]; else /* |cmp == 0| */ return p; } return NULL; } /* Inserts |item| into |tree| and returns a pointer to |item|'s address. If a duplicate item is found in the tree, returns a pointer to the duplicate without inserting |item|. */ avl_t *avl_insert(avl_tree_t *tree, avl_t *item) { avl_t *y, *z; /* Top node to update balance factor, and parent. */ avl_t *p, *q; /* Iterator, and parent. */ avl_t *n; /* Newly inserted node. */ avl_t *w; /* New root of rebalanced subtree. */ unsigned char dir; /* Direction to descend. */ unsigned char da[AVL_MAX_HEIGHT]; /* Cached comparison results. */ int k = 0; /* Number of cached results. */ // assert(tree != NULL && item != NULL); z = (avl_t *) &tree->root; y = tree->root; dir = 0; for (q = z, p = y; p != NULL; q = p, p = p->avl_link[dir]) { int cmp = tree->compar(item, p); if (cmp == 0) return p; if (p->avl_balance != 0) z = q, y = p, k = 0; da[k++] = dir = (cmp > 0); } n = q->avl_link[dir] = item; // tree->avl_count++; n->avl_link[0] = n->avl_link[1] = NULL; n->avl_balance = 0; if (y == NULL) return n; for (p = y, k = 0; p != n; p = p->avl_link[da[k]], k++) if (da[k] == 0) p->avl_balance--; else p->avl_balance++; if (y->avl_balance == -2) { avl_t *x = y->avl_link[0]; if (x->avl_balance == -1) { w = x; y->avl_link[0] = x->avl_link[1]; x->avl_link[1] = y; x->avl_balance = y->avl_balance = 0; } else { // assert (x->avl_balance == +1); w = x->avl_link[1]; x->avl_link[1] = w->avl_link[0]; w->avl_link[0] = x; y->avl_link[0] = w->avl_link[1]; w->avl_link[1] = y; if (w->avl_balance == -1) x->avl_balance = 0, y->avl_balance = +1; else if (w->avl_balance == 0) x->avl_balance = y->avl_balance = 0; else /* |w->avl_balance == +1| */ x->avl_balance = -1, y->avl_balance = 0; w->avl_balance = 0; } } else if (y->avl_balance == +2) { avl_t *x = y->avl_link[1]; if (x->avl_balance == +1) { w = x; y->avl_link[1] = x->avl_link[0]; x->avl_link[0] = y; x->avl_balance = y->avl_balance = 0; } else { // assert (x->avl_balance == -1); w = x->avl_link[0]; x->avl_link[0] = w->avl_link[1]; w->avl_link[1] = x; y->avl_link[1] = w->avl_link[0]; w->avl_link[0] = y; if (w->avl_balance == +1) x->avl_balance = 0, y->avl_balance = -1; else if (w->avl_balance == 0) x->avl_balance = y->avl_balance = 0; else /* |w->avl_balance == -1| */ x->avl_balance = +1, y->avl_balance = 0; w->avl_balance = 0; } } else return n; z->avl_link[y != z->avl_link[0]] = w; // tree->avl_generation++; return n; } /* Deletes from |tree| and returns an item matching |item|. Returns a null pointer if no matching item found. */ avl_t *avl_remove(avl_tree_t *tree, avl_t *item) { /* Stack of nodes. */ avl_t *pa[AVL_MAX_HEIGHT]; /* Nodes. */ unsigned char da[AVL_MAX_HEIGHT]; /* |avl_link[]| indexes. */ int k; /* Stack pointer. */ avl_t *p; /* Traverses tree to find node to delete. */ int cmp; /* Result of comparison between |item| and |p|. */ // assert (tree != NULL && item != NULL); k = 0; p = (avl_t *) &tree->root; for(cmp = -1; cmp != 0; cmp = tree->compar(item, p)) { unsigned char dir = (unsigned char)(cmp > 0); pa[k] = p; da[k++] = dir; p = p->avl_link[dir]; if(p == NULL) return NULL; } item = p; if (p->avl_link[1] == NULL) pa[k - 1]->avl_link[da[k - 1]] = p->avl_link[0]; else { avl_t *r = p->avl_link[1]; if (r->avl_link[0] == NULL) { r->avl_link[0] = p->avl_link[0]; r->avl_balance = p->avl_balance; pa[k - 1]->avl_link[da[k - 1]] = r; da[k] = 1; pa[k++] = r; } else { avl_t *s; int j = k++; for (;;) { da[k] = 0; pa[k++] = r; s = r->avl_link[0]; if (s->avl_link[0] == NULL) break; r = s; } s->avl_link[0] = p->avl_link[0]; r->avl_link[0] = s->avl_link[1]; s->avl_link[1] = p->avl_link[1]; s->avl_balance = p->avl_balance; pa[j - 1]->avl_link[da[j - 1]] = s; da[j] = 1; pa[j] = s; } } // assert (k > 0); while (--k > 0) { avl_t *y = pa[k]; if (da[k] == 0) { y->avl_balance++; if (y->avl_balance == +1) break; else if (y->avl_balance == +2) { avl_t *x = y->avl_link[1]; if (x->avl_balance == -1) { avl_t *w; // assert (x->avl_balance == -1); w = x->avl_link[0]; x->avl_link[0] = w->avl_link[1]; w->avl_link[1] = x; y->avl_link[1] = w->avl_link[0]; w->avl_link[0] = y; if (w->avl_balance == +1) x->avl_balance = 0, y->avl_balance = -1; else if (w->avl_balance == 0) x->avl_balance = y->avl_balance = 0; else /* |w->avl_balance == -1| */ x->avl_balance = +1, y->avl_balance = 0; w->avl_balance = 0; pa[k - 1]->avl_link[da[k - 1]] = w; } else { y->avl_link[1] = x->avl_link[0]; x->avl_link[0] = y; pa[k - 1]->avl_link[da[k - 1]] = x; if (x->avl_balance == 0) { x->avl_balance = -1; y->avl_balance = +1; break; } else x->avl_balance = y->avl_balance = 0; } } } else { y->avl_balance--; if (y->avl_balance == -1) break; else if (y->avl_balance == -2) { avl_t *x = y->avl_link[0]; if (x->avl_balance == +1) { avl_t *w; // assert (x->avl_balance == +1); w = x->avl_link[1]; x->avl_link[1] = w->avl_link[0]; w->avl_link[0] = x; y->avl_link[0] = w->avl_link[1]; w->avl_link[1] = y; if (w->avl_balance == -1) x->avl_balance = 0, y->avl_balance = +1; else if (w->avl_balance == 0) x->avl_balance = y->avl_balance = 0; else /* |w->avl_balance == +1| */ x->avl_balance = -1, y->avl_balance = 0; w->avl_balance = 0; pa[k - 1]->avl_link[da[k - 1]] = w; } else { y->avl_link[0] = x->avl_link[1]; x->avl_link[1] = y; pa[k - 1]->avl_link[da[k - 1]] = x; if (x->avl_balance == 0) { x->avl_balance = +1; y->avl_balance = -1; break; } else x->avl_balance = y->avl_balance = 0; } } } } // tree->avl_count--; // tree->avl_generation++; return item; } /* ------------------------------------------------------------------------- */ // below are functions by (C) Costa Tsaousis // --------------------------- // traversing static int avl_walker(avl_t *node, int (*callback)(void *entry, void *data), void *data) { int total = 0, ret = 0; if(node->avl_link[0]) { ret = avl_walker(node->avl_link[0], callback, data); if(ret < 0) return ret; total += ret; } ret = callback(node, data); if(ret < 0) return ret; total += ret; if(node->avl_link[1]) { ret = avl_walker(node->avl_link[1], callback, data); if (ret < 0) return ret; total += ret; } return total; } int avl_traverse(const avl_tree_t *t, int (*callback)(void *entry, void *data), void *data) { if(t->root) return avl_walker(t->root, callback, data); else return 0; } void avl_init(avl_tree_t *t, int (*compar)(void *a, void *b)) { t->root = NULL; t->compar = compar; } /* ------------------------------------------------------------------------- */ // below are functions by (C) Steve Grubb // --------------------------- avl_t *avl_first(avl_iterator *i, avl_tree_t *t) { if (t->root == NULL || i == NULL) return NULL; i->tree = t; i->height = 0; // follow the leftmost node to its bottom avl_t *node = t->root; while (node->avl_link[0]) { i->stack[i->height] = node; i->height++; node = node->avl_link[0]; } i->current = node; return node; } avl_t *avl_next(avl_iterator *i) { if (i == NULL || i->tree == NULL) return NULL; avl_t *node = i->current; if (node == NULL) return avl_first(i, i->tree); else if (node->avl_link[1]) { i->stack[i->height] = node; i->height++; node = node->avl_link[1]; while (node->avl_link[0]) { i->stack[i->height] = node; i->height++; node = node->avl_link[0]; } } else { avl_t *tmp; do { if (i->height == 0) { i->current = NULL; return NULL; } tmp = node; i->height--; node = i->stack[i->height]; } while (tmp == node->avl_link[1]); } i->current = node; return node; } static int avl_walker2(avl_t *node, avl_tree_t *haystack) { int ret; // If the lefthand has a link, take it so that we walk to the // leftmost bottom if(node->avl_link[0]) { ret = avl_walker2(node->avl_link[0], haystack); if (ret) return ret; } // Next, check the current node avl_t *res = avl_search(haystack, node); if (res) return 1; // If the righthand has a link, take it so that we check all the // rightmost nodes, too. if(node->avl_link[1]) { ret = avl_walker2(node->avl_link[1], haystack); if (ret) return ret; } // nothing found return 0; } int avl_intersection(const avl_tree_t *needle, avl_tree_t *haystack) { // traverse the needle and search the haystack // this implies that needle should be smaller than haystack if (needle && haystack && needle->root && haystack->root) return avl_walker2(needle->root, haystack); // something is not initialized, so we cannot search return 0; } fapolicyd-2.0/src/library/avl.h000066400000000000000000000043411524071644000165200ustar00rootroot00000000000000/* * avl.h - Header file for avl.c * Copyright (c) 2019-20,2022,2026 Red Hat Inc. * * This file contains declarations for code adapted from Netdata's AVL * implementation and libavl. Those AVL routines are licensed under the * GNU Lesser General Public License (LGPL). */ #ifndef AVL_HEADER #define AVL_HEADER #include "gcc-attributes.h" /* Maximum AVL tree height. */ #ifndef AVL_MAX_HEIGHT #define AVL_MAX_HEIGHT 92 #endif /* Data structures */ /* One element of the AVL tree */ typedef struct avl { struct avl *avl_link[2]; /* Subtrees - 0 left, 1 right */ signed char avl_balance; /* Balance factor. */ } avl_t; /* An AVL tree */ typedef struct avl_tree { avl_t *root; int (*compar)(void *a, void *b); } avl_tree_t; /* Iterator state struct */ typedef struct avl_iterator { avl_tree_t *tree; avl_t *current; avl_t *stack[AVL_MAX_HEIGHT]; unsigned height; } avl_iterator; /* Public methods */ /* Insert element a into the AVL tree t * returns the added element a, or a pointer the * element that is equal to a (as returned by t->compar()) * a is linked directly to the tree, so it has to * be properly allocated by the caller. */ avl_t *avl_insert(avl_tree_t *t, avl_t *a) __returns_nonnull __wur; /* Remove an element a from the AVL tree t * returns a pointer to the removed element * or NULL if an element equal to a is not found * (equal as returned by t->compar()) */ avl_t *avl_remove(avl_tree_t *t, avl_t *a) __wur; /* Find the element into the tree that equal to a * (equal as returned by t->compar()) * returns NULL is no element is equal to a */ avl_t *avl_search(const avl_tree_t *t, avl_t *a); /* Initialize the avl_tree_t */ void avl_init(avl_tree_t *t, int (*compar)(void *a, void *b)); /* Walk the tree and call callback at each node */ int avl_traverse(const avl_tree_t *t, int (*callback)(void *entry, void *data), void *data); /* Walk the tree down to the first node and return it */ avl_t *avl_first(avl_iterator *i, avl_tree_t *t); /* Walk the tree to the next logical node and return it */ avl_t *avl_next(avl_iterator *i); /* Given two trees, see if any in needle are contained in haystack */ int avl_intersection(const avl_tree_t *needle, avl_tree_t *haystack); #endif /* avl.h */ fapolicyd-2.0/src/library/backend-manager.c000066400000000000000000000114441524071644000207320ustar00rootroot00000000000000/* * backend-manager.c - backend management * Copyright (c) 2020,2022 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka */ #include "config.h" #include #include #include #include #include #include #include #include #include #include // close #include "conf.h" #include "message.h" #include "backend-manager.h" #include "fapolicyd-backend.h" extern backend file_backend; #ifdef USE_RPM extern backend rpm_backend; #endif #ifdef USE_DEB extern backend deb_backend; #endif static backend* compiled[] = { &file_backend, #ifdef USE_RPM &rpm_backend, #endif #ifdef USE_DEB &deb_backend, #endif NULL, }; static backend_entry* backends = NULL; extern atomic_bool stop; static int backend_push(const char *name) { long index = -1; for (long i = 0 ; compiled[i] != NULL; i++) { if (strcmp(name, compiled[i]->name) == 0) { index = i; break; } } if (index == -1) { msg(LOG_ERR, "%s backend not supported, aborting!", name); return 1; } else { backend_entry *tmp = (backend_entry *) malloc(sizeof(backend_entry)); if (!tmp) { msg(LOG_ERR, "cannot allocate %s backend", name); return 2; } tmp->backend = compiled[index]; tmp->next = NULL; if (!backends) backends = tmp; else { // Find the last entry backend_entry *cur = backends; while (cur->next) cur = cur->next; cur->next = tmp; } msg(LOG_DEBUG, "backend %s registered", name); } return 0; } static int backend_destroy(void) { backend_entry *be = backend_get_first(); backend_entry *tmp = NULL; while (be != NULL) { tmp = be; be = be->next; free(tmp); } backends = NULL; return 0; } static int backend_create(const char *trust_list) { char *ptr, *saved, *tmp = strdup(trust_list); if (!tmp) return 1; ptr = strtok_r(tmp, ",", &saved); while (ptr) { if (backend_push(ptr)) { free(tmp); return 1; } ptr = strtok_r(NULL, ",", &saved); } free(tmp); return 0; } /* * backend_wait_for_loader - wait for a package loader socket event. * @fd: loader socket descriptor. * @timeout_ms: maximum time to wait in milliseconds. * * Returns 0 when the socket is ready and -1 on timeout, shutdown, or error. */ int backend_wait_for_loader(int fd, int timeout_ms) { struct pollfd pfd = { .fd = fd, .events = POLLIN }; int remaining = timeout_ms; while (!stop && remaining > 0) { int interval = remaining > BACKEND_LOADER_POLL_MS ? BACKEND_LOADER_POLL_MS : remaining; int rc = poll(&pfd, 1, interval); if (rc > 0) return 0; if (rc < 0 && errno != EINTR) return -1; remaining -= interval; } errno = stop ? ECANCELED : ETIMEDOUT; return -1; } int backend_init(const conf_t *conf) { #if defined(USE_RPM) || defined(USE_DEB) struct sigaction action = { .sa_handler = SIG_IGN }; /* * A sealed memfd is a package loader's success commit. Process exit can * wedge after that commit, while the bounded socket wait handles an * earlier storage stall. Ignore SIGCHLD so neither path blocks reaping. * Do not restore waitpid() without bounded child supervision. */ sigemptyset(&action.sa_mask); if (sigaction(SIGCHLD, &action, NULL)) { msg(LOG_ERR, "Cannot configure package loader child reaping (%s)", strerror(errno)); return 1; } #endif if (backend_create(conf->trust)) return 1; for (backend_entry *be = backend_get_first(); be != NULL; be = be->next) { if (be->backend->init()) return 2; } return 0; } int backend_load(const conf_t *conf) { for (backend_entry *be = backend_get_first(); be != NULL; be = be->next) { if (be->backend->load(conf)) return 1; } return 0; } void backend_close(void) { for (backend_entry *be = backend_get_first(); be != NULL; be = be->next) { // If we have a memfd, close it if (be->backend->memfd != -1) { close(be->backend->memfd); be->backend->memfd = -1; be->backend->entries = -1; } // allow the backend to release any resources be->backend->close(); } backend_destroy(); } backend_entry* backend_get_first(void) { return backends; } fapolicyd-2.0/src/library/backend-manager.h000066400000000000000000000026001524071644000207310ustar00rootroot00000000000000/* * backend-manager.h - Header file for backend manager * Copyright (c) 2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka */ #ifndef BACKEND_MANAGER_H #define BACKEND_MANAGER_H #include #include "conf.h" #include "fapolicyd-backend.h" #define BACKEND_LOADER_TIMEOUT_MS (30 * 60 * 1000) #define BACKEND_LOADER_POLL_MS 1000 typedef struct _backend_entry { backend *backend; struct _backend_entry *next; } backend_entry; int backend_init(const conf_t *conf); int backend_load(const conf_t *conf); int backend_wait_for_loader(int fd, int timeout_ms); void backend_close(void); backend_entry* backend_get_first(void); #endif fapolicyd-2.0/src/library/conf.h000066400000000000000000000036661524071644000166740ustar00rootroot00000000000000/* conf.h configuration structure * Copyright 2018-20,22 Red Hat Inc. * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Authors: * Radovan Sroka * */ #ifndef CONF_H #define CONF_H #include #include typedef enum { IN_NONE, IN_SIZE, IN_IMA, IN_SHA256 } integrity_t; typedef enum { RESET_NEVER, RESET_AUTO, RESET_MANUAL } reset_strategy_t; typedef enum { TIMING_COLLECTION_OFF, TIMING_COLLECTION_MANUAL } timing_collection_t; typedef enum { REPORT_INTENT_STATUS, REPORT_INTENT_RESET_METRICS, REPORT_INTENT_TIMING_ARM, REPORT_INTENT_TIMING_STOP, REPORT_INTENT_METRICS } report_intent_t; typedef struct conf { unsigned int permissive; unsigned int nice_val; unsigned int q_size; unsigned int decision_threads; uid_t uid; gid_t gid; unsigned int do_stat_report; unsigned int detailed_report; unsigned int db_max_size; bool do_auto_db_sizing; unsigned int subj_cache_size; unsigned int obj_cache_size; const char *watch_fs; const char *ignore_mounts; const char *trust; integrity_t integrity; const char *syslog_format; unsigned int rpm_sha256_only; unsigned int allow_filesystem_mark; unsigned int report_interval; reset_strategy_t reset_strategy; timing_collection_t timing_collection; } conf_t; #endif fapolicyd-2.0/src/library/daemon-config.c000066400000000000000000000742731524071644000204520ustar00rootroot00000000000000/* * daemon-config.c - This is a config file parser * * Copyright 2018-22 Red Hat Inc. * All Rights Reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Authors: * Steve Grubb * Radovan Sroka * */ #include "config.h" #include "daemon-config.h" #include "message.h" #include "file.h" #include "database.h" #include "decision-defer.h" #include "decision-event.h" #include "lru.h" #include "queue.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "paths.h" /* * Leave room for daemon-owned descriptors outside queued fanotify permission * event fds: fanotify, timer/signal machinery, pid and mount files, trust * database files, syslog, and short-lived proc/config opens. The current fixed * set is smaller, so 64 is a conservative reserve for validation. */ #define DAEMON_CONFIG_FD_RESERVE 64 /* * Fixed worker allocations should stay a minority of available memory. Use a * quarter of physical memory or RLIMIT_AS as an early rejection threshold while * leaving space for variable cache payloads, LMDB maps, libraries, and the OS. */ #define DAEMON_CONFIG_MEMORY_BUDGET_PERCENT 25 /* * Extra per-worker allowance for context state that is not represented by the * cache, queue, and defer-array formulas below. This covers current small * fields plus future per-worker helper handles and metric padding. */ #define DAEMON_CONFIG_WORKER_FIXED_OVERHEAD 16384ULL /* Local prototypes */ struct nv_pair { const char *name; const char *value; }; struct kw_pair { const char *name; int (*parser)(const struct nv_pair *, int, conf_t *); }; struct nv_list { const char *name; int option; }; static char *get_line(FILE *f, char *buf, unsigned size, int *lineno, const char *file); static int nv_split(char *buf, struct nv_pair *nv); static const struct kw_pair *kw_lookup(const char *val); static int permissive_parser(const struct nv_pair *nv, int line, conf_t *config); static int nice_val_parser(const struct nv_pair *nv, int line, conf_t *config); static int q_size_parser(const struct nv_pair *nv, int line, conf_t *config); static int decision_threads_parser(const struct nv_pair *nv, int line, conf_t *config); static int uid_parser(const struct nv_pair *nv, int line, conf_t *config); static int gid_parser(const struct nv_pair *nv, int line, conf_t *config); static int detailed_report_parser(const struct nv_pair *nv, int line, conf_t *config); static int db_max_size_parser(const struct nv_pair *nv, int line, conf_t *config); static int subj_cache_size_parser(const struct nv_pair *nv, int line, conf_t *config); static int obj_cache_size_parser(const struct nv_pair *nv, int line, conf_t *config); static int do_stat_report_parser(const struct nv_pair *nv, int line, conf_t *config); static int watch_fs_parser(const struct nv_pair *nv, int line, conf_t *config); static int ignore_mounts_parser(const struct nv_pair *nv, int line, conf_t *config); static int trust_parser(const struct nv_pair *nv, int line, conf_t *config); static int integrity_parser(const struct nv_pair *nv, int line, conf_t *config); static int syslog_format_parser(const struct nv_pair *nv, int line, conf_t *config); static int rpm_sha256_only_parser(const struct nv_pair *nv, int line, conf_t *config); static int fs_mark_parser(const struct nv_pair *nv, int line, conf_t *config); static int report_interval_parser(const struct nv_pair *nv, int line, conf_t *config); static int reset_strategy_parser(const struct nv_pair *nv, int line, conf_t *config); static int timing_collection_parser(const struct nv_pair *nv, int line, conf_t *config); static const struct kw_pair keywords[] = { {"permissive", permissive_parser }, {"nice_val", nice_val_parser }, {"q_size", q_size_parser }, {"decision_threads", decision_threads_parser }, {"uid", uid_parser }, {"gid", gid_parser }, {"detailed_report", detailed_report_parser }, {"db_max_size", db_max_size_parser }, {"subj_cache_size", subj_cache_size_parser }, {"obj_cache_size", obj_cache_size_parser }, {"do_stat_report", do_stat_report_parser }, {"watch_fs", watch_fs_parser }, {"ignore_mounts", ignore_mounts_parser }, {"trust", trust_parser }, {"integrity", integrity_parser }, {"syslog_format", syslog_format_parser }, {"rpm_sha256_only", rpm_sha256_only_parser}, {"allow_filesystem_mark", fs_mark_parser }, {"report_interval", report_interval_parser }, {"reset_strategy", reset_strategy_parser }, {"timing_collection", timing_collection_parser }, { NULL, NULL } }; /* * Set everything to its default value */ static void clear_daemon_config(conf_t *config) { config->permissive = 0; config->nice_val = 10; config->q_size = 800; config->decision_threads = 1; config->uid = 0; config->gid = 0; config->do_stat_report = 1; config->detailed_report = 1; config->db_max_size = get_default_db_max_size(); config->do_auto_db_sizing = true; config->subj_cache_size = 4099; config->obj_cache_size = 8191; config->watch_fs = strdup("ext4,xfs,tmpfs"); config->ignore_mounts = NULL; #ifdef USE_RPM config->trust = strdup("rpmdb,file"); #else config->trust = strdup("file"); #endif config->integrity = IN_NONE; config->syslog_format = strdup("rule,dec,perm,auid,pid,exe,:,path,ftype"); config->rpm_sha256_only = 0; config->allow_filesystem_mark = 0; config->report_interval = 0; config->reset_strategy = RESET_NEVER; config->timing_collection = TIMING_COLLECTION_OFF; } int load_daemon_config(conf_t *config) { int fd, lineno = 1; FILE *f; char buf[8192]; clear_daemon_config(config); /* open the file */ fd = open(CONFIG_FILE, O_RDONLY|O_NOFOLLOW); if (fd < 0) { if (errno != ENOENT) { msg(LOG_ERR, "Error opening config file (%s)", strerror(errno)); return 1; } msg(LOG_WARNING, "Config file %s doesn't exist, skipping", CONFIG_FILE); return validate_daemon_config(config); } /* Make into FILE struct and read line by line */ f = fdopen(fd, "rm"); if (f == NULL) { msg(LOG_ERR, "Error - fdopen failed (%s)", strerror(errno)); close(fd); return 1; } while (get_line(f, buf, sizeof(buf), &lineno, CONFIG_FILE)) { // convert line into name-value pair const struct kw_pair *kw; struct nv_pair nv; int rc = nv_split(buf, &nv); switch (rc) { case 0: // fine break; case 1: // not the right number of tokens. msg(LOG_ERR, "Wrong number of arguments for line %d in %s", lineno, CONFIG_FILE); break; case 2: // no '=' sign msg(LOG_ERR, "Missing equal sign for line %d in %s", lineno, CONFIG_FILE); break; default: // something else went wrong... msg(LOG_ERR, "Unknown error for line %d in %s", lineno, CONFIG_FILE); break; } if (nv.name == NULL) { lineno++; continue; } if (nv.value == NULL) { fclose(f); msg(LOG_ERR, "Not processing any more lines in %s", CONFIG_FILE); return 1; } /* identify keyword or error */ kw = kw_lookup(nv.name); if (kw->name == NULL) { msg(LOG_ERR, "Unknown keyword \"%s\" in line %d of %s", nv.name, lineno, CONFIG_FILE); fclose(f); return 1; } else { /* dispatch to keyword's local parser */ rc = kw->parser(&nv, lineno, config); if (rc != 0) { fclose(f); return 1; // local parser puts message out } } lineno++; } fclose(f); return validate_daemon_config(config); } static char *get_line(FILE *f, char *buf, unsigned size, int *lineno, const char *file) { int too_long = 0; while (fgets_unlocked(buf, size, f)) { /* remove newline */ char *ptr = strchr(buf, 0x0a); if (ptr) { if (!too_long) { *ptr = 0; return buf; } // Reset and start with the next line too_long = 0; *lineno = *lineno + 1; } else { if (!too_long) { if (feof(f)) { // last line without trailing newline return buf; } // If a line is too long skip it. // Only output 1 warning msg(LOG_ERR, "Skipping line %d in %s: too long", *lineno, file); } too_long = 1; } } return NULL; } static char *_strsplit(char *s) { static char *str = NULL; char *ptr; if (s) str = s; else { if (str == NULL) return NULL; str++; } retry: ptr = strchr(str, ' '); if (ptr) { if (ptr == str) { str++; goto retry; } s = str; *ptr = 0; str = ptr; return s; } else { s = str; str = NULL; if (*s == 0) return NULL; return s; } } static int nv_split(char *buf, struct nv_pair *nv) { /* Get the name part */ char *ptr; nv->name = NULL; nv->value = NULL; ptr = _strsplit(buf); if (ptr == NULL) return 0; /* If there's nothing, go to next line */ if (ptr[0] == '#') return 0; /* If there's a comment, go to next line */ nv->name = ptr; /* Check for a '=' */ ptr = _strsplit(NULL); if (ptr == NULL) return 1; if (strcmp(ptr, "=") != 0) return 2; /* get the value */ ptr = _strsplit(NULL); if (ptr == NULL) return 1; nv->value = ptr; /* Make sure there's nothing else */ ptr = _strsplit(NULL); if (ptr) { /* Allow one option, but check that there's not 2 */ ptr = _strsplit(NULL); if (ptr) return 1; } /* Everything is OK */ return 0; } static const struct kw_pair *kw_lookup(const char *val) { int i = 0; while (keywords[i].name != NULL) { if (strcmp(keywords[i].name, val) == 0) break; i++; } return &keywords[i]; } void free_daemon_config(conf_t *config) { free((void*)config->watch_fs); free((void*)config->ignore_mounts); free((void*)config->trust); free((void*)config->syslog_format); } /* * daemon_config_lmdb_reader_limit - compute LMDB reader slots to reserve. * @config: daemon configuration to inspect. * * Returns the reader slots needed for decision workers plus maintenance * readers, or 0 if the configured worker count exceeds the daemon's supported * maximum. CPU-count validation belongs in validate_daemon_config(); reader * sizing only needs a non-overflowing daemon-supported worker count. */ unsigned int daemon_config_lmdb_reader_limit(const conf_t *config) { unsigned int threads = 1; if (config && config->decision_threads) threads = config->decision_threads; if (threads > DAEMON_CONFIG_DECISION_THREADS_MAX) return 0; return threads + DAEMON_CONFIG_LMDB_MAINTENANCE_READERS; } /* * u64_add_overflow - checked uint64_t addition. * @total: accumulator updated on success. * @value: value to add. * * Returns 0 on success and 1 when the sum would overflow. */ static int u64_add_overflow(uint64_t *total, uint64_t value) { if (*total > UINT64_MAX - value) return 1; *total += value; return 0; } /* * u64_mul_overflow - checked uint64_t multiplication. * @left: first factor. * @right: second factor. * @result: product written on success. * * Returns 0 on success and 1 when the product would overflow. */ static int u64_mul_overflow(uint64_t left, uint64_t right, uint64_t *result) { if (left != 0 && right > UINT64_MAX / left) return 1; *result = left * right; return 0; } /* * decision_defer_capacity_estimate - match defer array sizing policy. * @subj_cache_size: configured subject cache slots. * * Returns the number of deferred events one worker would preallocate. */ static unsigned int decision_defer_capacity_estimate( unsigned int subj_cache_size) { unsigned int capacity = subj_cache_size / DECISION_DEFER_RATIO; if (capacity < DECISION_DEFER_MIN) capacity = DECISION_DEFER_MIN; return capacity; } /* * lru_memory_estimate - estimate fixed memory for one LRU cache. * @slots: configured LRU slots. * * Returns bytes allocated before per-entry subject/object attributes. */ static int lru_memory_estimate(unsigned int slots, uint64_t *bytes) { uint64_t slot_bytes; if (u64_mul_overflow(slots, sizeof(QNode) + sizeof(QNode *), &slot_bytes)) return 1; *bytes = sizeof(Queue) + sizeof(Hash) + slot_bytes; return 0; } /* * worker_memory_estimate - estimate fixed memory per future worker. * @config: daemon configuration being validated. * @bytes: destination for the estimated bytes. * * Returns 0 on success and 1 when arithmetic overflows. */ static int worker_memory_estimate(const conf_t *config, uint64_t *bytes) { uint64_t total = DAEMON_CONFIG_WORKER_FIXED_OVERHEAD; uint64_t value; unsigned int defer_capacity; uint64_t defer_entry_size; if (lru_memory_estimate(config->subj_cache_size, &value) || u64_add_overflow(&total, value)) return 1; if (lru_memory_estimate(config->obj_cache_size, &value) || u64_add_overflow(&total, value)) return 1; if (u64_mul_overflow(config->q_size, sizeof(decision_event_t), &value) || u64_add_overflow(&total, sizeof(struct queue) + value)) return 1; defer_capacity = decision_defer_capacity_estimate( config->subj_cache_size); defer_entry_size = sizeof(decision_event_t) + (2 * sizeof(uint64_t)) + (2 * sizeof(unsigned int)) + sizeof(int) + 16; if (u64_mul_overflow(defer_capacity, defer_entry_size, &value) || u64_add_overflow(&total, value)) return 1; *bytes = total; return 0; } /* * effective_nofile_limit - estimate startup file descriptor capacity. * @limit_out: destination for the limit, or UINT64_MAX when unlimited. * * The daemon raises RLIMIT_NOFILE to at least DAEMON_CONFIG_MIN_NOFILE during * startup, so validation uses that startup target rather than the pre-start * soft limit. * * Returns 0 on success and 1 when getrlimit fails. */ static int effective_nofile_limit(uint64_t *limit_out) { struct rlimit limit; if (getrlimit(RLIMIT_NOFILE, &limit)) { msg(LOG_ERR, "Cannot inspect RLIMIT_NOFILE: %s", strerror(errno)); return 1; } if (limit.rlim_max == RLIM_INFINITY) { *limit_out = UINT64_MAX; return 0; } *limit_out = limit.rlim_max; if (*limit_out < DAEMON_CONFIG_MIN_NOFILE) *limit_out = DAEMON_CONFIG_MIN_NOFILE; return 0; } /* * memory_budget - estimate a fixed-allocation budget for workers. * * Returns a byte budget based on physical memory and RLIMIT_AS, or 0 when no * meaningful memory ceiling can be inspected. */ static uint64_t memory_budget(void) { struct rlimit limit; uint64_t budget = 0; long pages, page_size; pages = sysconf(_SC_PHYS_PAGES); page_size = sysconf(_SC_PAGESIZE); if (pages > 0 && page_size > 0 && (uint64_t)pages <= UINT64_MAX / (uint64_t)page_size) { uint64_t physical = (uint64_t)pages * (uint64_t)page_size; budget = (physical / 100) * DAEMON_CONFIG_MEMORY_BUDGET_PERCENT; } if (getrlimit(RLIMIT_AS, &limit) == 0 && limit.rlim_cur != RLIM_INFINITY) { uint64_t as_budget = ((uint64_t)limit.rlim_cur / 100) * DAEMON_CONFIG_MEMORY_BUDGET_PERCENT; if (budget == 0 || as_budget < budget) budget = as_budget; } return budget; } /* * validate_decision_threads - validate the future worker count. * @config: daemon configuration being validated. * * Returns 0 when the configured count fits the daemon's fixed worker limits * and host resource limits, otherwise 1. */ static int validate_decision_threads(const conf_t *config) { unsigned int threads, readers; uint64_t fd_limit, fd_needed, per_worker, memory_needed, budget; long cpus; threads = config->decision_threads; if (threads == 0) { msg(LOG_ERR, "decision_threads must be at least 1"); return 1; } if (threads > DAEMON_CONFIG_DECISION_THREADS_MAX) { msg(LOG_ERR, "decision_threads %u exceeds the maximum of %u", threads, DAEMON_CONFIG_DECISION_THREADS_MAX); return 1; } cpus = sysconf(_SC_NPROCESSORS_ONLN); if (cpus < 1) { msg(LOG_ERR, "Cannot determine online CPU count"); return 1; } if (threads > (unsigned long)cpus) { msg(LOG_ERR, "decision_threads %u exceeds online CPU count %ld", threads, cpus); return 1; } readers = daemon_config_lmdb_reader_limit(config); if (readers == 0 || readers > DAEMON_CONFIG_LMDB_MAX_READERS) { msg(LOG_ERR, "decision_threads %u requires %u LMDB readers, max is %u", threads, readers, DAEMON_CONFIG_LMDB_MAX_READERS); return 1; } if (effective_nofile_limit(&fd_limit)) return 1; if (u64_mul_overflow(config->q_size, threads, &fd_needed) || u64_add_overflow(&fd_needed, DAEMON_CONFIG_FD_RESERVE)) { msg(LOG_ERR, "decision_threads file descriptor estimate " "overflowed"); return 1; } if (fd_needed > fd_limit) { msg(LOG_ERR, "decision_threads %u with q_size %u needs about %llu " "file descriptors, limit is %llu", threads, config->q_size, (unsigned long long)fd_needed, (unsigned long long)fd_limit); return 1; } if (worker_memory_estimate(config, &per_worker) || u64_mul_overflow(per_worker, threads, &memory_needed)) { msg(LOG_ERR, "decision_threads memory estimate overflowed"); return 1; } budget = memory_budget(); if (budget && memory_needed > budget) { msg(LOG_ERR, "decision_threads %u needs about %llu MiB fixed worker " "memory, budget is %llu MiB", threads, (unsigned long long)(memory_needed / (1024 * 1024)), (unsigned long long)(budget / (1024 * 1024))); return 1; } return 0; } /* * validate_daemon_config - validate cross-field daemon configuration. * @config: daemon configuration populated from defaults and fapolicyd.conf. * * Returns 0 when the configuration is accepted, otherwise 1. */ int validate_daemon_config(const conf_t *config) { if (config == NULL) return 1; return validate_decision_threads(config); } /* * uintmax_parser - parse a decimal configuration value within a type limit. * @value: destination for the parsed value. * @max: largest value accepted by the destination type. * @str: configuration token to parse. * @line: source line used in error messages. * * Returns 0 on success and 1 for malformed or out-of-range input. */ static int uintmax_parser(uintmax_t *value, uintmax_t max, const char *str, int line) { const char *ptr = str; char *end; uintmax_t parsed; /* check that all chars are numbers */ if (*ptr == '\0') { msg(LOG_ERR, "Value should not be empty - line %d", line); return 1; } for (; *ptr; ptr++) { if (!isdigit((unsigned char)*ptr)) { msg(LOG_ERR, "Value %s should only be numbers - line %d", str, line); return 1; } } /* convert only after syntax validation so signs are never accepted */ errno = 0; parsed = strtoumax(str, &end, 10); if (errno) { msg(LOG_ERR, "Error converting string to a number (%s) - line %d", strerror(errno), line); return 1; } if (*end != '\0') { msg(LOG_ERR, "Value %s should only be numbers - line %d", str, line); return 1; } if (parsed > max) { msg(LOG_ERR, "Value %s is outside supported range - line %d", str, line); return 1; } *value = parsed; return 0; } static int unsigned_int_parser(unsigned *i, const char *str, int line) { uintmax_t value; if (uintmax_parser(&value, UINT_MAX, str, line)) return 1; *i = (unsigned int)value; return 0; } static int permissive_parser(const struct nv_pair *nv, int line, conf_t *config) { int rc = unsigned_int_parser(&(config->permissive), nv->value, line); if (rc == 0 && config->permissive > 1) { msg(LOG_WARNING, "permissive value reset to 1 - line %d", line); config->permissive = 1; } return rc; } static int nice_val_parser(const struct nv_pair *nv, int line, conf_t *config) { int rc = unsigned_int_parser(&(config->nice_val), nv->value, line); if (rc == 0 && config->nice_val > 20) { msg(LOG_WARNING, "Error, nice_val is larger than 20 - line %d", line); rc = 1; } return rc; } static int q_size_parser(const struct nv_pair *nv, int line, conf_t *config) { int rc = unsigned_int_parser(&(config->q_size), nv->value, line); if (rc == 0 && config->q_size > 10480) msg(LOG_WARNING, "q_size might be unnecessarily large - line %d", line); return rc; } /* * decision_threads_parser - parse configured decision worker count. * @nv: name/value pair describing the option. * @line: line number where the option was found. * @config: configuration structure to update. * * Returns 0 on success and 1 when the value is not a positive integer. */ static int decision_threads_parser(const struct nv_pair *nv, int line, conf_t *config) { int rc = unsigned_int_parser(&(config->decision_threads), nv->value, line); if (rc == 0 && config->decision_threads == 0) { msg(LOG_ERR, "decision_threads must be at least 1 - line %d", line); return 1; } return rc; } static int uid_parser(const struct nv_pair *nv, int line, conf_t *config) { uid_t uid = 0; gid_t gid = 0; if (isdigit((unsigned char)nv->value[0])) { uintmax_t max = (uintmax_t)((uid_t)-1); uintmax_t value; if ((uintmax_t)((gid_t)-1) < max) max = (uintmax_t)((gid_t)-1); if (uintmax_parser(&value, max, nv->value, line)) return 1; uid = (uid_t)value; gid = uid; } else { struct passwd *pw = getpwnam(nv->value); if (pw == NULL) { msg(LOG_ERR, "user %s is unknown - line %d", nv->value, line); return 1; } uid = pw->pw_uid; gid = pw->pw_gid; endpwent(); } config->uid = uid; config->gid = gid; return 0; } static int gid_parser(const struct nv_pair *nv, int line, conf_t *config) { gid_t gid = 0; if (isdigit((unsigned char)nv->value[0])) { uintmax_t value; if (uintmax_parser(&value, (uintmax_t)((gid_t)-1), nv->value, line)) return 1; gid = (gid_t)value; } else { struct group *gr ; gr = getgrnam(nv->value); if (gr == NULL) { msg(LOG_ERR, "group %s is unknown - line %d", nv->value, line); return 1; } gid = gr->gr_gid; endgrent(); } config->gid = gid; return 0; } /* * daemon_config_parse_unsigned_for_tests - exercise unsigned config parsing. * @value: destination for the parsed value. * @text: decimal token to parse. * * Returns 0 on success and 1 for malformed or out-of-range input. */ int daemon_config_parse_unsigned_for_tests(unsigned int *value, const char *text) { return unsigned_int_parser(value, text, 1); } static int detailed_report_parser(const struct nv_pair *nv, int line, conf_t *config) { return unsigned_int_parser(&(config->detailed_report), nv->value, line); } static int db_max_size_parser(const struct nv_pair *nv, int line, conf_t *config) { unsigned int db_max_size = config->db_max_size; // "auto" keeps utilization-based sizing enabled. A numeric value is an // explicit administrator override and keeps the legacy fixed MiB limit. if (strcmp(nv->value, "auto") == 0) { config->db_max_size = get_default_db_max_size(); config->do_auto_db_sizing = true; return 0; } if (unsigned_int_parser(&db_max_size, nv->value, line)) return 1; config->db_max_size = db_max_size; config->do_auto_db_sizing = false; return 0; } static int subj_cache_size_parser(const struct nv_pair *nv, int line, conf_t *config) { int rc=unsigned_int_parser(&(config->subj_cache_size), nv->value, line); if (rc == 0 && config->subj_cache_size > 16384) msg(LOG_WARNING, "subj_cache_size might be unnecessarily large - line %d", line); return rc; } static int obj_cache_size_parser(const struct nv_pair *nv, int line, conf_t *config) { int rc=unsigned_int_parser(&(config->obj_cache_size), nv->value, line); if (rc == 0 && config->obj_cache_size > 32768) msg(LOG_WARNING, "obj_cache_size might be unnecessarily large - line %d", line); return rc; } static int do_stat_report_parser(const struct nv_pair *nv, int line, conf_t *config) { int rc=unsigned_int_parser(&(config->do_stat_report), nv->value, line); if (rc == 0 && config->do_stat_report > 2) { msg(LOG_WARNING, "do_stat_report value reset to 1 - line %d", line); config->do_stat_report = 1; } return rc; } static int watch_fs_parser(const struct nv_pair *nv, int line, conf_t *config) { free((void *)config->watch_fs); config->watch_fs = strdup(nv->value); if (config->watch_fs) return 0; msg(LOG_ERR, "Could not store value line %d", line); return 1; } /* * ignore_mounts_parser - store ignore_mounts configuration setting. * @nv: name/value pair describing the option. * @line: line number where the option was found. * @config: configuration structure to update. * Returns 0 on success and 1 when memory cannot be allocated. */ static int ignore_mounts_parser(const struct nv_pair *nv, int line, conf_t *config) { free((void *)config->ignore_mounts); config->ignore_mounts = strdup(nv->value); if (config->ignore_mounts) return 0; msg(LOG_ERR, "Could not store value line %d", line); return 1; } static int report_interval_parser(const struct nv_pair *nv, int line, conf_t *config) { return unsigned_int_parser(&(config->report_interval), nv->value, line); } static const struct nv_list reset_strategies[] = { {"never", RESET_NEVER }, {"auto", RESET_AUTO }, {"manual", RESET_MANUAL }, { NULL, 0 } }; /* * reset_strategy_parser - parse metrics reset strategy. * @nv: name/value pair describing the option. * @line: line number where the option was found. * @config: configuration structure to update. * Returns 0 on success and 1 when the value is unknown. */ static int reset_strategy_parser(const struct nv_pair *nv, int line, conf_t *config) { for (int i = 0; reset_strategies[i].name != NULL; i++) { if (strcasecmp(nv->value, reset_strategies[i].name) == 0) { config->reset_strategy = reset_strategies[i].option; return 0; } } msg(LOG_ERR, "Option %s not found - line %d", nv->value, line); return 1; } static const struct nv_list timing_collections[] = { {"off", TIMING_COLLECTION_OFF }, {"manual", TIMING_COLLECTION_MANUAL }, { NULL, 0 } }; /* * timing_collection_parser - parse timing collection control mode. * @nv: name/value pair describing the option. * @line: line number where the option was found. * @config: configuration structure to update. * Returns 0 on success and 1 when the value is unknown. */ static int timing_collection_parser(const struct nv_pair *nv, int line, conf_t *config) { for (int i = 0; timing_collections[i].name != NULL; i++) { if (strcasecmp(nv->value, timing_collections[i].name) == 0) { config->timing_collection = timing_collections[i].option; return 0; } } msg(LOG_ERR, "Option %s not found - line %d", nv->value, line); return 1; } static int trust_parser(const struct nv_pair *nv, int line, conf_t *config) { free((void *)config->trust); config->trust = strdup(nv->value); if (config->trust) return 0; msg(LOG_ERR, "Could not store value line %d", line); return 1; } static const struct nv_list integrity_schemes[] = { {"none", IN_NONE }, {"size", IN_SIZE }, {"ima", IN_IMA }, {"sha256", IN_SHA256 }, { NULL, 0 } }; static int integrity_parser(const struct nv_pair *nv, int line, conf_t *config) { for (int i=0; integrity_schemes[i].name != NULL; i++) { if (strcasecmp(nv->value, integrity_schemes[i].name) == 0) { config->integrity = integrity_schemes[i].option; if (config->integrity == IN_IMA) { int fd = open("/bin/sh", O_RDONLY); if (fd >= 0) { char sha[FILE_DIGEST_STRING_MAX]; file_hash_alg_t alg; int rc = get_ima_hash(fd, &alg, sha); close(fd); if (rc == 0) { msg(LOG_ERR, "IMA integrity checking selected, but the extended attributes can't be read"); return 1; } } else { msg(LOG_ERR, "IMA integrity checking selected, but can't test the shell"); return 1; } } return 0; } } msg(LOG_ERR, "Option %s not found - line %d", nv->value, line); return 1; } const char *lookup_integrity(unsigned value) { if (value > 3) return NULL; return integrity_schemes[value].name; } /* * lookup_reset_strategy - return the reset strategy name. * @value: reset_strategy_t value to describe. * Returns the strategy name, or NULL when the value is unknown. */ const char *lookup_reset_strategy(unsigned value) { if (value > RESET_MANUAL) return NULL; return reset_strategies[value].name; } /* * lookup_timing_collection - return the timing collection mode name. * @value: timing_collection_t value to describe. * Returns the timing mode name, or NULL when the value is unknown. */ const char *lookup_timing_collection(unsigned value) { if (value > TIMING_COLLECTION_MANUAL) return NULL; return timing_collections[value].name; } static int syslog_format_parser(const struct nv_pair *nv, int line, conf_t *config) { free((void *)config->syslog_format); config->syslog_format = strdup(nv->value); if (config->syslog_format) return 0; msg(LOG_ERR, "Could not store value line %d", line); return 1; } static int rpm_sha256_only_parser(const struct nv_pair *nv, int line, conf_t *config) { int rc = 0; #ifndef USE_RPM msg(LOG_WARNING, "rpm_sha256_only: fapolicyd was not built with rpm support, ignoring" ); #else rc = unsigned_int_parser(&(config->rpm_sha256_only), nv->value, line); if (rc == 0 && config->rpm_sha256_only > 1) { msg(LOG_WARNING, "rpm_sha256_only value reset to 0 - line %d", line); config->rpm_sha256_only = 0; } #endif return rc; } static int fs_mark_parser(const struct nv_pair *nv, int line, conf_t *config) { int rc = 0; #if defined HAVE_DECL_FAN_MARK_FILESYSTEM && HAVE_DECL_FAN_MARK_FILESYSTEM != 0 rc = unsigned_int_parser(&(config->allow_filesystem_mark), nv->value, line); if (rc == 0 && config->allow_filesystem_mark > 1) { msg(LOG_WARNING, "allow_filesystem_mark value reset to 0 - line %d", line); config->allow_filesystem_mark = 0; } #else msg(LOG_WARNING, "allow_filesystem_mark is unsupported on this kernel - ignoring"); #endif return rc; } fapolicyd-2.0/src/library/daemon-config.h000066400000000000000000000050371524071644000204470ustar00rootroot00000000000000/* daemon-config.h -- * Copyright 2018-21,2026 Red Hat Inc. * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Authors: * Steve Grubb * Radovan Sroka * */ #ifndef DAEMON_CONFIG_H #define DAEMON_CONFIG_H #include "conf.h" /* * Keep this cap in sync with fixed-size per-worker arrays such as decision * timing and attribute-lookup metrics. The value is intentionally tied to the * largest CPU count this implementation is prepared to use, not to arbitrary * configuration input. */ #define DAEMON_CONFIG_DECISION_THREADS_MAX 32 /* * Reserve LMDB reader slots for non-decision activity such as status walks, * trust database reload inspection, compaction validation, and administrative * checks. Eight gives those maintenance paths headroom without keeping the * LMDB reader table much larger than the worker cap. */ #define DAEMON_CONFIG_LMDB_MAINTENANCE_READERS 8 /* Largest LMDB reader table this config layer will request. */ #define DAEMON_CONFIG_LMDB_MAX_READERS \ (DAEMON_CONFIG_DECISION_THREADS_MAX + \ DAEMON_CONFIG_LMDB_MAINTENANCE_READERS) /* * Startup raises RLIMIT_NOFILE to at least this value when possible. The value * preserves the existing daemon behavior and lets default q_size plus future * worker queues fit under the service limit without requiring immediate * systemd LimitNOFILE changes. */ #define DAEMON_CONFIG_MIN_NOFILE 16384 int load_daemon_config(conf_t *config); int validate_daemon_config(const conf_t *config); void free_daemon_config(conf_t *config); unsigned int daemon_config_lmdb_reader_limit(const conf_t *config); const char *lookup_integrity(unsigned value); const char *lookup_reset_strategy(unsigned value); const char *lookup_timing_collection(unsigned value); // Function for unit test use int daemon_config_parse_unsigned_for_tests(unsigned int *value, const char *text); #endif fapolicyd-2.0/src/library/database-internal.h000066400000000000000000000021741524071644000213160ustar00rootroot00000000000000/* * database-internal.h - private boundary between trust DB modules * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef DATABASE_INTERNAL_HEADER #define DATABASE_INTERNAL_HEADER #include #include "conf.h" #include "fapolicyd-backend.h" #include "gcc-attributes.h" int database_update_controls_init(void) __wur; void database_update_controls_destroy(void); int database_update_thread_start(conf_t *config) __nonnull ((1)) __wur; void database_update_thread_stop(void); void database_update_read_lock(void); void database_update_read_unlock(void); int database_reload_from_backends(conf_t *config) __nonnull ((1)) __wur; void database_compact_from_backends(conf_t *config) __nonnull ((1)); int database_store_update_record(const char *path, trustdb_size_t size, const char *hash) __nonnull ((1, 3)) __attr_access ((__read_only__, 1)) __attr_access ((__read_only__, 3)) __wur; #endif fapolicyd-2.0/src/library/database-update.c000066400000000000000000000504771524071644000207700ustar00rootroot00000000000000/* * database-update.c - trust database update controller * Copyright (c) 2016,2018-26 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. */ /* * Overview * ------- * * This file owns the daemon-side control plane for trust database updates. * The LMDB storage, generation publication, sizing, and lookup rules live in * database.c. This module keeps the operational machinery around that database: * the update FIFO, the background polling thread, reload request coalescing, * cache-flush commands, and the legacy locks used to block readers during a * controlled environment swap. * * The boundary is intentionally small. The controller parses external commands * and calls database_reload_from_backends(), database_compact_from_backends(), * or database_store_update_record(). It does not reach into LMDB handles or * generation state. Conversely, database.c calls database_update_read_lock() * when a decision lookup needs shared read ownership. * * RELOAD_TRUSTDB_COMMAND requests are coalesced. If a reload arrives while one * is active, one follow-up request stays pending so a SIGHUP that changes * backend configuration cannot be hidden by an earlier rebuild. * Once published, the FIFO must have a live consumer. Transient resource * failures are retried so control-plane updates cannot silently stop. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "database.h" #include "database-internal.h" #include "event.h" #include "failure-action.h" #include "fd-fgets.h" #include "message.h" #include "paths.h" #include "policy.h" enum { DB_NO_OP, ONE_FILE, RELOAD_DB, FLUSH_CACHE, RELOAD_RULES, COMPACT_DB, }; #define UPDATE_BUFFER_SIZE 4096 /* fd 0 is valid when the daemon starts with stdin closed. */ #define UPDATE_FIFO_CLOSED_FD (-1) #define UPDATE_RETRY_DELAY_SEC 1 static struct pollfd ffd[1] = { { UPDATE_FIFO_CLOSED_FD, 0, 0 } }; static pthread_t update_thread; static int update_thread_created; static int update_lock_inited; static int rule_lock_inited; static pthread_rwlock_t update_lock; static pthread_mutex_t rule_lock; static atomic_bool reload_db = false; static atomic_bool reload_db_active = false; extern atomic_bool stop; extern atomic_bool reload_rules; static void *update_thread_main(void *arg); /* * update_retry_delay - pause before retrying a transient controller failure. * * Returns nothing. Shutdown is observed before retrying the failed operation. */ static void update_retry_delay(void) { struct timespec delay = { .tv_sec = UPDATE_RETRY_DELAY_SEC }; while (!stop && nanosleep(&delay, &delay) && errno == EINTR) ; } /* * database_update_close_fifo - close the update FIFO if it is open. * * Returns nothing. */ static void database_update_close_fifo(void) { if (ffd[0].fd >= 0) close(ffd[0].fd); ffd[0].fd = UPDATE_FIFO_CLOSED_FD; ffd[0].events = 0; ffd[0].revents = 0; } /* * preconstruct_fifo - create and open the daemon update FIFO. * @config: daemon configuration containing the target group id. * * Returns 0 on success and 1 on failure. */ int preconstruct_fifo(const conf_t *config) { int rc; char err_buff[UPDATE_BUFFER_SIZE]; /* Keep RUN_DIR mode/owner aligned with daemon IPC expectations. */ if (mkdir(RUN_DIR, 0770) && errno != EEXIST) { msg(LOG_ERR, "Failed to create a directory %s (%s)", RUN_DIR, strerror_r(errno, err_buff, UPDATE_BUFFER_SIZE)); return 1; } else { if ((chmod(RUN_DIR, 0770))) { msg(LOG_ERR, "Failed to fix mode of dir %s (%s)", RUN_DIR, strerror_r(errno, err_buff, UPDATE_BUFFER_SIZE)); return 1; } if ((chown(RUN_DIR, 0, config->gid))) { msg(LOG_ERR, "Failed to fix ownership of dir %s (%s)", RUN_DIR, strerror_r(errno, err_buff, UPDATE_BUFFER_SIZE)); return 1; } /* Make sure that there is no such file/fifo */ unlink_fifo(); } rc = mkfifo(fifo_path, 0660); if (rc != 0) { msg(LOG_ERR, "Failed to create a pipe %s (%s)", fifo_path, strerror_r(errno, err_buff, UPDATE_BUFFER_SIZE)); return 1; } if ((ffd[0].fd = open(fifo_path, O_RDWR)) == -1) { msg(LOG_ERR, "Failed to open a pipe %s (%s)", fifo_path, strerror_r(errno, err_buff, UPDATE_BUFFER_SIZE)); ffd[0].fd = UPDATE_FIFO_CLOSED_FD; unlink_fifo(); return 1; } if (config->gid != getgid()) { if ((fchown(ffd[0].fd, 0, config->gid))) { msg(LOG_ERR, "Failed to fix ownership of pipe %s (%s)", fifo_path, strerror_r(errno, err_buff, UPDATE_BUFFER_SIZE)); unlink_fifo(); database_update_close_fifo(); return 1; } } return 0; } /* * unlink_fifo - remove the daemon update FIFO. * * Returns nothing. */ void unlink_fifo(void) { unlink(fifo_path); } /* * database_update_controls_init - initialize update/rule synchronization. * * Returns 0 on success or a pthread error code. */ int database_update_controls_init(void) { int rc; if (!update_lock_inited) { /* * FIXME: Default glibc rwlocks can starve an update writer * under overlapping integrity lookups. Audit recursive read * locking, then investigate * PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP with a portable * fallback. */ rc = pthread_rwlock_init(&update_lock, NULL); if (rc) return rc; update_lock_inited = 1; } if (!rule_lock_inited) { rc = pthread_mutex_init(&rule_lock, NULL); if (rc) return rc; rule_lock_inited = 1; } return 0; } /* * database_update_controls_destroy - destroy update/rule synchronization. * * Returns nothing. */ void database_update_controls_destroy(void) { if (update_lock_inited) { pthread_rwlock_destroy(&update_lock); update_lock_inited = 0; } if (rule_lock_inited) { pthread_mutex_destroy(&rule_lock); rule_lock_inited = 0; } } /* * database_update_thread_start - start the background update controller. * @config: daemon configuration passed to reload and FIFO setup paths. * * Returns 0 on success or a pthread error code. */ int database_update_thread_start(conf_t *config) { char err_buff[UPDATE_BUFFER_SIZE]; int rc; if (update_thread_created) return 0; rc = database_update_controls_init(); if (rc) return rc; rc = pthread_create(&update_thread, NULL, update_thread_main, config); if (rc == 0) { update_thread_created = 1; return 0; } msg(LOG_ERR, "Failed to create update thread (%s)", strerror_r(rc, err_buff, sizeof(err_buff))); return rc; } /* * database_update_thread_stop - join the update thread and clean the FIFO. * * Returns nothing. */ void database_update_thread_stop(void) { if (update_thread_created) { pthread_join(update_thread, NULL); update_thread_created = 0; } database_update_close_fifo(); unlink_fifo(); } /* * lock_update_thread - take exclusive trust DB update ownership. * * Returns nothing. */ void lock_update_thread(void) { pthread_rwlock_wrlock(&update_lock); //msg(LOG_DEBUG, "lock_update_thread()"); } /* * unlock_update_thread - release exclusive trust DB update ownership. * * Returns nothing. */ void unlock_update_thread(void) { pthread_rwlock_unlock(&update_lock); //msg(LOG_DEBUG, "unlock_update_thread()"); } /* * database_update_read_lock - take shared trust DB read ownership. * * Returns nothing. */ void database_update_read_lock(void) { /* Shutdown reports can run after close_database() destroys this lock. */ if (!update_lock_inited) return; pthread_rwlock_rdlock(&update_lock); } /* * database_update_read_unlock - release shared trust DB read ownership. * * Returns nothing. */ void database_update_read_unlock(void) { if (!update_lock_inited) return; pthread_rwlock_unlock(&update_lock); } /* * lock_rule - take the rule reload mutex when it is available. * * Returns nothing. */ void lock_rule(void) { /* * Rules load before init_database() creates this mutex, and the final * shutdown report can run after close_database() destroys it. Those * phases are single-threaded with no rule reload race to serialize. */ if (!rule_lock_inited) return; pthread_mutex_lock(&rule_lock); //msg(LOG_DEBUG, "lock_rule()"); } /* * unlock_rule - release the rule reload mutex when it is available. * * Returns nothing. */ void unlock_rule(void) { if (!rule_lock_inited) return; pthread_mutex_unlock(&rule_lock); //msg(LOG_DEBUG, "unlock_rule()"); } /* * handle_update_record - process one path update from the FIFO. * @buffer: Raw line containing path, file size, and SHA256 hash. * * Returns 0 after successfully storing the record, 1 when processing should * stop due to malformed data or a shutdown request. * * A future throughput optimization may collect a bounded run of ONE_FILE * commands and store them in one LMDB transaction. Such a batch must flush * before control commands and on a time or record limit so package scriptlets * see new trust records promptly. */ static int handle_update_record(const char *buffer) { char path[2048 + 1]; char hash[64 + 1]; trustdb_size_t size; int rc; int res; if (stop) return 1; // validating input res = sscanf(buffer, "%2048s %" SCNu64 " %64s", path, &size, hash); msg(LOG_DEBUG, "update_thread: Parsing input buffer: %s", buffer); msg(LOG_DEBUG, "update_thread: Parsing input words(expected 3): %d", res); if (res != 3) { msg(LOG_INFO, "Corrupted data read, ignoring..."); return 1; } msg(LOG_DEBUG, "update_thread: Saving %s", path); rc = database_store_update_record(path, size, hash); return rc ? 1 : 0; } /* * request_reload_trust_database - queue a trust DB reload if one is needed. * @source: short log label for the caller requesting reload. * * A trust DB reload rebuilds the database from the current backend snapshots. * If another request is already pending, the later request does not add more * ordering information: both reloads would consume the same current backend * state by the time the update thread can run them. Requests received during * an active reload are left pending because SIGHUP can change the configured * backend list after the active rebuild has already selected its snapshots. * This still coalesces many in-flight requests into one follow-up rebuild. * * Returns 1 when a new request was queued, 0 when it was coalesced. */ static int request_reload_trust_database(const char *source) { if (atomic_exchange_explicit(&reload_db, true, memory_order_acq_rel)) { msg(LOG_INFO, "Dropping trust database reload from %s: reload already pending", source); return 0; } if (atomic_load_explicit(&reload_db_active, memory_order_acquire)) msg(LOG_INFO, "Queued trust database reload from %s: reload already active", source); return 1; } /* * set_reload_trust_database - queue a SIGHUP-triggered trust DB reload. * * Returns nothing. */ void set_reload_trust_database(void) { request_reload_trust_database("SIGHUP"); } /* * begin_trust_database_reload - mark a trust DB reload active. * @source: short log label for the caller starting reload. * * Returns 1 when the caller may run the reload, 0 when another reload is * already active. */ static int begin_trust_database_reload(const char *source) { bool expected = false; /* * Consume the request before publishing the active marker. A SIGHUP that * arrives after this point must remain pending for a follow-up reload; * clearing reload_db after reload_db_active becomes true could drop that * config-changing request. */ atomic_store_explicit(&reload_db, false, memory_order_release); if (!atomic_compare_exchange_strong_explicit(&reload_db_active, &expected, true, memory_order_acq_rel, memory_order_acquire)) { atomic_store_explicit(&reload_db, true, memory_order_release); msg(LOG_INFO, "Deferring trust database reload from %s: reload already active", source); return 0; } return 1; } /* * finish_trust_database_reload - clear the active trust DB reload marker. * * Returns nothing. */ static void finish_trust_database_reload(void) { atomic_store_explicit(&reload_db_active, false, memory_order_release); } /* * run_trust_database_reload - run a coalesced trust DB reload request. * @config: active daemon configuration. * @source: short log label for the request source. * * Returns 1 when a reload ran, 0 when the request was coalesced with another * active reload. */ static int run_trust_database_reload(conf_t *config, const char *source) { if (!begin_trust_database_reload(source)) return 0; database_reload_from_backends(config); finish_trust_database_reload(); return 1; } /* * reload_rules_from_file - perform a requested rule reload. * @config: daemon configuration used for parsing syslog fields. * * Returns 0 on success and non-zero on failure. */ static int reload_rules_from_file(conf_t *config) { int rc; if (load_rule_file()) { failure_action_record(FAILURE_REASON_RULE_RELOAD_FAILURE); msg(LOG_ERR, "Rule reload aborted: unable to open rules file (%s)", strerror(errno)); return 1; } /* * Rules now publish as immutable snapshots. Parsing can be slow for * large macro/set based policies, so do not hold the legacy rule mutex * here; decisions keep using the old snapshot until publish succeeds. */ rc = do_reload_rules(config); if (rc) msg(LOG_ERR, "Rule reload failed; previous policy preserved"); return rc; } /* * update_thread_main - poll the update FIFO and dispatch daemon commands. * @arg: active daemon configuration. * * Returns NULL when the thread exits. */ static void *update_thread_main(void *arg) { int rc; int flags; int poll_error_logged = 0; int reader_error_logged = 0; sigset_t sigs; char buff[UPDATE_BUFFER_SIZE]; char err_buff[UPDATE_BUFFER_SIZE]; conf_t *config = (conf_t *)arg; fd_fgets_state_t *st = NULL; int do_operation = DB_NO_OP; #ifdef DEBUG msg(LOG_DEBUG, "Update thread main started"); #endif /* This is a worker thread. Don't handle external signals. */ sigemptyset(&sigs); sigaddset(&sigs, SIGTERM); sigaddset(&sigs, SIGHUP); sigaddset(&sigs, SIGUSR1); sigaddset(&sigs, SIGINT); sigaddset(&sigs, SIGQUIT); pthread_sigmask(SIG_SETMASK, &sigs, NULL); if (ffd[0].fd < 0) { if (preconstruct_fifo(config)) return NULL; } /* * fd_fgets_r() must not block if poll readiness is consumed or only * a partial line is available. */ flags = fcntl(ffd[0].fd, F_GETFL); if (flags == -1) { msg(LOG_ERR, "Failed to read pipe flags (%s)", strerror_r(errno, err_buff, UPDATE_BUFFER_SIZE)); goto finalize; } if (fcntl(ffd[0].fd, F_SETFL, flags | O_NONBLOCK) == -1) { msg(LOG_ERR, "Failed to set non-blocking pipe mode (%s)", strerror_r(errno, err_buff, UPDATE_BUFFER_SIZE)); goto finalize; } ffd[0].events = POLLIN; /* * This thread is the only consumer of runtime rule, trust database, and * FIFO update requests. No supervisor restarts it, so transient failures * must not leave the daemon permanently enforcing stale state. */ while (!stop && st == NULL) { st = fd_fgets_init(); if (st == NULL) { if (!reader_error_logged) { msg(LOG_ERR, "Failed to initialize buffered FIFO reader; retrying"); reader_error_logged = 1; } update_retry_delay(); } } while (!stop) { int trust_reload_done_this_cycle = 0; /* * The FIFO connected at ffd[0] carries update commands from * fapolicy-cli and backend helper processes. Commands may be * the single-character control values defined in database.h * or full path entries emitted by the backend notifier when a * package manager changes a file. */ rc = poll(ffd, 1, 1000); if (stop) break; if (reload_rules) { reload_rules = false; reload_rules_from_file(config); } // got SIGHUP if (atomic_load_explicit(&reload_db, memory_order_acquire)) trust_reload_done_this_cycle = run_trust_database_reload(config, "pending request"); #ifdef DEBUG msg(LOG_DEBUG, "Update poll interrupted"); #endif if (rc < 0) { int poll_errno = errno; if (poll_errno == EINTR) { #ifdef DEBUG msg(LOG_DEBUG, "update poll rc = EINTR"); #endif continue; } if (!poll_error_logged) { msg(LOG_ERR, "Update poll error (%s); retrying", strerror_r(poll_errno, err_buff, UPDATE_BUFFER_SIZE)); poll_error_logged = 1; } update_retry_delay(); continue; } else if (rc == 0) { poll_error_logged = 0; #ifdef DEBUG msg(LOG_DEBUG, "Update poll timeout expired"); #endif continue; } else { poll_error_logged = 0; if (ffd[0].revents & POLLIN) { do { int res; if (stop) break; res = fd_fgets_r(st, buff, sizeof(buff), ffd[0].fd); // nothing to read if (res == -1) break; else if (res > 0) { char *end = strchr(buff, '\n'); int count; if (end == NULL) { msg(LOG_ERR, "Too long line?"); continue; } count = end - buff; *end = '\0'; for (int i = 0; i < count; i++) { /* * Identify the requested action by scanning * the buffer. Control characters map directly * to values above while a leading slash * indicates a file path update. */ if (stop) break; // assume file name // operation = 0 if (buff[i] == '/') { do_operation = ONE_FILE; break; } if (buff[i] == RELOAD_TRUSTDB_COMMAND) { do_operation = RELOAD_DB; break; } if (buff[i] == FLUSH_CACHE_COMMAND) { do_operation = FLUSH_CACHE; break; } if (buff[i] == RELOAD_RULES_COMMAND) { do_operation = RELOAD_RULES; break; } if (buff[i] == COMPACT_TRUSTDB_COMMAND) { do_operation = COMPACT_DB; break; } if (isspace((unsigned char)buff[i])) continue; msg(LOG_ERR, "Cannot handle data \"%s\" from pipe", buff); break; } *end = '\n'; if (stop) break; // got "1" -> reload db if (do_operation == RELOAD_DB) { /* * A RELOAD_TRUSTDB_COMMAND triggers a * complete rebuild from all configured * backends. */ do_operation = DB_NO_OP; if (trust_reload_done_this_cycle) { msg(LOG_INFO, "Dropping trust database reload from FIFO: " "reload already handled in this update cycle"); } else { trust_reload_done_this_cycle = run_trust_database_reload(config, "FIFO"); } } else if (do_operation == RELOAD_RULES) { /* * The rules command instructs the daemon * to re-parse policy files. */ do_operation = DB_NO_OP; reload_rules_from_file(config); } else if (do_operation == COMPACT_DB) { /* * Explicit maintenance command: build a * replacement LMDB environment from backend * snapshots and swap it after validation. */ do_operation = DB_NO_OP; database_compact_from_backends(config); // got "2" -> flush cache } else if (do_operation == FLUSH_CACHE) { /* * Cache flushes originate from helper * tools needing clients to drop cached * trust decisions. */ do_operation = DB_NO_OP; request_object_cache_flush(); } else if (do_operation == ONE_FILE) { /* * Backend helpers send path/size/hash * tuples for individual files that * changed on disk. */ do_operation = DB_NO_OP; if (handle_update_record(buff)) continue; } } } while (!fd_fgets_eof_r(st) && !stop); } } } finalize: if (st != NULL) fd_fgets_destroy(st); database_update_close_fifo(); unlink_fifo(); return NULL; } fapolicyd-2.0/src/library/database.c000066400000000000000000005141611524071644000175030ustar00rootroot00000000000000/* * database.c - Trust database * Copyright (c) 2016,2018-24 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * Radovan Sroka * Marek Tamaskovic */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* isspace() */ #include "database.h" #include "database-internal.h" #include "daemon-config.h" #include "decision-config.h" #include "decision-timing.h" #include "event.h" #include "failure-action.h" #include "message.h" #include "file.h" #include "fd-fgets.h" #include "string-util.h" #include "fapolicyd-backend.h" #include "backend-manager.h" #include "gcc-attributes.h" #include "paths.h" // Local defines enum { READ_DATA, READ_TEST_KEY, READ_DATA_DUP }; enum autosize_plan_mode { AUTOSIZE_STARTUP_INSPECTION, AUTOSIZE_LIVE_INSPECTION, AUTOSIZE_RELOAD_PREFLIGHT, }; #define BUFFER_SIZE 4096 #define MEGABYTE (1024*1024) #define MAX_DELIMS 3 // Trustdb has 4 fields - therefore 3 delimiters #define DEFAULT_DB_MAX_SIZE_MB 100 #define WRITE_DB_MAP_FULL 6 #define UPDATE_DB_PRESERVED 7 #define TRUST_DB_ACTIVE_TARGET_PERCENT 75 #define TRUST_DB_RELOAD_HIGHWATER_PERCENT 80 #define TRUST_DB_SHRINK_TRIGGER_PERCENT 65 #define TRUST_DB_SHRINK_HYSTERESIS_PERCENT 90 #define TRUST_DB_RELOAD_WORK_FACTOR 2 #define TRUST_DB_REBUILD_TXN_RECORDS 4096 #define TRUST_DB_GENERATION_SLOT_PREFIX "trust.slot" #define TRUST_DB_GENERATION_DB_SLOTS 32 /* * mdb_env_set_maxdbs() sizes LMDB's named-DB slot table for each * transaction, and mdb_dbi_open() searches opened slots linearly. Keep the * cap close to the reusable trust generation slots while leaving room for * metadata, old DB names during migration, and future small additions. */ #define TRUST_DB_RESERVED_NAMED_DBS 8 #define TRUST_DB_MAX_NAMED_DBS \ (TRUST_DB_GENERATION_DB_SLOTS + TRUST_DB_RESERVED_NAMED_DBS) #define TRUST_DB_COMPACT_REASON_SIZE 160 /* * Trust database generation lifecycle * =================================== * * The trust database is stored in one LMDB environment with multiple named * databases. The small metadata database named TRUST_DB_METADATA_NAME stores * TRUST_DB_METADATA_KEY, whose value identifies the currently published named * database, the daemon-local generation number, entry count, publish time, and * sizing snapshot. The named database in metadata is durable. The generation * number is deliberately a daemon-runtime epoch, like config and ruleset * generations, so startup reports begin at generation 1. * * Startup path: * - init_database() performs migration and startup autosizing, then calls * init_db(). * - init_db() opens LMDB and calls init_dbi(). * - init_dbi() opens the metadata DB, reads the persisted active named DB if * present, opens that named DB, and publishes it in memory as generation 1. * It ignores the persisted generation number because that belonged to the * previous daemon run, then rewrites metadata with generation 1. * - backend_load() builds backend snapshots. database_empty() populates a new * empty DB in place as generation 1. If check_database_copy() finds a * mismatch in an existing DB, init_database() calls update_database(config, * 1), which resets next_generation so the startup rebuild also publishes * generation 1. * * Runtime reload path: * - database-update.c coalesces external reload requests from SIGHUP, the * update FIFO, and fapolicy-cli into database_reload_from_backends(). * - database_reload_from_backends() refreshes backends and calls * update_database(config, 0). * update_database() runs autosize_reload_preflight(), creates a candidate * generation, imports all backend records, publishes only a complete * candidate, and requests a cache flush on success. * * Candidate creation and publication: * - create_candidate_generation() chooses a bounded reusable LMDB slot name * through trust_db_candidate_name(). Slots are named with * TRUST_DB_GENERATION_SLOT_PREFIX and are skipped while active or retired * generations still reference them. * - create_database_for_generation() populates the candidate from backend * memfd snapshots using do_memfd_update_to_dbi(). Imports are chunked by * TRUST_DB_REBUILD_TXN_RECORDS so large rebuilds do not hold one huge write * transaction. * - Failed imports call drop_candidate_generation(); active_generation and * metadata are unchanged, so readers keep using the last complete DB. * - publish_candidate_generation() writes metadata for the candidate in one * LMDB transaction. Only after that commit succeeds does it swap * active_generation and dbi under generation_lock. The old generation is * moved to retired_generations with retired_time set. * * Reader safety and reclamation: * - Decision lookups call trust_db_read_open(), which acquires the active * generation via trust_db_generation_acquire() before opening the LMDB read * transaction and cursor. trust_db_read_close() releases that reference. * - Retired generations remain open while their readers count is non-zero. * trust_db_reclaim_retired() drops the LMDB named database only after all * daemon-local readers drain. If drop fails, the retired generation is put * back on the list so it is retried later instead of being forgotten. * - CLI maintenance readers open their own MDB_RDONLY environment but still use * LMDB's reader table. They do not pin daemon generation structs, so LMDB * must keep their old transaction pages from being reused after reload. * * LMDB environment generations: * - Trust DB generations are logical content epochs inside one open LMDB * environment. They solve normal reload publication: new decisions can move * to a complete named database while old decisions finish on the previous * named database. * - LMDB environment generations are physical storage epochs. They advance * only when fapolicyd closes the live environment and reopens a replacement * data.mdb. This is intentionally rarer than trust DB generation publish * because the environment contains LMDB's page allocation history, free * list, reader table relationship, and map high-water mark. * - Offline rebuild uses backend memfd snapshots as the source of truth, * writes a complete replacement environment in a temporary directory, * validates that environment, and then swaps it during a controlled window * where decision readers are blocked and the live environment is closed. * If validation, rename, or reopen fails, the previous data.mdb is kept or * restored before readers are released. * - The two generation layers complement each other. Normal reloads should * stay on the logical trust DB generation path because it keeps the hot path * available. Offline environment replacement is for compaction and * high-water reset after LMDB allocation churn, not for ordinary reload * publication. * * Operator and QE signals: * - database_generation_snapshot() feeds the status and metrics headers with * the active trust DB generation and entry count. * - The same header also reports the active LMDB environment generation so an * operator can distinguish a normal trust-data publish from a physical * environment replacement. * - database_utilization_report() reports active pages, allocated high-water * pages, retired generation count, oldest retired age, and max reclaim * delay. Stable active pages with growing high-water pages point at LMDB * reload working-set pressure. Non-zero retired count or growing oldest age * means readers are holding old generations. Max reclaim delay shows the * worst observed reader drain delay. Manual db_max_size deployments also * get a resize recommendation here when the map is below the same safe * reload target that auto sizing would use. * - Auto sizing retries one MDB_MAP_FULL reload after growing the live map. * If an earlier bounded shrink target was too small for LMDB's current * allocation history, that emergency grow starts from the actual live map * size, not the stale configured target, and records a reload floor so later * live inspections do not immediately pick the same bad size again. * - database_metrics_report_reset() reports trust lookup count and reader-slot * exhaustion. Reader-slot exhaustion indicates max reader pressure, not a * generation leak. * * Sizing knobs: * - TRUST_DB_ACTIVE_TARGET_PERCENT targets steady-state active-page usage. * Lowering it uses more disk for ordinary growth headroom. * - TRUST_DB_RELOAD_WORK_FACTOR models how many active-sized copies must fit * during a full rebuild. It is 2 for current generation plus candidate. * Change it only if the reload algorithm starts keeping more or fewer full * copies live at once. * - TRUST_DB_RELOAD_HIGHWATER_PERCENT targets the bounded reload working set. * Lowering it grows the map and gives reload storms more room. Raising it * saves disk but increases MDB_MAP_FULL risk. Tune with repeated HUP reload * tests and watch allocated high-water pages converge. * - TRUST_DB_SHRINK_TRIGGER_PERCENT and TRUST_DB_SHRINK_HYSTERESIS_PERCENT * keep auto shrink conservative so the daemon does not oscillate map size * during reload churn. * - TRUST_DB_GENERATION_DB_SLOTS bounds reusable named databases. Increase it * only if tests show many held readers pin more retired generations than * available slots. TRUST_DB_MAX_NAMED_DBS must stay comfortably above the * slot count plus metadata and any legacy DB name. */ // Local variables static MDB_env *env; static MDB_dbi dbi; static MDB_dbi metadata_dbi; static int dbi_init = 0; static int metadata_dbi_init = 0; static unsigned int db_max_readers; static unsigned MDB_maxkeysize; static const char *data_dir = DB_DIR; static const char *db = DB_NAME; static int lib_symlink=0, lib64_symlink=0, bin_symlink=0, sbin_symlink=0; static unsigned int autosize_reload_floor_mb; static unsigned int startup_compaction_target_mb; static unsigned int startup_compaction_fallback_mb; /* * IMA mismatch logging policy: five LOG_ERR entries, five LOG_CRIT entries, * one silence notice, then suppression to protect syslog from floods. */ static unsigned int ima_mismatch_err_budget = 5; static unsigned int ima_mismatch_crit_budget = 5; static int ima_mismatch_silenced; static pthread_mutex_t generation_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t generation_reclaim_lock = PTHREAD_MUTEX_INITIALIZER; struct trust_db_read_handle { MDB_txn *txn; MDB_cursor *cursor; struct trust_db_generation *generation; }; struct trust_db_lookup { const char *path; struct file_info *info; int fd; int *error; }; struct trust_db_record_input { const char *idx; size_t idx_len; const char *data; }; struct trust_db_key { MDB_val val; char *hash; }; struct trust_db_metrics_snapshot { unsigned long lookups; unsigned long reader_slots_full; }; struct trust_db_metrics { atomic_ulong lookups; atomic_ulong reader_slots_full; }; static struct trust_db_metrics trust_metrics; static MDB_env *readonly_lookup_env; static MDB_dbi readonly_lookup_dbi; static int readonly_lookup_open; struct trust_db_sizing_state { size_t page_size; size_t map_pages; size_t active_pages; size_t allocated_pages; size_t reload_work_pages; size_t active_target_pages; size_t highwater_target_pages; size_t recommended_pages; unsigned int current_mb; unsigned int recommended_mb; unsigned long active_percent; unsigned long allocated_percent; size_t entries; }; struct trust_db_metadata { unsigned long generation; char name[TRUST_DB_GENERATION_NAME_SIZE]; long entries; time_t publish_time; }; struct trust_db_generation { unsigned long generation; char name[TRUST_DB_GENERATION_NAME_SIZE]; MDB_dbi handle; unsigned long readers; long entries; time_t publish_time; struct trust_db_sizing_state sizing; time_t retired_time; struct trust_db_generation *next; }; struct lmdb_environment_generation { unsigned long generation; time_t publish_time; }; struct offline_lmdb { char tmpdir[PATH_MAX]; MDB_env *env; MDB_dbi metadata_dbi; MDB_dbi trust_dbi; unsigned int maxkeysize; long entries; unsigned long trust_generation; unsigned long env_generation; }; static struct trust_db_generation *active_generation; static struct trust_db_generation *retired_generations; static unsigned long next_generation = 1; static unsigned long max_generation_reclaim_delay; static pthread_mutex_t lmdb_environment_lock = PTHREAD_MUTEX_INITIALIZER; static struct lmdb_environment_generation lmdb_environment = { .generation = 0, .publish_time = 0, }; /* * lmdb_record - Parsed representation of a single LMDB value payload. * @tsource: Trust source identifier stored alongside the record. * @size: Expected file size for integrity verification. * @digest: Hex encoded digest string extracted from the LMDB record. * @digest_len: Cached length of the @digest field for comparisons. * @alg: Inferred digest algorithm. RPM entries may carry multiple algorithms * while other backends default to SHA256 for backward compatibility. */ struct lmdb_record { unsigned int tsource; off_t size; char digest[FILE_DIGEST_STRING_MAX]; file_hash_alg_t alg; }; // Local functions static int update_database(conf_t *config, int startup_rebuild); static int write_db(const char *idx, size_t idx_len, const char *data) __attr_access ((__read_only__, 1, 2)) __wur; static int refresh_active_generation_metadata(void); static struct trust_db_generation *trust_db_generation_acquire(void); static void trust_db_generation_release(struct trust_db_generation *gen); static void trust_db_reclaim_retired(void); static int compact_trust_database(conf_t *config, const char *source_name); static int init_db_with_generations(const conf_t *config, unsigned long trust_generation, unsigned long env_generation); static void check_db_size(const conf_t *config); static int trust_db_record_from_line(char *buff, struct trust_db_record_input *record); static int trust_db_key_init_with_max(struct trust_db_key *key, const char *idx, size_t idx_len, unsigned int maxkeysize); static void trust_db_key_destroy(struct trust_db_key *key); static int do_memfd_update_to_dbi_in_env(MDB_env *target_env, int memfd, MDB_dbi target_dbi, unsigned int maxkeysize, long *entries); static void close_env(int do_close_dbi); static void log_lmdb_state(int priority, const char *context, int lmdb_rc); static void walk_database_reset(void); static int check_trust_database_with_read(struct trust_db_read_handle *read, const char *path, struct file_info *info, int fd); // External variables extern atomic_bool stop; static int is_link(const char *path) { int rc; struct stat sb; rc = lstat(path, &sb); if (rc == 0) { if (S_ISLNK(sb.st_mode)) return 1; } return 0; } const char *lookup_tsource(unsigned int tsource) { switch (tsource) { case SRC_RPM: return "rpmdb"; case SRC_DEB: return "debdb"; case SRC_FILE_DB: return "filedb"; } return "src_unknown"; } /* * database_set_location - Override LMDB environment directory and DB name. * @dir: Directory containing LMDB files. When NULL, use default DB_DIR. * @name: Logical LMDB database name. When NULL, use default DB_NAME. * * Returns 0 when values were accepted, or 1 when either argument is empty. */ int database_set_location(const char *dir, const char *name) { if (dir && dir[0] == '\0') return 1; if (name && name[0] == '\0') return 1; data_dir = dir ? dir : DB_DIR; db = name ? name : DB_NAME; if (env == NULL) { pthread_mutex_lock(&generation_lock); next_generation = 1; max_generation_reclaim_delay = 0; pthread_mutex_unlock(&generation_lock); pthread_mutex_lock(&lmdb_environment_lock); lmdb_environment.generation = 0; lmdb_environment.publish_time = 0; pthread_mutex_unlock(&lmdb_environment_lock); } return 0; } unsigned get_default_db_max_size(void) { return DEFAULT_DB_MAX_SIZE_MB; /* 100 MiB baseline */ } /* * configured_reader_limit - compute LMDB reader slots to reserve. * @config: active daemon configuration. * * Returns the number of LMDB reader slots requested for the environment. */ static unsigned int configured_reader_limit(const conf_t *config) { return daemon_config_lmdb_reader_limit(config); } /* * trust_metric_add - add to an unsigned long metric. * @counter: metric counter to update. * @value: amount to add. * * Returns nothing. */ static void trust_metric_add(atomic_ulong *counter, unsigned long value) { atomic_fetch_add_explicit(counter, value, memory_order_relaxed); } /* * lmdb_environment_publish - update the physical LMDB environment epoch. * @generation: generation number to report. * * Trust DB generations describe logical content publication. This environment * generation describes the currently opened data.mdb file. It starts at one * for a daemon run and advances only after controlled environment replacement. */ static void lmdb_environment_publish(unsigned long generation) { pthread_mutex_lock(&lmdb_environment_lock); lmdb_environment.generation = generation; lmdb_environment.publish_time = time(NULL); pthread_mutex_unlock(&lmdb_environment_lock); } /* * lmdb_environment_next_generation - return the next environment epoch. * * The caller uses this before closing the old environment so the replacement * reopen can publish a monotonic physical generation after it succeeds. */ static unsigned long lmdb_environment_next_generation(void) { unsigned long generation; pthread_mutex_lock(&lmdb_environment_lock); generation = lmdb_environment.generation + 1; if (generation == 0) generation = 1; pthread_mutex_unlock(&lmdb_environment_lock); return generation; } /* * lmdb_environment_snapshot - copy the current physical environment epoch. * @generation: destination generation value. * @publish_time: destination effective timestamp. */ static void lmdb_environment_snapshot(unsigned long *generation, time_t *publish_time) { pthread_mutex_lock(&lmdb_environment_lock); if (generation) *generation = lmdb_environment.generation; if (publish_time) *publish_time = lmdb_environment.publish_time; pthread_mutex_unlock(&lmdb_environment_lock); } /* * trust_db_record_reader_error - count full LMDB reader slot table errors. * @rc: LMDB return code from a read transaction open. * * Returns nothing. */ static void trust_db_record_reader_error(int rc) { if (rc == MDB_READERS_FULL) trust_metric_add(&trust_metrics.reader_slots_full, 1); } /* * pages_for_percent - calculate map pages needed for a utilization target. * @pages: pages that must fit in the map. * @percent: target utilization percentage. * * Returns the number of map pages needed so @pages occupies no more than * @percent of the map, rounded up. */ static size_t pages_for_percent(size_t pages, unsigned int percent) { if (pages == 0 || percent == 0) return 0; return ((pages * 100) + percent - 1) / percent; } /* * pages_times - multiply page counts without wrapping. * @pages: base page count. * @factor: integer multiplier. * * Returns the product, or the largest size_t value if it would overflow. */ static size_t pages_times(size_t pages, unsigned int factor) { if (factor != 0 && pages > ((size_t)-1) / factor) return (size_t)-1; return pages * factor; } /* * percent_of - calculate an integer percentage. * @used: numerator value. * @total: denominator value. * * Returns @used as a percentage of @total, or 0 when @total is 0. */ static unsigned long percent_of(size_t used, size_t total) { if (total == 0) return 0; return (100 * used) / total; } /* * bytes_to_mb - convert bytes to whole MiB for db_max_size. * @bytes: byte count to convert. * * Returns a ceil-rounded MiB value clamped to unsigned int. */ static unsigned int bytes_to_mb(size_t bytes) { size_t mb = (bytes + MEGABYTE - 1) / MEGABYTE; if (mb > UINT_MAX) return UINT_MAX; if (mb == 0) return 1; return mb; } /* * pages_to_mb - convert LMDB page count to whole MiB. * @pages: LMDB pages. * @page_size: LMDB page size in bytes. * * Returns a ceil-rounded MiB value clamped to unsigned int. */ static unsigned int pages_to_mb(size_t pages, size_t page_size) { size_t max_bytes = (size_t)UINT_MAX * MEGABYTE; if (page_size == 0) page_size = 4096; if (pages > max_bytes / page_size) return UINT_MAX; return bytes_to_mb(pages * page_size); } /* * complete_lmdb_sizing_state - derive autosize targets from raw LMDB state. * @state: sizing state with raw page counts already populated. * * Returns nothing. */ static void complete_lmdb_sizing_state(struct trust_db_sizing_state *state) { /* * me_last_pgno is LMDB's file high-water mark. It is useful for * seeing that reload churn has touched most of the map, but it is not * the same as live trust data. If auto sizing chases that monotonic * high-water mark directly, every drop/rebuild can make the next * target slightly larger even when the backend entry set is unchanged. * * Generation publication keeps the active DB live while a candidate * named DB is populated. Size reload work for both copies. This gives * the candidate room to complete without turning old file high-water * growth into permanent map growth. */ state->reload_work_pages = pages_times(state->active_pages, TRUST_DB_RELOAD_WORK_FACTOR); if (state->reload_work_pages < state->active_pages) state->reload_work_pages = state->active_pages; state->active_target_pages = pages_for_percent(state->active_pages, TRUST_DB_ACTIVE_TARGET_PERCENT); state->highwater_target_pages = pages_for_percent(state->reload_work_pages, TRUST_DB_RELOAD_HIGHWATER_PERCENT); state->recommended_pages = state->active_target_pages; if (state->highwater_target_pages > state->recommended_pages) state->recommended_pages = state->highwater_target_pages; state->recommended_pages++; state->recommended_mb = pages_to_mb(state->recommended_pages, state->page_size); state->active_percent = percent_of(state->active_pages, state->map_pages); state->allocated_percent = percent_of(state->allocated_pages, state->map_pages); } /* * fill_lmdb_sizing_state - collect named DB and map sizing details. * @txn: read transaction for the environment. * @sizing_dbi: named database handle to inspect. * @info: environment info already read from the same environment. * @state: destination sizing state. * * Returns 0 on success or an LMDB error code. */ static int fill_lmdb_sizing_state(MDB_txn *txn, MDB_dbi sizing_dbi, const MDB_envinfo *info, struct trust_db_sizing_state *state) { MDB_stat stat; int rc; memset(state, 0, sizeof(*state)); rc = mdb_stat(txn, sizing_dbi, &stat); if (rc) return rc; state->page_size = stat.ms_psize ? stat.ms_psize : 4096; state->map_pages = info->me_mapsize / state->page_size; state->allocated_pages = info->me_last_pgno + 1; state->active_pages = stat.ms_branch_pages + stat.ms_leaf_pages + stat.ms_overflow_pages; state->entries = stat.ms_entries; state->current_mb = bytes_to_mb(info->me_mapsize); complete_lmdb_sizing_state(state); return 0; } /* * autosize_effective_target_mb - apply any runtime reload safety floor. * @state: sizing state with computed recommendation. * @mode: caller context for startup, live inspection, or reload preflight. * * Returns the MiB target after honoring recent reload growth when appropriate. */ static unsigned int autosize_effective_target_mb( const struct trust_db_sizing_state *state, enum autosize_plan_mode mode) { unsigned int target_mb = state->recommended_mb; if (mode != AUTOSIZE_STARTUP_INSPECTION && autosize_reload_floor_mb && target_mb < autosize_reload_floor_mb) target_mb = autosize_reload_floor_mb; return target_mb; } /* * autosize_shrink_allowed - decide whether auto mode may reduce map size. * @config: active daemon configuration. * @state: LMDB sizing state from the environment being considered. * @target_mb: proposed new map size after any reload safety floor. * @mode: caller context for startup, live inspection, or reload preflight. * * Shrink is intentionally conservative. The final trust DB is usually stable, * but reload is not an in-place no-op: fapolicyd drops the named database and * reinserts records from backend snapshots. LMDB must commit copy-on-write * metadata and freelist changes before old pages are reusable. If we shrink * as soon as active pages dip below the steady-state target, a reload storm * can push the allocated high-water mark back to MDB_MAP_FULL. Require both * active pages and allocated pages to be comfortably low, and require the * proposed shrink to be large enough to avoid map-size oscillation. Startup * is the exception for allocated high-water pages: after a daemon restart * there are no old fapolicyd reader transactions to pin deleted pages, so * startup compaction may use the bounded rebuild working set instead of * preserving a stale file high-water mark forever. * * Returns 1 when shrink is allowed, 0 otherwise. */ static int autosize_shrink_allowed(const conf_t *config, const struct trust_db_sizing_state *state, unsigned int target_mb, enum autosize_plan_mode mode) { if (target_mb >= config->db_max_size) return 0; if (state->active_percent >= TRUST_DB_SHRINK_TRIGGER_PERCENT) return 0; if (mode != AUTOSIZE_STARTUP_INSPECTION && state->allocated_percent >= TRUST_DB_RELOAD_HIGHWATER_PERCENT) return 0; if ((unsigned long long)target_mb * 100 > (unsigned long long)config->db_max_size * TRUST_DB_SHRINK_HYSTERESIS_PERCENT) return 0; return 1; } /* * apply_autosize_plan - update config->db_max_size from LMDB sizing state. * @config: active daemon configuration. * @state: LMDB sizing state from either the live or on-disk environment. * @mode: caller context for startup, live inspection, or reload preflight. * @context: short log label describing the caller. * * Auto sizing has two targets. The active-page target keeps the final trust * database near 75 percent utilization so normal package changes have room. * The reload target keeps a bounded copy-on-write working set below 80 * percent so a full drop/rebuild reload has room to commit metadata before * old pages are reusable. Manual configurations are not changed here; callers * should log the recommended size for administrators. * * Returns 1 when config->db_max_size changed, 0 otherwise. */ static int apply_autosize_plan(conf_t *config, const struct trust_db_sizing_state *state, enum autosize_plan_mode mode, const char *context) { unsigned int target_mb = autosize_effective_target_mb(state, mode); if (state->active_pages == 0) { msg(LOG_INFO, "autosize: empty DB during %s - keeping %u MiB", context, config->db_max_size); return 0; } if (target_mb > config->db_max_size) { msg(LOG_INFO, "autosize: %s growing map %u->%u MiB " "(entries=%zu active=%zu/%zu pages %lu%%, " "allocated=%zu/%zu pages %lu%%, reload_work=%zu)", context, config->db_max_size, target_mb, state->entries, state->active_pages, state->map_pages, state->active_percent, state->allocated_pages, state->map_pages, state->allocated_percent, state->reload_work_pages); config->db_max_size = target_mb; return 1; } if (mode == AUTOSIZE_RELOAD_PREFLIGHT || !autosize_shrink_allowed(config, state, target_mb, mode)) { msg(LOG_INFO, "autosize: %s keeping %u MiB " "(target=%u MiB active=%lu%% allocated=%lu%%)", context, config->db_max_size, target_mb, state->active_percent, state->allocated_percent); return 0; } msg(LOG_INFO, "autosize: %s shrinking map %u->%u MiB " "(entries=%zu active=%zu/%zu pages %lu%%, " "allocated=%zu/%zu pages %lu%%, reload_work=%zu)", context, config->db_max_size, target_mb, state->entries, state->active_pages, state->map_pages, state->active_percent, state->allocated_pages, state->map_pages, state->allocated_percent, state->reload_work_pages); config->db_max_size = target_mb; return 1; } /* * read_live_lmdb_sizing_state - inspect the currently open LMDB environment. * @state: destination sizing state. * * Returns 0 on success or an LMDB error code. */ static int read_live_lmdb_sizing_state(struct trust_db_sizing_state *state) { MDB_envinfo info; MDB_txn *txn = NULL; struct trust_db_generation *gen; int rc; if (env == NULL) return EINVAL; rc = mdb_env_info(env, &info); if (rc) return rc; gen = trust_db_generation_acquire(); if (gen == NULL) return EINVAL; rc = mdb_txn_begin(env, NULL, MDB_RDONLY, &txn); if (rc) { trust_db_generation_release(gen); return rc; } rc = fill_lmdb_sizing_state(txn, gen->handle, &info, state); mdb_txn_abort(txn); trust_db_generation_release(gen); return rc; } /* * read_existing_lmdb_sizing_state - inspect the on-disk LMDB environment. * @state: destination sizing state. * * This is used before applying a resize to the live environment. The read-only * handle avoids LMDB lockfile mutexes because autosize only needs sizing * metadata and must not interfere with the daemon's active environment. * * Returns 0 on success or an LMDB error code. */ static int read_existing_lmdb_sizing_state(struct trust_db_sizing_state *state) { MDB_env *tmp_env = NULL; MDB_envinfo info; MDB_txn *txn = NULL; MDB_dbi dbi_tmp; char active_name[TRUST_DB_GENERATION_NAME_SIZE]; int rc; rc = mdb_env_create(&tmp_env); if (rc) return rc; rc = mdb_env_set_maxdbs(tmp_env, TRUST_DB_MAX_NAMED_DBS); if (rc) goto out_close; rc = mdb_env_open(tmp_env, data_dir, MDB_RDONLY|MDB_NOLOCK, 0); if (rc) goto out_close; rc = mdb_env_info(tmp_env, &info); if (rc) goto out_close; rc = mdb_txn_begin(tmp_env, NULL, MDB_RDONLY, &txn); if (rc) goto out_close; rc = database_read_active_name(txn, active_name, sizeof(active_name)); if (rc) goto out_abort; rc = mdb_dbi_open(txn, active_name, 0, &dbi_tmp); if (rc) goto out_abort; rc = fill_lmdb_sizing_state(txn, dbi_tmp, &info, state); out_abort: mdb_txn_abort(txn); out_close: mdb_env_close(tmp_env); return rc; } /* * trust_db_generation_alloc - create in-memory state for one named DB. * @generation: Monotonic generation number associated with the DB. * @name: LMDB named database that stores this generation's trust records. * * Returns a new generation object, or NULL on allocation/name errors. */ static struct trust_db_generation *trust_db_generation_alloc( unsigned long generation, const char *name) { struct trust_db_generation *gen; if (name == NULL || name[0] == 0 || strlen(name) >= TRUST_DB_GENERATION_NAME_SIZE) return NULL; gen = calloc(1, sizeof(*gen)); if (gen == NULL) return NULL; gen->generation = generation; snprintf(gen->name, sizeof(gen->name), "%s", name); return gen; } /* * trust_db_generation_slot_name - format one reusable LMDB generation slot. * @slot: Bounded slot number to include in the name. * @name: Destination buffer. * * Returns 0 on success or ENAMETOOLONG. */ static int trust_db_generation_slot_name(unsigned int slot, char name[TRUST_DB_GENERATION_NAME_SIZE]) { int len; len = snprintf(name, TRUST_DB_GENERATION_NAME_SIZE, "%s_%u", TRUST_DB_GENERATION_SLOT_PREFIX, slot); if (len < 0 || len >= TRUST_DB_GENERATION_NAME_SIZE) return ENAMETOOLONG; return 0; } /* * trust_db_generation_name_in_use - check active/retired generation names. * @name: Candidate LMDB named database. * * generation_lock must be held by the caller. * * Returns 1 when an active or retired reader may still reference @name. */ static int trust_db_generation_name_in_use(const char *name) { struct trust_db_generation *gen; if (active_generation && strcmp(active_generation->name, name) == 0) return 1; for (gen = retired_generations; gen; gen = gen->next) { if (strcmp(gen->name, name) == 0) return 1; } return 0; } /* * trust_db_candidate_name - choose a reusable LMDB named DB slot. * @name: Destination buffer. * * generation_lock must be held by the caller. * * Returns 0 on success, EBUSY if every bounded slot is pinned by an active or * retired generation, or a formatting error. */ static int trust_db_candidate_name(char name[TRUST_DB_GENERATION_NAME_SIZE]) { char slot_name[TRUST_DB_GENERATION_NAME_SIZE]; unsigned int slot; int rc; for (slot = 0; slot < TRUST_DB_GENERATION_DB_SLOTS; slot++) { rc = trust_db_generation_slot_name(slot, slot_name); if (rc) return rc; if (trust_db_generation_name_in_use(slot_name)) continue; snprintf(name, TRUST_DB_GENERATION_NAME_SIZE, "%s", slot_name); return 0; } return EBUSY; } /* * trust_db_metadata_parse - parse the current-generation metadata value. * @data: NUL-terminated metadata value. * @metadata: Destination metadata. * * Returns 0 when at least generation and DB name were parsed. */ static int trust_db_metadata_parse(const char *data, struct trust_db_metadata *metadata) { char *copy, *line, *save = NULL; int have_generation = 0, have_name = 0; memset(metadata, 0, sizeof(*metadata)); copy = strdup(data); if (copy == NULL) return 1; for (line = strtok_r(copy, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) { if (sscanf(line, "generation=%lu", &metadata->generation) == 1) { have_generation = 1; } else if (strncmp(line, "name=", 5) == 0) { snprintf(metadata->name, sizeof(metadata->name), "%s", line + 5); have_name = metadata->name[0] != 0; } else if (sscanf(line, "entries=%ld", &metadata->entries) == 1) { continue; } else { long long publish_time; if (sscanf(line, "publish_time=%lld", &publish_time) == 1) metadata->publish_time = (time_t)publish_time; } } free(copy); return !(have_generation && have_name); } /* * trust_db_metadata_read_from_dbi - read current-generation metadata. * @txn: active transaction. * @source_dbi: metadata DBI in the same environment as @txn. * @metadata: destination metadata. * * Offline rebuild validation cannot use the daemon globals because it opens a * temporary LMDB environment beside the live one. Keep the metadata parser * shared and pass the DBI explicitly so live and offline readers validate the * same publication record. */ static int trust_db_metadata_read_from_dbi(MDB_txn *txn, MDB_dbi source_dbi, struct trust_db_metadata *metadata) { MDB_val key, value; char data[BUFFER_SIZE]; int rc; key.mv_data = (void *)TRUST_DB_METADATA_KEY; key.mv_size = sizeof(TRUST_DB_METADATA_KEY) - 1; rc = mdb_get(txn, source_dbi, &key, &value); if (rc) return rc; if (value.mv_size >= sizeof(data)) return EINVAL; memcpy(data, value.mv_data, value.mv_size); data[value.mv_size] = 0; if (trust_db_metadata_parse(data, metadata)) return EINVAL; return 0; } /* * database_read_active_name - read the currently published trust DB name. * @txn: read transaction for an LMDB environment containing trust metadata. * @name: destination buffer. * @name_size: size of @name. * * Older trust databases did not have generation metadata, so missing metadata * falls back to the default DB name. Present but unreadable metadata is an * error because otherwise callers may inspect a stale generation. */ int database_read_active_name(MDB_txn *txn, char *name, size_t name_size) { MDB_dbi active_metadata_dbi; struct trust_db_metadata metadata; int len; int rc; if (name_size == 0) return EINVAL; len = snprintf(name, name_size, "%s", db); if (len < 0 || len >= (int)name_size) return ENAMETOOLONG; rc = mdb_dbi_open(txn, TRUST_DB_METADATA_NAME, 0, &active_metadata_dbi); if (rc == MDB_NOTFOUND) return 0; if (rc) return rc; rc = trust_db_metadata_read_from_dbi(txn, active_metadata_dbi, &metadata); if (rc == MDB_NOTFOUND) return 0; if (rc) return rc; len = snprintf(name, name_size, "%s", metadata.name); if (len < 0 || len >= (int)name_size) return ENAMETOOLONG; return 0; } /* * trust_db_metadata_read - read current-generation metadata from live LMDB. * @txn: Active transaction. * @metadata: Destination metadata. * * Returns 0 on success, MDB_NOTFOUND when metadata is absent, or an LMDB/ * parse error code. */ static int trust_db_metadata_read(MDB_txn *txn, struct trust_db_metadata *metadata) { if (!metadata_dbi_init) return EINVAL; return trust_db_metadata_read_from_dbi(txn, metadata_dbi, metadata); } /* * trust_db_metadata_write_to_env - persist publication metadata. * @target_env: LMDB environment containing @txn. * @target_metadata_dbi: metadata DBI in @target_env. * @txn: Writable transaction that makes the metadata visible atomically. * @gen: Generation being published or refreshed. * * Returns 0 on success or an LMDB/formatting error. */ static int trust_db_metadata_write_to_env(MDB_env *target_env, MDB_dbi target_metadata_dbi, MDB_txn *txn, struct trust_db_generation *gen) { MDB_envinfo info; MDB_val key, value; char data[BUFFER_SIZE]; int len, rc; rc = mdb_env_info(target_env, &info); if (rc) return rc; rc = fill_lmdb_sizing_state(txn, gen->handle, &info, &gen->sizing); if (rc) return rc; gen->entries = (long)gen->sizing.entries; if (gen->publish_time == 0) gen->publish_time = time(NULL); len = snprintf(data, sizeof(data), "generation=%lu\n" "name=%s\n" "entries=%ld\n" "publish_time=%lld\n" "page_size=%zu\n" "map_pages=%zu\n" "active_pages=%zu\n" "allocated_pages=%zu\n" "reload_work_pages=%zu\n" "recommended_pages=%zu\n", gen->generation, gen->name, gen->entries, (long long)gen->publish_time, gen->sizing.page_size, gen->sizing.map_pages, gen->sizing.active_pages, gen->sizing.allocated_pages, gen->sizing.reload_work_pages, gen->sizing.recommended_pages); if (len < 0 || len >= (int)sizeof(data)) return ENAMETOOLONG; key.mv_data = (void *)TRUST_DB_METADATA_KEY; key.mv_size = sizeof(TRUST_DB_METADATA_KEY) - 1; value.mv_data = data; value.mv_size = len; return mdb_put(txn, target_metadata_dbi, &key, &value, 0); } /* * trust_db_metadata_write - persist live current-generation metadata. * @txn: writable transaction that makes the metadata visible atomically. * @gen: generation being published or refreshed. * * Returns 0 on success or an LMDB/formatting error. */ static int trust_db_metadata_write(MDB_txn *txn, struct trust_db_generation *gen) { return trust_db_metadata_write_to_env(env, metadata_dbi, txn, gen); } /* * trust_db_generation_acquire - pin the current trust DB generation. * * Returns the active generation with its reader count incremented, or NULL * when no trust DB has been initialized. */ static struct trust_db_generation *trust_db_generation_acquire(void) { struct trust_db_generation *gen; pthread_mutex_lock(&generation_lock); gen = active_generation; if (gen) gen->readers++; pthread_mutex_unlock(&generation_lock); return gen; } /* * trust_db_generation_release - release a pinned trust DB generation. * @gen: Generation returned by trust_db_generation_acquire(). * * Returns nothing. */ static void trust_db_generation_release(struct trust_db_generation *gen) { if (gen == NULL) return; pthread_mutex_lock(&generation_lock); if (gen->readers) gen->readers--; pthread_mutex_unlock(&generation_lock); /* * FIXME: Reclaim can block in an LMDB write transaction while a * decision lookup still holds the update read lock. Investigate * handing retired generations to the update thread; an empty-list * fast path alone does not prevent a blocking database drop. */ trust_db_reclaim_retired(); } /* * drop_generation_database - delete a retired named DB from LMDB. * @gen: Retired generation with no readers. * * Returns 0 on success or an LMDB error code. */ static int drop_generation_database(struct trust_db_generation *gen) { MDB_txn *txn = NULL; int rc; rc = mdb_txn_begin(env, NULL, 0, &txn); if (rc) return rc; rc = mdb_drop(txn, gen->handle, 1); if (rc) { mdb_txn_abort(txn); return rc; } rc = mdb_txn_commit(txn); if (rc) return rc; return 0; } /* * trust_db_reclaim_retired - drop retired generations that have no readers. * * Returns nothing. Failed drops are left on the retired list for a later * attempt so old generations are never forgotten before LMDB deletes them. */ static void trust_db_reclaim_retired(void) { struct trust_db_generation *gen, **link; time_t now; int rc; if (env == NULL) return; pthread_mutex_lock(&generation_reclaim_lock); for (;;) { gen = NULL; pthread_mutex_lock(&generation_lock); for (link = &retired_generations; *link; link = &(*link)->next) { if ((*link)->readers == 0) { gen = *link; *link = gen->next; gen->next = NULL; break; } } pthread_mutex_unlock(&generation_lock); if (gen == NULL) break; rc = drop_generation_database(gen); if (rc) { msg(LOG_WARNING, "Could not reclaim retired trust DB generation %lu (%s): %s", gen->generation, gen->name, mdb_strerror(rc)); pthread_mutex_lock(&generation_lock); gen->next = retired_generations; retired_generations = gen; pthread_mutex_unlock(&generation_lock); break; } now = time(NULL); if (gen->retired_time && now >= gen->retired_time) { unsigned long delay = now - gen->retired_time; pthread_mutex_lock(&generation_lock); if (delay > max_generation_reclaim_delay) max_generation_reclaim_delay = delay; pthread_mutex_unlock(&generation_lock); msg(LOG_INFO, "Reclaimed retired trust DB generation %lu (%s) after %lu seconds", gen->generation, gen->name, delay); } free(gen); } pthread_mutex_unlock(&generation_reclaim_lock); } /* * trust_db_reset_next_generation - reset the runtime publication counter. * @generation: Next generation number to publish. * * Trust DB metadata persists the LMDB named database, but the reported * generation is a daemon-runtime epoch like ruleset and config generations. * Startup rebuilds use this to publish generation 1 regardless of the prior * daemon's last persisted value. * * Returns nothing. */ static void trust_db_reset_next_generation(unsigned long generation) { pthread_mutex_lock(&generation_lock); next_generation = generation; pthread_mutex_unlock(&generation_lock); } /* * trust_db_generation_report_snapshot - copy publication/reclamation state. * @report: Destination report snapshot. * * Returns nothing. */ static void trust_db_generation_report_snapshot( database_generation_report_t *report) { struct trust_db_generation *gen; time_t now = time(NULL); memset(report, 0, sizeof(*report)); pthread_mutex_lock(&generation_lock); if (active_generation) { report->generation = active_generation->generation; report->entries = active_generation->entries; report->publish_time = active_generation->publish_time; } for (gen = retired_generations; gen; gen = gen->next) { unsigned long age = 0; report->retired_count++; if (gen->retired_time && now >= gen->retired_time) age = now - gen->retired_time; if (age > report->oldest_retired_age) report->oldest_retired_age = age; } report->max_reclaim_delay = max_generation_reclaim_delay; pthread_mutex_unlock(&generation_lock); lmdb_environment_snapshot(&report->lmdb_generation, &report->lmdb_publish_time); } /* * dir_file_path - format a child path below a directory. * @buf: destination buffer. * @buf_size: destination buffer size. * @dir: parent directory. * @name: file name below @dir. * * Returns 0 on success or ENAMETOOLONG. */ static int dir_file_path(char *buf, size_t buf_size, const char *dir, const char *name) { int written; written = snprintf(buf, buf_size, "%s/%s", dir, name); if (written < 0 || (size_t)written >= buf_size) return ENAMETOOLONG; return 0; } /* * lmdb_file_path - format a path below the LMDB environment directory. * @buf: destination buffer. * @buf_size: destination buffer size. * @name: file name below data_dir. * * Returns 0 on success or ENAMETOOLONG. */ static int lmdb_file_path(char *buf, size_t buf_size, const char *name) { return dir_file_path(buf, buf_size, data_dir, name); } /* * remove_lmdb_environment_dir - remove temporary LMDB files and directory. * @dir: temporary directory created by mkdtemp(). * * This cleanup is deliberately narrow. Offline rebuild temporary directories * should only contain LMDB's data and lock files; anything else is left behind * so an unexpected path does not get silently deleted. */ static void remove_lmdb_environment_dir(const char *dir) { char path[PATH_MAX]; if (dir == NULL || dir[0] == 0) return; if (dir_file_path(path, sizeof(path), dir, "data.mdb") == 0) unlink(path); if (dir_file_path(path, sizeof(path), dir, "lock.mdb") == 0) unlink(path); rmdir(dir); } /* * trust_db_generation_readers_active - check if environment replacement waits. * @reason: optional text destination for a human-readable blocker. * @reason_size: size of @reason. * * The update rwlock blocks normal decision reads before a controlled swap, but * this check catches maintenance/test references and unreclaimed retired named * DBs. Closing an LMDB environment while a generation can still be referenced * would turn a logical generation safety feature into a use-after-close bug. * * Returns 1 when the environment must not be replaced, 0 when it is quiescent. */ static int trust_db_generation_readers_active(char *reason, size_t reason_size) { struct trust_db_generation *gen; int busy = 0; pthread_mutex_lock(&generation_lock); if (active_generation && active_generation->readers) { busy = 1; if (reason && reason_size) snprintf(reason, reason_size, "active generation has %lu readers", active_generation->readers); } else if (retired_generations) { busy = 1; gen = retired_generations; if (reason && reason_size) snprintf(reason, reason_size, "retired generation %lu is still pinned or unreclaimed", gen->generation); } pthread_mutex_unlock(&generation_lock); return busy; } /* * trust_db_current_generation_number - read active logical generation number. * * The controlled swap saves this before close_env() forgets in-memory handles. * If restoring the old environment is needed, reports continue to show the * same trust DB generation instead of appearing to publish new trust content. */ static unsigned long trust_db_current_generation_number(void) { unsigned long generation = 1; pthread_mutex_lock(&generation_lock); if (active_generation) generation = active_generation->generation; pthread_mutex_unlock(&generation_lock); return generation; } /* * trust_db_next_generation_number - reserve the next logical generation id. * * Offline environment replacement publishes a complete trust database built * from current backend snapshots. In a live daemon that is a real logical * trust DB publish, so it consumes the same monotonic generation sequence used * by named-DB reload publication. */ static unsigned long trust_db_next_generation_number(void) { unsigned long generation; pthread_mutex_lock(&generation_lock); generation = next_generation; if (generation == 0) generation = 1; pthread_mutex_unlock(&generation_lock); return generation; } /* * open_offline_lmdb - create a temporary replacement LMDB environment. * @candidate: replacement environment state to initialize. * @config: map/read sizing source. * * The replacement is built beside the live environment so data.mdb can be * renamed into place. It starts with one logical trust DB named slot 0; after * publication, later normal reloads can rotate through the usual slots. * * Returns 0 on success or an errno/LMDB error code. */ static int open_offline_lmdb(struct offline_lmdb *candidate, const conf_t *config) __nonnull ((1, 2)); static int open_offline_lmdb(struct offline_lmdb *candidate, const conf_t *config) { MDB_txn *txn = NULL; struct trust_db_generation gen; char name[TRUST_DB_GENERATION_NAME_SIZE]; unsigned int flags = MDB_MAPASYNC|MDB_NOSYNC; int rc; #ifndef DEBUG flags |= MDB_WRITEMAP; #endif memset(candidate->tmpdir, 0, sizeof(candidate->tmpdir)); candidate->env = NULL; candidate->metadata_dbi = 0; candidate->trust_dbi = 0; candidate->maxkeysize = 0; candidate->entries = 0; rc = lmdb_file_path(candidate->tmpdir, sizeof(candidate->tmpdir), ".offline-rebuild.XXXXXX"); if (rc) return rc; if (mkdtemp(candidate->tmpdir) == NULL) return errno; rc = trust_db_generation_slot_name(0, name); if (rc) goto out_remove; memset(&gen, 0, sizeof(gen)); gen.generation = candidate->trust_generation; snprintf(gen.name, sizeof(gen.name), "%s", name); rc = mdb_env_create(&candidate->env); if (rc) goto out_remove; rc = mdb_env_set_maxdbs(candidate->env, TRUST_DB_MAX_NAMED_DBS); if (rc) goto out_close; rc = mdb_env_set_mapsize(candidate->env, (size_t)config->db_max_size * MEGABYTE); if (rc) goto out_close; rc = mdb_env_set_maxreaders(candidate->env, configured_reader_limit(config)); if (rc) goto out_close; rc = mdb_env_open(candidate->env, candidate->tmpdir, flags, 0660); if (rc) goto out_close; candidate->maxkeysize = mdb_env_get_maxkeysize(candidate->env); rc = mdb_txn_begin(candidate->env, NULL, 0, &txn); if (rc) goto out_close; rc = mdb_dbi_open(txn, TRUST_DB_METADATA_NAME, MDB_CREATE, &candidate->metadata_dbi); if (rc) goto out_abort; rc = mdb_dbi_open(txn, gen.name, MDB_CREATE|MDB_DUPSORT, &candidate->trust_dbi); if (rc) goto out_abort; rc = mdb_txn_commit(txn); txn = NULL; if (rc) goto out_close; return 0; out_abort: mdb_txn_abort(txn); out_close: if (candidate->env) { mdb_env_close(candidate->env); candidate->env = NULL; } out_remove: remove_lmdb_environment_dir(candidate->tmpdir); candidate->tmpdir[0] = 0; return rc; } /* * write_offline_metadata - write replacement environment publication metadata. * @candidate: populated replacement environment. * * Returns 0 on success or an LMDB error code. */ static int write_offline_metadata(struct offline_lmdb *candidate) __nonnull ((1)); static int write_offline_metadata(struct offline_lmdb *candidate) { MDB_txn *txn = NULL; struct trust_db_generation gen; char name[TRUST_DB_GENERATION_NAME_SIZE]; int rc; rc = trust_db_generation_slot_name(0, name); if (rc) return rc; memset(&gen, 0, sizeof(gen)); gen.generation = candidate->trust_generation; snprintf(gen.name, sizeof(gen.name), "%s", name); gen.handle = candidate->trust_dbi; gen.publish_time = time(NULL); rc = mdb_txn_begin(candidate->env, NULL, 0, &txn); if (rc) return rc; rc = trust_db_metadata_write_to_env(candidate->env, candidate->metadata_dbi, txn, &gen); if (rc) { mdb_txn_abort(txn); return rc; } candidate->entries = gen.entries; return mdb_txn_commit(txn); } /* * import_backends_to_offline_lmdb - copy backend snapshots into temp LMDB. * @candidate: replacement environment being populated. * * Returns 0 on success or a write/import error code. */ static int import_backends_to_offline_lmdb(struct offline_lmdb *candidate) { long entries; int rc; for (backend_entry *be = backend_get_first(); be != NULL && !stop; be = be->next) { msg(LOG_INFO, "Loading trust data from %s backend", be->backend->name); if (be->backend->memfd == -1) { msg(LOG_ERR, "%s backend does not provide a memfd snapshot", be->backend->name); return EINVAL; } rc = do_memfd_update_to_dbi_in_env(candidate->env, be->backend->memfd, candidate->trust_dbi, candidate->maxkeysize, &entries); if (rc) { msg(LOG_ERR, "Failed to import trust data from %s backend", be->backend->name); return rc; } be->backend->entries = entries; candidate->entries += entries; } if (stop) return EINTR; return 0; } /* * validate_offline_lmdb - validate replacement LMDB before publication. * @candidate: closed replacement environment. * * MVP validation is intentionally structural. Reopen the candidate read-only, * verify that publication metadata names an existing DUPSORT trust DB, and * compare LMDB's entry count with the backend import count. Full record-by- * record checking remains the normal check-trustdb responsibility. */ static int validate_offline_lmdb(const struct offline_lmdb *candidate) __nonnull ((1)); static int validate_offline_lmdb(const struct offline_lmdb *candidate) { MDB_env *validate_env = NULL; MDB_txn *txn = NULL; MDB_dbi validate_metadata_dbi; MDB_dbi validate_trust_dbi; MDB_stat stat; struct trust_db_metadata metadata; int rc; rc = mdb_env_create(&validate_env); if (rc) return rc; rc = mdb_env_set_maxdbs(validate_env, TRUST_DB_MAX_NAMED_DBS); if (rc) goto out_close; rc = mdb_env_open(validate_env, candidate->tmpdir, MDB_RDONLY|MDB_NOLOCK, 0); if (rc) goto out_close; rc = mdb_txn_begin(validate_env, NULL, MDB_RDONLY, &txn); if (rc) goto out_close; rc = mdb_dbi_open(txn, TRUST_DB_METADATA_NAME, 0, &validate_metadata_dbi); if (rc) goto out_abort; rc = trust_db_metadata_read_from_dbi(txn, validate_metadata_dbi, &metadata); if (rc) goto out_abort; rc = mdb_dbi_open(txn, metadata.name, MDB_DUPSORT, &validate_trust_dbi); if (rc) goto out_abort; rc = mdb_stat(txn, validate_trust_dbi, &stat); if (rc) goto out_abort; if ((long)stat.ms_entries != candidate->entries || metadata.entries != candidate->entries) { msg(LOG_ERR, "Rebuilt trust DB validation count mismatch: stat=%zu metadata=%ld backend=%ld", stat.ms_entries, metadata.entries, candidate->entries); rc = EINVAL; } else { rc = 0; } out_abort: if (txn) mdb_txn_abort(txn); out_close: mdb_env_close(validate_env); return rc; } /* * build_offline_lmdb_from_backends - build and validate replacement LMDB. * @config: active daemon configuration. * @candidate: replacement environment plan/result. * * Returns 0 when @candidate contains a complete validated replacement * environment. On failure the temporary environment is removed and the live * environment has not been touched. */ static int build_offline_lmdb_from_backends(conf_t *config, struct offline_lmdb *candidate) { int rc; // Build in isolation while the live LMDB environment remains open. rc = open_offline_lmdb(candidate, config); if (rc) return rc; // Backend memfds are the authoritative trust source for replacement. rc = import_backends_to_offline_lmdb(candidate); if (rc) goto out_close; // Metadata makes the candidate self-describing after reopen. rc = write_offline_metadata(candidate); if (rc) goto out_close; rc = mdb_env_sync(candidate->env, 1); if (rc) goto out_close; mdb_env_close(candidate->env); candidate->env = NULL; // Validate the persisted files, not the writer's in-memory handles. rc = validate_offline_lmdb(candidate); if (rc) goto out_remove; return 0; out_close: if (candidate->env) { mdb_env_close(candidate->env); candidate->env = NULL; } out_remove: remove_lmdb_environment_dir(candidate->tmpdir); candidate->tmpdir[0] = 0; return rc; } struct lmdb_swap_backup { char data_backup[PATH_MAX]; int have_data_backup; }; /* * lmdb_swap_backup_init - create an old-data backup path. * @backup: backup descriptor to initialize. * * Returns 0 on success or errno on failure. */ static int lmdb_swap_backup_init(struct lmdb_swap_backup *backup) { int fd; int rc; memset(backup, 0, sizeof(*backup)); rc = lmdb_file_path(backup->data_backup, sizeof(backup->data_backup), "data.mdb.compact-backup.XXXXXX"); if (rc) return rc; fd = mkstemp(backup->data_backup); if (fd < 0) return errno; close(fd); unlink(backup->data_backup); return 0; } /* * swap_lmdb_data_file - publish replacement data.mdb on disk. * @tmpdir: validated temporary LMDB environment. * @backup: receives a hard-link backup of the previous data.mdb. * * Only data.mdb is replaced. The live lock.mdb is removed after the live * environment is closed so LMDB recreates a reader table matching the reopened * environment. If anything fails before data.mdb is renamed, the old file is * still the live file. */ static int swap_lmdb_data_file(const char *tmpdir, struct lmdb_swap_backup *backup) { char live_data[PATH_MAX]; char live_lock[PATH_MAX]; char tmp_data[PATH_MAX]; int rc; rc = lmdb_file_path(live_data, sizeof(live_data), "data.mdb"); if (rc) return rc; rc = lmdb_file_path(live_lock, sizeof(live_lock), "lock.mdb"); if (rc) return rc; rc = dir_file_path(tmp_data, sizeof(tmp_data), tmpdir, "data.mdb"); if (rc) return rc; rc = lmdb_swap_backup_init(backup); if (rc) return rc; if (link(live_data, backup->data_backup) == 0) { backup->have_data_backup = 1; } else if (errno != ENOENT) { rc = errno; return rc; } if (rename(tmp_data, live_data) < 0) { rc = errno; if (backup->have_data_backup) unlink(backup->data_backup); return rc; } /* * The data file is authoritative. Removing the stale lock file avoids * carrying old reader slots into an environment generation that has no * live readers yet. Missing lock files are harmless because LMDB creates * one on reopen. */ if (unlink(live_lock) && errno != ENOENT) msg(LOG_WARNING, "Could not remove stale LMDB lock file: %s", strerror(errno)); return 0; } /* * restore_lmdb_data_file - restore old data.mdb after failed replacement. * @backup: backup descriptor from swap_lmdb_data_file(). * * Returns 0 on success or errno on restore failure. */ static int restore_lmdb_data_file(const struct lmdb_swap_backup *backup) { char live_data[PATH_MAX]; char live_lock[PATH_MAX]; int rc; rc = lmdb_file_path(live_data, sizeof(live_data), "data.mdb"); if (rc) return rc; rc = lmdb_file_path(live_lock, sizeof(live_lock), "lock.mdb"); if (rc) return rc; if (backup->have_data_backup) { if (rename(backup->data_backup, live_data) < 0) return errno; } else if (unlink(live_data) && errno != ENOENT) { return errno; } if (unlink(live_lock) && errno != ENOENT) return errno; return 0; } /* * finish_lmdb_swap_backup - remove old-data backup after successful reopen. * @backup: backup descriptor from swap_lmdb_data_file(). */ static void finish_lmdb_swap_backup(const struct lmdb_swap_backup *backup) { if (backup->have_data_backup) unlink(backup->data_backup); } /* * reopen_old_environment_after_swap_failure - restore and reopen old LMDB. * @config: daemon configuration. db_max_size must already describe old env. * @backup: old data.mdb backup. * @old_trust_generation: logical generation to preserve in reports. * @old_env_generation: physical generation to preserve in reports. * * Returns 0 when the old environment was restored and reopened. */ static int reopen_old_environment_after_swap_failure(conf_t *config, const struct lmdb_swap_backup *backup, unsigned long old_trust_generation, unsigned long old_env_generation) { int rc; rc = restore_lmdb_data_file(backup); if (rc) { msg(LOG_ERR, "Failed to restore previous LMDB environment after compaction failure: %s", strerror(rc)); return rc; } rc = init_db_with_generations(config, old_trust_generation, old_env_generation); if (rc) msg(LOG_ERR, "Failed to reopen restored LMDB environment: init_db() (%d)", rc); return rc; } /* * publish_offline_lmdb - swap a validated temp environment into service. * @config: active daemon configuration. * @candidate: validated temporary LMDB environment. * * The caller has already done slow build and validation. This function owns * the short maintenance window: block readers, verify no pinned generation * remains, close the live environment, rename data.mdb, and reopen. Any * failure before a successful reopen attempts to restore and reopen the old * environment before releasing the update lock. */ static int publish_offline_lmdb(conf_t *config, const struct offline_lmdb *candidate) __nonnull ((1, 2)); static int publish_offline_lmdb(conf_t *config, const struct offline_lmdb *candidate) { struct lmdb_swap_backup backup; char reason[TRUST_DB_COMPACT_REASON_SIZE]; unsigned long old_trust_generation; unsigned long old_env_generation; int rc; old_trust_generation = trust_db_current_generation_number(); lmdb_environment_snapshot(&old_env_generation, NULL); if (old_env_generation == 0) old_env_generation = 1; lock_update_thread(); // Enter the short maintenance window only after the candidate is ready. trust_db_reclaim_retired(); if (trust_db_generation_readers_active(reason, sizeof(reason))) { msg(LOG_WARNING, "Trust DB compaction postponed: %s", reason); unlock_update_thread(); return EBUSY; } // No live generations are pinned; close LMDB before replacing data.mdb. close_env(1); rc = swap_lmdb_data_file(candidate->tmpdir, &backup); if (rc) { int reopen_rc; msg(LOG_ERR, "Could not publish rebuilt LMDB environment: %s", strerror(rc)); reopen_rc = init_db_with_generations(config, old_trust_generation, old_env_generation); unlock_update_thread(); return reopen_rc ? reopen_rc : rc; } // Publication is complete only after the replacement reopens cleanly. rc = init_db_with_generations(config, candidate->trust_generation, candidate->env_generation); if (rc) { msg(LOG_ERR, "Could not reopen rebuilt LMDB environment: init_db() (%d)", rc); rc = reopen_old_environment_after_swap_failure(config, &backup, old_trust_generation, old_env_generation); unlock_update_thread(); return rc ? rc : EIO; } finish_lmdb_swap_backup(&backup); unlock_update_thread(); return 0; } /* * install_offline_lmdb_at_startup - publish replacement before daemon starts. * @config: daemon configuration. * @candidate: validated temporary LMDB environment. * * Startup has no live LMDB readers yet, but it still uses the same backup and * restore rules as the runtime path. The function leaves an LMDB environment * open for the rest of init_database(). */ static int install_offline_lmdb_at_startup(conf_t *config, const struct offline_lmdb *candidate) { struct lmdb_swap_backup backup; int rc; rc = swap_lmdb_data_file(candidate->tmpdir, &backup); if (rc) return rc; rc = init_db_with_generations(config, candidate->trust_generation, candidate->env_generation); if (rc) { unsigned int compact_target = config->db_max_size; msg(LOG_ERR, "Could not open rebuilt startup LMDB environment: init_db() (%d)", rc); config->db_max_size = startup_compaction_fallback_mb; if (reopen_old_environment_after_swap_failure(config, &backup, 1, 1) == 0) { msg(LOG_WARNING, "Startup trust DB compaction at %u MiB failed; previous environment preserved", compact_target); return UPDATE_DB_PRESERVED; } return rc; } finish_lmdb_swap_backup(&backup); return 0; } /* * compact_trust_database - rebuild and replace the LMDB environment. * @config: active daemon configuration. * @source: caller category for logs/status. * * Backend snapshots must already be loaded. This keeps the admin path explicit * about when package/trust-file state is refreshed and avoids hidden * filesystem swaps during ordinary reloads. */ static int compact_trust_database(conf_t *config, const char *source_name) { struct offline_lmdb candidate = { 0 }; int rc; candidate.trust_generation = trust_db_next_generation_number(); candidate.env_generation = lmdb_environment_next_generation(); msg(LOG_INFO, "Starting %s trust DB compaction into LMDB environment generation %lu", source_name, candidate.env_generation); // Slow path: build and validate a complete replacement off to the side. rc = build_offline_lmdb_from_backends(config, &candidate); if (rc) goto out_status; // Fast path: briefly close/reopen the live environment for the swap. rc = publish_offline_lmdb(config, &candidate); remove_lmdb_environment_dir(candidate.tmpdir); if (rc) goto out_status; request_object_cache_flush(); mdb_env_sync(env, 1); check_db_size(config); out_status: if (rc) { msg(LOG_ERR, "Trust DB compaction from %s failed (%s); previous environment preserved", source_name, mdb_strerror(rc)); } else { msg(LOG_INFO, "Trust DB compaction from %s published LMDB environment generation %lu", source_name, candidate.env_generation); } return rc; } /* * compact_trust_database_at_startup - rebuild compact environment before use. * @config: active daemon configuration. * * Backend snapshots must already be loaded. On build/swap failure this leaves * the old environment open when possible so startup can continue with the * preserved trust DB instead of failing closed due only to compaction. */ static int compact_trust_database_at_startup(conf_t *config) { struct offline_lmdb candidate = { .trust_generation = 1, .env_generation = 1, }; int rc; msg(LOG_INFO, "Starting startup trust DB compaction into a new LMDB environment"); // Startup has backend snapshots but no live LMDB readers yet. rc = build_offline_lmdb_from_backends(config, &candidate); if (rc) goto out_failed; rc = install_offline_lmdb_at_startup(config, &candidate); remove_lmdb_environment_dir(candidate.tmpdir); if (rc == UPDATE_DB_PRESERVED) { return 0; } if (rc) goto out_failed; check_db_size(config); msg(LOG_INFO, "Startup trust DB compaction completed"); return 0; out_failed: if (candidate.tmpdir[0]) remove_lmdb_environment_dir(candidate.tmpdir); config->db_max_size = startup_compaction_fallback_mb; if (env == NULL) { int open_rc = init_db_with_generations(config, 1, 1); if (open_rc) return open_rc; } msg(LOG_WARNING, "Startup trust DB compaction failed (%s); previous environment preserved", mdb_strerror(rc)); return 0; } /* * mark_startup_compaction_needed - remember startup compaction recommendation. * @target_mb: compacted map size selected by autosize. * @fallback_mb: current map size to restore if compaction fails. * * Autosize inspection runs before backend snapshots are loaded. Store the * recommendation so init_database() can build the replacement from backend * truth after backend_load() succeeds. */ static void mark_startup_compaction_needed(unsigned int target_mb, unsigned int fallback_mb) { msg(LOG_INFO, "autosize: startup will rebuild compact trust DB environment at %u MiB", target_mb); startup_compaction_target_mb = target_mb; startup_compaction_fallback_mb = fallback_mb; } /* autosize_database - compute new map size when utilisation drifts * @config: active daemon configuration, db_max_size is updated in-place * @mode: startup or live reload context. * Returns 1 when the configured map size was modified, 0 otherwise. On * inspection error the function keeps the caller's db_max_size. If startup * compaction fails after a shrink decision, db_max_size is restored to the * actual existing map size so init_db() can safely open the preserved DB. */ static int autosize_database(conf_t *config, enum autosize_plan_mode mode) { struct trust_db_sizing_state state; unsigned int current_mb; unsigned int target_mb; int rc; /* * Open the existing environment read-only without LMDB lockfile * mutexes. This start/reload inspection only needs a point-in-time * sizing view and must not disturb the live daemon environment. */ rc = read_existing_lmdb_sizing_state(&state); if (rc) { msg(LOG_WARNING, "autosize: could not inspect LMDB (%s) - keeping %u MiB", mdb_strerror(rc), config->db_max_size); return 0; } current_mb = config->db_max_size; if (state.current_mb != config->db_max_size) { msg(LOG_INFO, "autosize: database inspection using actual map %u MiB " "instead of configured baseline %u MiB", state.current_mb, config->db_max_size); config->db_max_size = state.current_mb; } if (!apply_autosize_plan(config, &state, mode, "database inspection")) { if (current_mb != config->db_max_size) return 1; return 0; } target_mb = autosize_effective_target_mb(&state, mode); if (mode == AUTOSIZE_STARTUP_INSPECTION && target_mb < state.current_mb) { mark_startup_compaction_needed(target_mb, state.current_mb); } return 1; } /* * autosize_reload_growth_step_mb - grow one emergency retry step. * @base_mb: baseline map size in MiB. * * Returns a 25 percent growth step, or one MiB when integer rounding would * otherwise leave the value unchanged. */ static unsigned long autosize_reload_growth_step_mb(unsigned long base_mb) { unsigned long new_mb = base_mb + (base_mb / 4); if (new_mb <= base_mb) new_mb++; return new_mb; } /* * autosize_reload_grow_from_state_mb - choose a map-full retry size. * @old_mb: Current configured map size. * @state: Optional live LMDB sizing state. * * A bounded auto shrink can make config->db_max_size smaller than the live * LMDB map if the file high-water allocation cannot physically shrink. On * MDB_MAP_FULL, grow from the actual map in that case; growing from the stale * configured value may call mdb_env_set_mapsize() with a size that still does * not expand the mapping, causing the retry to repeat the same full map. * * Returns the retry target clamped to the configuration type range. */ static unsigned long autosize_reload_grow_from_state_mb(unsigned long old_mb, const struct trust_db_sizing_state *state) { unsigned long base_mb = old_mb; unsigned long new_mb; if (state && base_mb < state->current_mb) base_mb = state->current_mb; new_mb = autosize_reload_growth_step_mb(base_mb); if (state) { unsigned int target_mb = autosize_effective_target_mb(state, AUTOSIZE_RELOAD_PREFLIGHT); if (new_mb < target_mb) new_mb = target_mb; } if (new_mb > UINT_MAX) new_mb = UINT_MAX; return new_mb; } /* * autosize_reload_grow_target_mb - choose a map-full retry size. * @old_mb: Current configured map size. * * Returns the larger of the historical emergency growth increment and the * current generation-aware autosize recommendation. The recommendation matters * when an existing single-generation database was compacted under the old * in-place reload assumptions and now needs room for the live generation plus * the candidate generation. */ static unsigned long autosize_reload_grow_target_mb(unsigned long old_mb) { struct trust_db_sizing_state state; if (read_live_lmdb_sizing_state(&state) == 0) return autosize_reload_grow_from_state_mb(old_mb, &state); return autosize_reload_grow_from_state_mb(old_mb, NULL); } /* Grow the live LMDB map after encountering MDB_MAP_FULL during rebuilds. * @config: active daemon configuration updated in place on success * Returns 0 when the map was expanded, otherwise 1. */ static int grow_map_after_full(conf_t *config) { unsigned long old_mb = config->db_max_size; unsigned long new_mb = autosize_reload_grow_target_mb(old_mb); /* * Emergency growth should be rare. Dump the complete map/reader state * before changing the map so QA can tell whether the full condition was * caused by reader pinning, high-water growth, or a genuine active-data * increase. */ log_lmdb_state(LOG_ERR, "trust DB autosize grow trigger", MDB_MAP_FULL); int rc = mdb_env_set_mapsize(env, new_mb * MEGABYTE); if (rc) { msg(LOG_ERR, "autosize: failed to grow trust DB to %lu MiB (%s)", new_mb, mdb_strerror(rc)); return 1; } config->db_max_size = new_mb; if (autosize_reload_floor_mb < new_mb) autosize_reload_floor_mb = new_mb; msg(LOG_INFO, "autosize: trust DB full at %lu MiB - grew to %lu MiB, retrying rebuild", old_mb, new_mb); log_lmdb_state(LOG_INFO, "trust DB autosize after grow", 0); return 0; } /* * autosize_reload_preflight - grow the live LMDB map before drop/rebuild. * @config: active daemon configuration. * * Reload is a complete named-database drop followed by a full import from the * backend snapshots. Identical backend contents do not make the operation a * no-op: LMDB still has to commit the drop transaction and the freelist * metadata before old data pages are reusable. A map sized only for the final * active database can therefore hit MDB_MAP_FULL during mdb_drop() after * repeated reloads raise the allocated high-water mark. This hook runs while * the update thread owns the trust DB write lock and grows auto-sized maps * before the destructive transaction starts. Manual configurations are left * unchanged, but the recommended size is logged so self-managed installs have * an actionable value instead of a generic "increase db_max_size" error. * * Returns 0 on success or when no resize is needed, non-zero on resize error. */ static int autosize_reload_preflight(conf_t *config) { struct trust_db_sizing_state state; unsigned int old_db_max_size = config->db_max_size; unsigned int old_reload_floor_mb = autosize_reload_floor_mb; int rc; rc = read_live_lmdb_sizing_state(&state); if (rc) { msg(LOG_WARNING, "autosize: reload preflight could not inspect LMDB (%s)", mdb_strerror(rc)); return 0; } if (!config->do_auto_db_sizing) { if (state.recommended_pages > state.map_pages) msg(LOG_WARNING, "db_max_size may be too small for safe reload: " "active=%zu pages, allocated=%zu pages, " "configured=%u MiB, recommended at least %u MiB; " "set db_max_size to at least this value or use auto", state.active_pages, state.allocated_pages, config->db_max_size, state.recommended_mb); return 0; } if (!apply_autosize_plan(config, &state, AUTOSIZE_RELOAD_PREFLIGHT, "reload preflight")) return 0; rc = mdb_env_set_mapsize(env, (size_t)config->db_max_size * MEGABYTE); if (rc) { msg(LOG_ERR, "autosize: reload preflight resize to %u MiB: %s", config->db_max_size, mdb_strerror(rc)); config->db_max_size = old_db_max_size; autosize_reload_floor_mb = old_reload_floor_mb; return rc; } return 0; } /* * init_dbi - open metadata and publish the active LMDB generation handle. * * Returns 0 on success or a non-zero LMDB error code. */ static int init_dbi(unsigned long generation) { MDB_txn *txn; struct trust_db_metadata metadata; struct trust_db_generation *gen = NULL; const char *active_name = db; unsigned int active_flags = MDB_DUPSORT|MDB_CREATE; int rc; rc = mdb_txn_begin(env, NULL, 0, &txn); if (rc) return rc; rc = mdb_dbi_open(txn, TRUST_DB_METADATA_NAME, MDB_CREATE, &metadata_dbi); if (rc) { msg(LOG_ERR, "%s", mdb_strerror(rc)); mdb_txn_abort(txn); return rc; } metadata_dbi_init = 1; rc = trust_db_metadata_read(txn, &metadata); if (rc == 0) { active_name = metadata.name; // Persisted generation belongs to the previous daemon run. active_flags = MDB_DUPSORT; } else if (rc != MDB_NOTFOUND) { msg(LOG_WARNING, "Ignoring unreadable trust DB generation metadata: %s", mdb_strerror(rc)); } gen = trust_db_generation_alloc(generation, active_name); if (gen == NULL) { metadata_dbi_init = 0; mdb_txn_abort(txn); return ENOMEM; } rc = mdb_dbi_open(txn, gen->name, active_flags, &gen->handle); if (rc) { msg(LOG_ERR, "%s", mdb_strerror(rc)); metadata_dbi_init = 0; free(gen); mdb_txn_abort(txn); return rc; } gen->publish_time = time(NULL); rc = trust_db_metadata_write(txn, gen); if (rc) { msg(LOG_ERR, "Could not write trust DB metadata: %s", mdb_strerror(rc)); metadata_dbi_init = 0; free(gen); mdb_txn_abort(txn); return rc; } rc = mdb_txn_commit(txn); if (rc) { msg(LOG_ERR, "%s", mdb_strerror(rc)); dbi_init = 0; metadata_dbi_init = 0; free(gen); return rc; } pthread_mutex_lock(&generation_lock); active_generation = gen; dbi = gen->handle; next_generation = gen->generation + 1; pthread_mutex_unlock(&generation_lock); dbi_init = 1; return 0; } static int init_db_with_generations(const conf_t *config, unsigned long trust_generation, unsigned long env_generation) { /* * Decision workers are long-lived across autosize close/reopen cycles * and may re-enter trust lookups from one OS thread. Keep LMDB reader * slots tied to transactions instead of thread TLS so each aborted read * transaction fully releases its slot before the next lookup. */ unsigned int flags = MDB_MAPASYNC|MDB_NOSYNC|MDB_NOTLS; int rc; #ifndef DEBUG flags |= MDB_WRITEMAP; #endif if (mdb_env_create(&env)) { /* env not allocated on failure, but ensure it's NULL */ env = NULL; return 1; } if (mdb_env_set_maxdbs(env, TRUST_DB_MAX_NAMED_DBS)) { /* Clean up environment on failure */ mdb_env_close(env); env = NULL; return 2; } if (mdb_env_set_mapsize(env, (size_t)config->db_max_size * MEGABYTE)) { /* Clean up environment on failure */ mdb_env_close(env); env = NULL; return 3; } db_max_readers = configured_reader_limit(config); if (mdb_env_set_maxreaders(env, db_max_readers)) { /* Clean up environment on failure */ mdb_env_close(env); env = NULL; return 4; } rc = mdb_env_open(env, data_dir, flags, 0660); if (rc) { msg(LOG_ERR, "env_open error: %s", mdb_strerror(rc)); /* Clean up environment on failure */ mdb_env_close(env); env = NULL; return 5; } rc = init_dbi(trust_generation); if (rc) { /* Clean up environment on failure */ mdb_env_close(env); env = NULL; return 6; } MDB_maxkeysize = mdb_env_get_maxkeysize(env); msg(LOG_INFO, "fapolicyd integrity is %u", decision_config_integrity(NULL)); lib_symlink = is_link("/lib"); lib64_symlink = is_link("/lib64"); bin_symlink = is_link("/bin"); sbin_symlink = is_link("/sbin"); lmdb_environment_publish(env_generation); return 0; } static int init_db(const conf_t *config) { return init_db_with_generations(config, 1, 1); } static unsigned get_pages_in_use(void); static unsigned long pages, max_pages; static unsigned long allocated_pages, allocated_pages_percent; /* * forget_generation_list - release in-memory generation handles. * @list: List of generations to free. * @close_handles: Non-zero closes DBI handles before freeing. * * Returns nothing. */ static void forget_generation_list(struct trust_db_generation *list, int close_handles) { struct trust_db_generation *next; while (list) { next = list->next; if (close_handles && env) mdb_close(env, list->handle); free(list); list = next; } } /* * forget_generations - clear active and retired generation state. * @close_handles: Non-zero closes DBI handles before forgetting them. * * Returns nothing. */ static void forget_generations(int close_handles) { struct trust_db_generation *active, *retired; pthread_mutex_lock(&generation_lock); active = active_generation; retired = retired_generations; active_generation = NULL; retired_generations = NULL; next_generation = 1; pthread_mutex_unlock(&generation_lock); if (active) { active->next = NULL; forget_generation_list(active, close_handles); } forget_generation_list(retired, close_handles); } /* * close_env - close LMDB env and clear cached handle state * @do_close_dbi: non-zero closes cached dbi before env close * * Returns: none */ static void close_env(int do_close_dbi) { if (env == NULL) return; forget_generations(do_close_dbi); if (do_close_dbi && metadata_dbi_init) mdb_close(env, metadata_dbi); mdb_env_close(env); env = NULL; dbi_init = 0; metadata_dbi_init = 0; } struct lmdb_reader_log { int priority; const char *context; unsigned int lines; }; /* * log_lmdb_reader_line - bridge mdb_reader_list() output into syslog. * @line: reader-table line provided by LMDB. * @ctx: struct lmdb_reader_log with priority and context. * * Returns 0 so LMDB continues listing reader slots. */ static int log_lmdb_reader_line(const char *line, void *ctx) { struct lmdb_reader_log *log = ctx; if (line == NULL || log == NULL) return 0; msg(log->priority, "LMDB reader after %s: %s", log->context, line); log->lines++; return 0; } /* * log_lmdb_readers - dump LMDB reader slots for map-full diagnostics. * @priority: syslog priority used for the diagnostic lines. * @context: short description of the operation being diagnosed. * * The configured max reader count only says how many slots may exist. The * reader-list dump shows whether any slot is actually active and which * transaction id it pins, which separates reader pinning from write-churn * growth during reload storms. * * Returns: none. */ static void log_lmdb_readers(int priority, const char *context) { struct lmdb_reader_log log = { .priority = priority, .context = context ? context : "unknown operation", }; int rc; if (env == NULL) return; rc = mdb_reader_list(env, log_lmdb_reader_line, &log); if (rc < 0) msg(priority, "LMDB reader list after %s failed: %s", log.context, mdb_strerror(rc)); else if (log.lines == 0) msg(priority, "LMDB reader after %s: no active readers", log.context); } /* * log_lmdb_state - log LMDB map, named DB, and reader-table state. * @priority: syslog priority used for the diagnostic lines. * @context: short description of the operation being diagnosed. * @lmdb_rc: LMDB return code that triggered the diagnostic, or 0. * * LMDB map pressure follows the environment high-water mark, not just the * current named database page count. Rebuilding a steady set of trust entries * can still run out of map space when old reader transactions keep deleted * pages from being reused. These diagnostics are intentionally grouped around * failures and size checks so the logs show active DB pages, allocated pages, * the last transaction id, and reader-table pressure together. * * Returns: none. */ static void log_lmdb_state(int priority, const char *context, int lmdb_rc) { MDB_envinfo info; MDB_stat stat; MDB_txn *txn = NULL; struct trust_db_generation *gen = NULL; const char *where = context ? context : "unknown operation"; size_t page_size = 4096; size_t map_pages = 0; size_t env_allocated_pages = 0; size_t db_pages = 0; int stale_readers = 0; int reader_rc; int stat_rc = 0; int rc; if (env == NULL) { msg(priority, "LMDB state after %s: environment is closed", where); return; } rc = mdb_env_info(env, &info); if (rc) { msg(priority, "LMDB state after %s: mdb_env_info failed: %s", where, mdb_strerror(rc)); return; } reader_rc = mdb_reader_check(env, &stale_readers); if (dbi_init) { gen = trust_db_generation_acquire(); } if (gen) { stat_rc = mdb_txn_begin(env, NULL, MDB_RDONLY, &txn); if (stat_rc == 0) { stat_rc = mdb_stat(txn, gen->handle, &stat); mdb_txn_abort(txn); } } else { stat_rc = EINVAL; } if (gen) trust_db_generation_release(gen); if (stat_rc == 0) { page_size = stat.ms_psize; db_pages = stat.ms_branch_pages + stat.ms_leaf_pages + stat.ms_overflow_pages; } if (page_size) map_pages = info.me_mapsize / page_size; env_allocated_pages = info.me_last_pgno + 1; if (lmdb_rc) msg(priority, "LMDB state after %s: error=%s (%d)", where, mdb_strerror(lmdb_rc), lmdb_rc); msg(priority, "LMDB env after %s: map=%zu MiB pages=%zu " "allocated=%zu (%zu%%) last_pgno=%zu txnid=%zu", where, info.me_mapsize / MEGABYTE, map_pages, env_allocated_pages, map_pages ? (100 * env_allocated_pages) / map_pages : 0, info.me_last_pgno, info.me_last_txnid); if (stat_rc == 0) { msg(priority, "LMDB db after %s: entries=%zu pages=%zu (%zu%%) " "branch=%zu leaf=%zu overflow=%zu depth=%u page_size=%zu", where, stat.ms_entries, db_pages, map_pages ? (100 * db_pages) / map_pages : 0, stat.ms_branch_pages, stat.ms_leaf_pages, stat.ms_overflow_pages, stat.ms_depth, page_size); } else { msg(priority, "LMDB db after %s: stat unavailable: %s", where, mdb_strerror(stat_rc)); } if (reader_rc) { msg(priority, "LMDB readers after %s: reader_check failed: %s slots_used=%u max=%u", where, mdb_strerror(reader_rc), info.me_numreaders, info.me_maxreaders); } else { msg(priority, "LMDB readers after %s: slots_used=%u max=%u " "stale_cleared=%d configured_max=%u", where, info.me_numreaders, info.me_maxreaders, stale_readers, db_max_readers); } if (lmdb_rc == MDB_MAP_FULL) log_lmdb_readers(priority, where); } static void close_db(int do_report) { if (do_report) { MDB_envinfo st; // Collect useful stats unsigned size = get_pages_in_use(); if (size == 0) { msg(LOG_DEBUG, "The trust database is empty."); } else { mdb_env_info(env, &st); max_pages = st.me_mapsize / size; allocated_pages = st.me_last_pgno + 1; allocated_pages_percent = max_pages ? ((100 * allocated_pages) / max_pages) : 0; msg(LOG_DEBUG, "Trust database max pages: %lu", max_pages); msg(LOG_DEBUG, "Trust database pages in use: %lu (%lu%%)", pages, max_pages ? ((100*pages)/max_pages) : 0); msg(LOG_DEBUG, "Trust database allocated high-water pages: %lu (%lu%%)", allocated_pages, allocated_pages_percent); } } // Now close down close_env(1); } static void check_db_size(const conf_t *config) { struct trust_db_sizing_state state; unsigned int target_mb; int rc; rc = read_live_lmdb_sizing_state(&state); if (rc || state.page_size == 0) { msg(LOG_WARNING, "Cannot inspect trust database size (%s)", rc ? mdb_strerror(rc) : "empty database"); pages = 0; allocated_pages = 0; allocated_pages_percent = 0; return; } pages = state.active_pages; max_pages = state.map_pages; allocated_pages = state.allocated_pages; allocated_pages_percent = state.allocated_percent; // Active DB pages can stay steady while LMDB's high-water mark grows // across repeated rebuilds. In auto mode this is informational unless // a write actually fails; manual mode keeps it as a warning because the // administrator may need to adjust db_max_size. msg(LOG_DEBUG, "Trust database active pages: %lu (%lu%%), allocated pages: %zu (%lu%%)", pages, state.active_percent, state.allocated_pages, state.allocated_percent); if (state.allocated_percent > TRUST_DB_RELOAD_HIGHWATER_PERCENT && state.allocated_percent > state.active_percent) { int priority = config->do_auto_db_sizing ? LOG_INFO : LOG_WARNING; msg(priority, "Trust database LMDB map high-water at %lu%% capacity " "while active DB pages are at %lu%%", state.allocated_percent, state.active_percent); if (!config->do_auto_db_sizing) log_lmdb_state(LOG_WARNING, "trust database size check", 0); } target_mb = autosize_effective_target_mb(&state, AUTOSIZE_LIVE_INSPECTION); if (config->do_auto_db_sizing) { if (target_mb > config->db_max_size) { if (state.active_target_pages > state.map_pages) msg(LOG_INFO, "Trust database at %lu%% capacity - " "map will grow automatically before next rebuild", state.active_percent); else msg(LOG_INFO, "Trust database reload headroom target is %u MiB " "(active=%lu%% high-water=%lu%%) - " "map will grow automatically before next rebuild", target_mb, state.active_percent, state.allocated_percent); } else if (autosize_shrink_allowed(config, &state, target_mb, AUTOSIZE_LIVE_INSPECTION)) { msg(LOG_INFO, "Trust database at %lu%% capacity - " "map will shrink automatically on next rebuild", state.active_percent); } return; } if (target_mb > config->db_max_size) { msg(LOG_WARNING, "Trust database may need %u MiB for safe reload " "(active=%lu%% allocated=%lu%%); set db_max_size to at " "least this value or use auto", target_mb, state.active_percent, state.allocated_percent); } else if (state.active_percent < TRUST_DB_SHRINK_TRIGGER_PERCENT) { msg(LOG_WARNING, "Trust database at %lu%% capacity - " "might consider shrinking the size to save space", state.active_percent); } } /* * database_config_report - write trust database configured size. * @f: report stream. * Returns nothing. */ void database_config_report(FILE *f) { fprintf(f, "Trust database max pages: %lu\n", max_pages); fprintf(f, "Trust database max readers: %u\n", db_max_readers); } /* * database_resize_report - write manual-size growth recommendation. * @f: report stream. * @config: active daemon configuration. * * Auto mode owns map growth itself, so this report is intentionally silent * unless an administrator has selected a numeric db_max_size. In manual mode, * the daemon preserves the old DB on reload failure but will not grow the map; * the status report gives the administrator the same target used by autosize. */ static void database_resize_report(FILE *f, const conf_t *config) { struct trust_db_sizing_state state; const char *recommended = "no"; const char *reason = "manual db_max_size has safe reload headroom"; unsigned int target_mb = 0; int rc; if (config == NULL || config->do_auto_db_sizing) return; rc = read_live_lmdb_sizing_state(&state); if (rc) { reason = "could not inspect LMDB sizing"; } else { target_mb = autosize_effective_target_mb(&state, AUTOSIZE_LIVE_INSPECTION); if (target_mb > config->db_max_size) { recommended = "yes"; reason = "manual db_max_size is below the safe reload target"; } else { target_mb = 0; } } fprintf(f, "Trust database resize recommended: %s\n", recommended); fprintf(f, "Trust database resize reason: %s\n", reason); if (target_mb) fprintf(f, "Trust database resize target: %u MiB\n", target_mb); } /* * database_compaction_report - write compaction recommendation. * @f: report stream. * @generation_report: active generation/reclamation snapshot. * * Compaction is useful when LMDB's allocated high-water mark is much larger * than the active trust DB footprint and no retired generations are pinned. * If retired generations are still present, the first fix is to find the held * readers; replacing the whole environment would have to wait for the same * safety condition. */ static void database_compaction_report(FILE *f, const database_generation_report_t *generation_report) { struct trust_db_sizing_state state; const char *recommended = "no"; const char *reason = "high-water usage is within the active working set"; unsigned int target_mb = 0; int rc; rc = read_live_lmdb_sizing_state(&state); if (rc) { reason = "could not inspect LMDB sizing"; } else if (generation_report->retired_count) { reason = "retired trust database generations are still pinned"; } else if (state.allocated_percent > TRUST_DB_RELOAD_HIGHWATER_PERCENT && state.allocated_percent > state.active_percent + 15) { recommended = "yes"; reason = "allocated high-water is much larger than active pages"; target_mb = autosize_effective_target_mb(&state, AUTOSIZE_LIVE_INSPECTION); } fprintf(f, "Trust database compaction recommended: %s\n", recommended); fprintf(f, "Trust database compaction reason: %s\n", reason); if (target_mb) fprintf(f, "Trust database compaction target: %u MiB\n", target_mb); } /* * database_utilization_report - write current trust database utilization. * @f: report stream. * @config: active daemon configuration, or NULL when unavailable. * Returns nothing. */ void database_utilization_report(FILE *f, const conf_t *config) { database_generation_report_t report; /* * State reports run on decision workers and can overlap the update * thread. Shared ownership prevents close_env() or a compaction swap from * invalidating the live LMDB handles while sizing reads use them. */ database_update_read_lock(); trust_db_generation_report_snapshot(&report); fprintf(f, "Trust database pages in use: %lu (%lu%%)\n", pages, max_pages ? ((100*pages)/max_pages) : 0); fprintf(f, "Trust database allocated high-water pages: %lu (%lu%%)\n", allocated_pages, allocated_pages_percent); fprintf(f, "Retired trust database generations: %u\n", report.retired_count); fprintf(f, "Oldest retired trust database generation age: %lu seconds\n", report.oldest_retired_age); fprintf(f, "Max trust database generation reclaim delay: %lu seconds\n", report.max_reclaim_delay); database_resize_report(f, config); database_compaction_report(f, &report); database_update_read_unlock(); } void database_report(FILE *f) { database_config_report(f); database_utilization_report(f, NULL); } /* * trust_metric_snapshot_ulong - copy and optionally reset an ulong metric. * @counter: metric counter to read. * @reset: non-zero resets the counter after copying. * * Returns the copied metric value. */ static unsigned long trust_metric_snapshot_ulong(atomic_ulong *counter, int reset) { if (reset) return atomic_exchange_explicit(counter, 0, memory_order_relaxed); return atomic_load_explicit(counter, memory_order_relaxed); } /* * database_metrics_snapshot_reset - copy trust DB metrics. * @metrics: destination snapshot. * @reset: non-zero resets counters after copying. * * Returns nothing. */ static void database_metrics_snapshot_reset( struct trust_db_metrics_snapshot *metrics, int reset) { if (metrics == NULL) return; metrics->lookups = trust_metric_snapshot_ulong( &trust_metrics.lookups, reset); metrics->reader_slots_full = trust_metric_snapshot_ulong( &trust_metrics.reader_slots_full, reset); } /* * database_metrics_report_reset - write resettable trust DB read metrics. * @f: report stream. * @reset: non-zero resets counters after copying. * * Returns nothing. */ void database_metrics_report_reset(FILE *f, int reset) { struct trust_db_metrics_snapshot metrics; if (f == NULL) return; database_metrics_snapshot_reset(&metrics, reset); fputs("\nTrust database lookups:\n", f); fprintf(f, "Trust DB lookups: %lu\n", metrics.lookups); fprintf(f, "Trust DB reader slots full: %lu\n", metrics.reader_slots_full); } static void abort_transaction(MDB_txn *txn) { mdb_txn_abort(txn); } /* * Fast parser for one LMDB record line: " ". * Returns 0 on success, 1 on malformed input or overflow. */ static int lmdb_scan_record(const char *rec, unsigned int *tsource, off_t *size, char *digest) { const char *p = rec; char *end; /* --- tsource -------------------------------------------------- */ errno = 0; unsigned long v = strtoul(p, &end, 10); if (end == p || errno == ERANGE) return 1; *tsource = (unsigned int)v; /* skip whitespace */ p = end; while (isspace((unsigned char)*p)) p++; /* --- size ----------------------------------------------------- */ trustdb_size_t stored_size; unsigned long long parsed_size; if (*p == '-') return 1; errno = 0; parsed_size = strtoull(p, &end, 10); if (end == p || errno == ERANGE || (uintmax_t)parsed_size > UINT64_MAX) return 1; stored_size = (trustdb_size_t)parsed_size; if (trustdb_size_to_off_t(stored_size, size)) return 1; /* skip whitespace */ p = end; while (isspace((unsigned char)*p)) p++; /* --- digest --------------------------------------------------- */ size_t len = 0; while (p[len] && !isspace((unsigned char)p[len])) len++; if (len == 0 || len >= FILE_DIGEST_STRING_MAX) return 1; memcpy(digest, p, len); digest[len] = '\0'; return 0; } /* * parse_lmdb_record - Convert a serialized LMDB entry into structured data. * @record: Raw string pulled from the LMDB value. * @parsed: Output structure populated on success. * * Returns 0 when the record can be decoded, or 1 on parse/validation errors. * The algorithm is inferred from the stored digest length, but legacy * fragments without an algorithm hint still fall back to SHA256 so older * entries remain valid. */ static int parse_lmdb_record(const char *record, struct lmdb_record *parsed) { if (lmdb_scan_record(record, &parsed->tsource, &parsed->size, parsed->digest)) return 1; /* Fast-path: identify the algorithm without a full-string strlen */ parsed->alg = file_hash_alg_fast(parsed->digest); if (parsed->alg == FILE_HASH_ALG_NONE) parsed->alg = FILE_HASH_ALG_SHA256; /* legacy fallback */ size_t digest_len = file_hash_length(parsed->alg) * 2; if (digest_len == 0 || digest_len >= FILE_DIGEST_STRING_MAX) return 1; return 0; } /* * log_ima_mismatch - Rate-limit diagnostics when IMA measurements disagree. * @path: file path associated with the mismatch. * @record_alg: algorithm stored in metadata backing the trust database. * @ima_alg: algorithm parsed from the security.ima digest-ng header. */ static void log_ima_mismatch(const char *path, file_hash_alg_t record_alg, file_hash_alg_t ima_alg) { const char *meta = file_hash_alg_name(record_alg); const char *ima = file_hash_alg_name(ima_alg); if (ima_mismatch_silenced) return; if (ima_mismatch_err_budget) { ima_mismatch_err_budget--; msg(LOG_ERR, "IMA digest mismatch for %s (metadata %s, xattr %s)", path, meta ? meta : "unknown", ima ? ima : "unknown"); return; } if (ima_mismatch_crit_budget) { ima_mismatch_crit_budget--; msg(LOG_CRIT, "IMA digest mismatch for %s (metadata %s, xattr %s)", path, meta ? meta : "unknown", ima ? ima : "unknown"); return; } msg(LOG_NOTICE, "IMA digest mismatch logging silenced after repeated reports"); ima_mismatch_silenced = 1; } /* * Convert path to a hash value. Used when the path exceeds the LMDB key * limit(511). Note: Returned value must be deallocated. */ static char *path_to_hash(const char *path, const size_t path_len) __attr_dealloc_free __attr_access ((__read_only__, 1, 2)); static char *path_to_hash(const char *path, const size_t path_len) { unsigned char hptr[80]; char *digest; if (path_len == 0) return NULL; SHA512((unsigned char *)path, path_len, (unsigned char *)&hptr); digest = malloc((SHA512_LEN * 2) + 1); if (digest == NULL) return digest; bytes2hex(digest, hptr, SHA512_LEN); return digest; } /* * trust_db_key_init_with_max - prepare an LMDB key from a trust path. * @key: caller-owned key wrapper to initialize. * @idx: path string used as the key. * @idx_len: length hint for @idx, or 0 when unknown. * @maxkeysize: LMDB key limit for the target environment. * * Long paths are stored by SHA512 of the full path, not by a truncated * prefix. The returned key may point into @idx or into @key->hash; callers * must finish with trust_db_key_destroy(). * * Returns 0 on success or ENOMEM. */ static int trust_db_key_init_with_max(struct trust_db_key *key, const char *idx, size_t idx_len, unsigned int maxkeysize) { memset(key, 0, sizeof(*key)); if (idx_len == 0) idx_len = strlen(idx); if (idx_len > maxkeysize) { key->hash = path_to_hash(idx, idx_len); if (key->hash == NULL) return ENOMEM; key->val.mv_data = key->hash; key->val.mv_size = (SHA512_LEN * 2) + 1; } else { key->val.mv_data = (void *)idx; key->val.mv_size = idx_len; } return 0; } /* * trust_db_key_destroy - release storage owned by a prepared key. * @key: key initialized by trust_db_key_init_with_max(). * * Returns nothing. */ static void trust_db_key_destroy(struct trust_db_key *key) { free(key->hash); memset(key, 0, sizeof(*key)); } /* * trust_db_begin_write_txn_for_env - begin a trust DB write transaction. * @target_env: LMDB environment to write. * @txn: destination transaction handle. * @context: log label describing the caller. * * Returns 0 on success, WRITE_DB_MAP_FULL for map exhaustion, or a stage * code compatible with write_db(). */ static int trust_db_begin_write_txn_for_env(MDB_env *target_env, MDB_txn **txn, const char *context) { int rc; rc = mdb_txn_begin(target_env, NULL, 0, txn); if (rc) { msg(LOG_ERR, "mdb_txn_begin failed before %s: %s", context, mdb_strerror(rc)); if (target_env == env) log_lmdb_state(LOG_ERR, context, rc); return rc == MDB_MAP_FULL ? WRITE_DB_MAP_FULL : 1; } if (target_env == env && !dbi_init) { abort_transaction(*txn); *txn = NULL; msg(LOG_ERR, "open_dbi failed before %s: %s", context, mdb_strerror(EINVAL)); return 2; } return 0; } /* * trust_db_begin_write_txn - begin a live trust DB write transaction. * @txn: destination transaction handle. * @context: log label describing the caller. * * Returns 0 on success, WRITE_DB_MAP_FULL for map exhaustion, or a stage * code compatible with write_db(). */ static int trust_db_begin_write_txn(MDB_txn **txn, const char *context) { return trust_db_begin_write_txn_for_env(env, txn, context); } /* * trust_db_commit_write_txn_for_env - commit a trust DB write transaction. * @target_env: LMDB environment to write. * @txn: transaction handle to commit and clear. * @context: log label describing the caller. * * Returns 0 on success, WRITE_DB_MAP_FULL for map exhaustion, or a stage * code compatible with write_db(). */ static int trust_db_commit_write_txn_for_env(MDB_env *target_env, MDB_txn **txn, const char *context) { int rc; if (*txn == NULL) return 0; rc = mdb_txn_commit(*txn); *txn = NULL; if (rc) { msg(LOG_ERR, "mdb_txn_commit failed after %s: %s", context, mdb_strerror(rc)); if (target_env == env) log_lmdb_state(LOG_ERR, context, rc); return rc == MDB_MAP_FULL ? WRITE_DB_MAP_FULL : 4; } return 0; } /* * trust_db_commit_write_txn - commit a live trust DB write transaction. * @txn: transaction handle to commit and clear. * @context: log label describing the caller. * * Returns 0 on success, WRITE_DB_MAP_FULL for map exhaustion, or a stage * code compatible with write_db(). */ static int trust_db_commit_write_txn(MDB_txn **txn, const char *context) { return trust_db_commit_write_txn_for_env(env, txn, context); } /* * trust_db_put_record_with_max - write one record into an active transaction. * @txn: writable LMDB transaction. * @record: parsed trust DB record. * @maxkeysize: LMDB key limit for the target environment. * @context: log label describing the caller. * * Returns 0 on success, WRITE_DB_MAP_FULL for map exhaustion, 3 for mdb_put * failures, and 5 when key hashing fails. */ static int trust_db_put_record_with_max(MDB_txn *txn, MDB_dbi target_dbi, const struct trust_db_record_input *record, unsigned int maxkeysize, const char *context) { struct trust_db_key key; MDB_val value; int rc; rc = trust_db_key_init_with_max(&key, record->idx, record->idx_len, maxkeysize); if (rc) return 5; value.mv_data = (void *)record->data; value.mv_size = strlen(record->data); rc = mdb_put(txn, target_dbi, &key.val, &value, 0); trust_db_key_destroy(&key); if (rc) { msg(LOG_ERR, "mdb_put failed during %s: %s", context, mdb_strerror(rc)); return rc == MDB_MAP_FULL ? WRITE_DB_MAP_FULL : 3; } return 0; } /* * trust_db_put_record - write one record into the live environment. * @txn: writable LMDB transaction. * @target_dbi: destination named database. * @record: parsed trust DB record. * @context: log label describing the caller. * * Returns 0 on success, WRITE_DB_MAP_FULL for map exhaustion, 3 for mdb_put * failures, and 5 when key hashing fails. */ static int trust_db_put_record(MDB_txn *txn, MDB_dbi target_dbi, const struct trust_db_record_input *record, const char *context) { return trust_db_put_record_with_max(txn, target_dbi, record, MDB_maxkeysize, context); } /* * write_db - Persist a single trust record into the LMDB database. * @idx: Path string used as the key for the record. When the path exceeds * the LMDB key size limit the function hashes the path before storage. * @idx_len: Length hint for @idx. Pass 0 if length unknown. * @data: Serialized metadata for the path. The buffer contains the integrity * status, file size, and SHA256 hash sourced from the backend loaders. * * Returns 0 on success, or an error code describing the stage that failed: * 1 when the transaction cannot start, 2 on dbi open failure, 3 if mdb_put * reports an error, 4 if mdb_txn_commit fails, and 5 when key hashing fails. */ static int write_db(const char *idx, size_t idx_len, const char *data) { struct trust_db_record_input record = { .idx = idx, .idx_len = idx_len, .data = data, }; struct trust_db_generation *gen; MDB_txn *txn = NULL; int rc; gen = trust_db_generation_acquire(); if (gen == NULL) return 2; rc = trust_db_begin_write_txn(&txn, "single trust DB write"); if (rc) { trust_db_generation_release(gen); return rc; } rc = trust_db_put_record(txn, gen->handle, &record, "single trust DB write"); if (rc) { abort_transaction(txn); log_lmdb_state(LOG_ERR, "single trust DB write", rc == WRITE_DB_MAP_FULL ? MDB_MAP_FULL : 0); trust_db_generation_release(gen); return rc; } rc = trust_db_commit_write_txn(&txn, "single trust DB write"); trust_db_generation_release(gen); return rc; } /* * database_store_update_record - persist one FIFO path update in LMDB. * @path: Trust path received from a backend helper. * @size: File size to store with the record. * @hash: SHA256 digest string received from a backend helper. * * This is the deliberate bridge from database-update.c into LMDB storage for * incremental updates. The controller validates the command shape; database.c * owns serialization, locking around the write transaction, and metadata * refresh for the active generation. * * Package-manager integrations may send these records while an rpm/dnf * transaction is still running. That intentionally updates the active * generation instead of waiting for the next full reload, because scriptlets * can execute a newly installed file before the candidate generation is built * and published. The active DB uses duplicate keys so old and new hashes for * one path can coexist during that window; the next successful full reload * publishes a backend snapshot and drops stale duplicates. * * Returns 0 on success or the write_db() stage code on failure. */ int database_store_update_record(const char *path, trustdb_size_t size, const char *hash) { char data[BUFFER_SIZE]; int rc; snprintf(data, BUFFER_SIZE, DATA_FORMAT, (unsigned int)SRC_UNKNOWN, size, hash); /* * LMDB readers and a writer can run concurrently. Shared update ownership * keeps env alive across both per-record write transactions without making * every decision reader wait during large package transactions. */ database_update_read_lock(); rc = write_db(path, 0, data); if (rc == 0) refresh_active_generation_metadata(); database_update_read_unlock(); return rc; } /* * trust_db_read_open - open a private LMDB read transaction and cursor. * @read: caller-owned read handle to initialize. * * Returns 0 on success or 1 on error. MDB_READERS_FULL is counted so admins * can see when worker concurrency exhausted LMDB reader slots. */ static int trust_db_read_open(struct trust_db_read_handle *read) { struct trust_db_generation *gen; int rc; memset(read, 0, sizeof(*read)); gen = trust_db_generation_acquire(); if (gen == NULL) return 1; read->generation = gen; rc = mdb_txn_begin(env, NULL, MDB_RDONLY, &read->txn); if (rc) { trust_db_record_reader_error(rc); msg(LOG_ERR, "txn_begin:%s", mdb_strerror(rc)); trust_db_generation_release(gen); memset(read, 0, sizeof(*read)); return 1; } if (!dbi_init) { msg(LOG_ERR, "open_dbi:%s", mdb_strerror(EINVAL)); abort_transaction(read->txn); trust_db_generation_release(gen); memset(read, 0, sizeof(*read)); return 1; } if ((rc = mdb_cursor_open(read->txn, gen->handle, &read->cursor))) { msg(LOG_ERR, "cursor_open:%s", mdb_strerror(rc)); abort_transaction(read->txn); trust_db_generation_release(gen); memset(read, 0, sizeof(*read)); return 1; } return 0; } /* * trust_db_read_close - close a private LMDB read transaction and cursor. * @read: read handle previously initialized by trust_db_read_open(). * * Returns nothing. */ static void trust_db_read_close(struct trust_db_read_handle *read) { if (read->cursor) mdb_cursor_close(read->cursor); if (read->txn) abort_transaction(read->txn); trust_db_generation_release(read->generation); memset(read, 0, sizeof(*read)); } /* * read_database_stat - read LMDB statistics from a private read txn. * @st: destination for database statistics. * * Returns 0 on success or an LMDB error code on failure. */ static int read_database_stat(MDB_stat *st) { struct trust_db_generation *gen; MDB_txn *txn; int rc; gen = trust_db_generation_acquire(); if (gen == NULL) return EINVAL; rc = mdb_txn_begin(env, NULL, MDB_RDONLY, &txn); if (rc) { trust_db_record_reader_error(rc); trust_db_generation_release(gen); return rc; } rc = mdb_stat(txn, gen->handle, st); mdb_txn_abort(txn); trust_db_generation_release(gen); return rc; } static unsigned get_pages_in_use(void) { MDB_stat st; if (read_database_stat(&st)) { pages = 0; return 0; } pages = st.ms_leaf_pages + st.ms_branch_pages + st.ms_overflow_pages; return st.ms_psize; } // if success, the function returns positive number of entries in database // if error, it returns -1 static long get_number_of_entries(void) { MDB_stat status; if (read_database_stat(&status)) return -1; return status.ms_entries; } /* * trust_db_read_record - read one LMDB record through a private cursor. * @read: active read handle whose cursor keeps duplicate-key position. * @index: path key to find. * @operation: READ_DATA, READ_TEST_KEY, or READ_DATA_DUP. * @error: set to non-zero on LMDB or allocation failure. * * Returns a newly allocated value string, or NULL on error/not-found. The * returned string must be freed by the caller. */ static char *trust_db_read_record(struct trust_db_read_handle *read, const char *index, int operation, int *error) __attr_dealloc_free; static char *trust_db_read_record(struct trust_db_read_handle *read, const char *index, int operation, int *error) { int rc; char *data, *hash = NULL; MDB_val key, value; size_t len; *error = 1; // Assume an error // If the path is too long, convert to a hash // Need the whole length so read/write matches len = strlen(index); if (len > MDB_maxkeysize) { hash = path_to_hash(index, len); if (hash == NULL) return NULL; key.mv_data = (void *)hash; key.mv_size = (SHA512_LEN * 2) + 1; } else { key.mv_data = (void *)index; key.mv_size = len; } value.mv_data = NULL; value.mv_size = 0; // set cursor and read first data if (operation == READ_DATA || operation == READ_TEST_KEY) { // Read the value pointed to by key if ((rc = mdb_cursor_get(read->cursor, &key, &value, MDB_SET))) { free(hash); if (rc == MDB_NOTFOUND) { *error = 0; } else { msg(LOG_ERR, "MDB_SET: cursor_get:%s", mdb_strerror(rc)); } return NULL; } } // read next available data // READ_DATA_DUP is supposed to be used // as subsequent call just after READ_DATA if (operation == READ_DATA_DUP) { size_t nleaves; mdb_cursor_count(read->cursor, &nleaves); if (nleaves <= 1) { free(hash); *error = 0; return NULL; } // is there a next duplicate? if ((rc = mdb_cursor_get(read->cursor, &key, &value, MDB_NEXT_DUP))) { free(hash); if (rc == MDB_NOTFOUND) { *error = 0; } else { msg(LOG_ERR, "MDB_NEXT_DUP: cursor_get:%s", mdb_strerror(rc)); } return NULL; } } if (len > MDB_maxkeysize) free(hash); // Failure was already returned. Need to return a pointer of // some kind. Using the db name since its non-NULL. // A next step might be to check the status field to see that its // trusted. *error = 0; if (operation == READ_TEST_KEY) { const char *name = read->generation ? read->generation->name : db; return strndup(name, MDB_maxkeysize); } if ((data = malloc(value.mv_size+1))) { memcpy(data, value.mv_data, value.mv_size); data[value.mv_size] = 0; } return data; } /* * This function takes a path as input and looks it up. If found it * will delete the entry. * static int delete_entry_db(const char *index) { MDB_txn *txn; MDB_val key, value; if (mdb_txn_begin(env, NULL, 0, &txn)) return 1; if (open_dbi(txn)) { abort_transaction(txn); return 1; } // FIXME: if we ever use this function, it will need patching // to use hashes if the path is larger than MDB_maxkeysize. key.mv_data = (void *)index; key.mv_size = strlen(index); value.mv_data = NULL; value.mv_size = 0; if (mdb_del(txn, dbi, &key, &value)) { abort_transaction(txn); return 1; } if (mdb_txn_commit(txn)) return 1; return 0; }*/ // This function checks the database to see if its empty. It returns // a 0 if it has entries, 1 on empty, and -1 if an error static int database_empty(void) { MDB_stat status; if (read_database_stat(&status)) return -1; if (status.ms_entries == 0) return 1; return 0; } /* * delete_all_entries_db - Clear all trust records in one LMDB transaction. * * The old trust database remains published until the drop transaction * commits. Any error reported here means the delete did not become visible to * readers, so callers can preserve the current trust database instead of * treating the reload as a partially applied update. MDB_MAP_FULL is returned * as WRITE_DB_MAP_FULL so auto-sizing paths can grow the live map and retry. * * Returns 0 on success, WRITE_DB_MAP_FULL for map exhaustion, and a non-zero * stage code for other failures. */ static int delete_all_entries_db(MDB_dbi target_dbi) { int rc = 0; MDB_txn *txn; rc = mdb_txn_begin(env, NULL, 0, &txn); if (rc) { msg(LOG_ERR, "mdb_txn_begin failed before trust DB delete: %s", mdb_strerror(rc)); log_lmdb_state(LOG_ERR, "trust DB delete begin", rc); return 1; } if (!dbi_init) { abort_transaction(txn); return 2; } // 0 -> delete , 1 -> delete and close if ((rc = mdb_drop(txn, target_dbi, 0))) { abort_transaction(txn); msg(LOG_ERR, "mdb_drop failed while clearing trust DB: %s", mdb_strerror(rc)); log_lmdb_state(LOG_ERR, "trust DB delete", rc); return rc == MDB_MAP_FULL ? WRITE_DB_MAP_FULL : 3; } if ((rc = mdb_txn_commit(txn))) { if (rc == MDB_MAP_FULL) msg(LOG_ERR, "mdb_txn_commit hit MDB_MAP_FULL while clearing trust DB"); else msg(LOG_ERR, "mdb_txn_commit while clearing trust DB: %s", mdb_strerror(rc)); log_lmdb_state(LOG_ERR, "trust DB delete commit", rc); return rc == MDB_MAP_FULL ? WRITE_DB_MAP_FULL : 4; } return 0; } /* * trust_db_record_from_line - parse one backend snapshot line. * @buff: mutable line buffer, converted into key and value strings. * @record: parsed record pointing into @buff. * * Backend records are "path source size digest". The path may contain spaces, * so parsing starts from the end and finds the three metadata delimiters. * * Returns 0 on success or 1 for malformed input. */ static int trust_db_record_from_line(char *buff, struct trust_db_record_input *record) { char *end; char *delim = NULL; int delims = 0; int size; end = fapolicyd_strnchr(buff, '\n', BUFFER_SIZE); if (end == NULL) { msg(LOG_ERR, "Too long line?"); return 1; } size = end - buff; *end = '\0'; for (int i = size - 1 ; i >= 0 ; i--) { if (isspace((unsigned char)buff[i])) { delim = &buff[i]; delims++; } if (delims >= MAX_DELIMS) { buff[i] = '\0'; break; } } if (delim == NULL) { msg(LOG_ERR, "Malformed backend record: %s", buff); return 1; } record->idx = buff; record->idx_len = delim - buff; record->data = delim + 1; return 0; } /* * do_memfd_update - Populate the LMDB trust database from a backend memfd. * * Full rebuilds used to call write_db() once per backend record, which meant * one LMDB write transaction and one freelist/metadata commit per path. During * reload storms that can advance LMDB's high-water page number even when the * active trust set is unchanged. Import records in bounded chunks instead: * large enough to avoid per-record commit churn, but small enough that a * single transaction does not become the next map-pressure problem. * * Returns 0 when all records write successfully, 1 when reading fails, or a * WRITE_DB_* code when LMDB reports an import failure. */ static int do_memfd_update_to_dbi_in_env(MDB_env *target_env, int memfd, MDB_dbi target_dbi, unsigned int maxkeysize, long *entries) { int rc = 0; *entries = 0; struct stat sb; char buff[BUFFER_SIZE]; fd_fgets_state_t *st = fd_fgets_init(); MDB_txn *txn = NULL; unsigned int txn_records = 0; unsigned long txns_committed = 0; if (st == NULL) { msg(LOG_ERR, "Failed to initialize buffered memfd reader"); return 1; } // On any failure, fall back to descriptor based reads lseek(memfd, 0, SEEK_SET); /* rewind in case */ if (fstat(memfd, &sb) == 0) { void *base = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, memfd, 0); if (base != MAP_FAILED) fd_setvbuf_r(st,base,sb.st_size,MEM_MMAP_FILE); } do { int res = fd_fgets_r(st, buff, sizeof(buff), memfd); if (res == -1) { msg(LOG_ERR, "fd_fgets_r on memfd (%s)", strerror(errno)); rc = 1; break; } else if (res > 0) { struct trust_db_record_input record; (*entries)++; if (trust_db_record_from_line(buff, &record)) continue; if (txn == NULL) { res = trust_db_begin_write_txn_for_env( target_env, &txn, "bulk trust DB import"); if (res) { rc = res; break; } } res = trust_db_put_record_with_max(txn, target_dbi, &record, maxkeysize, "bulk trust DB import"); if (res) { abort_transaction(txn); txn = NULL; if (target_env == env) log_lmdb_state(LOG_ERR, "bulk trust DB import", res == WRITE_DB_MAP_FULL ? MDB_MAP_FULL : 0); msg(LOG_ERR, "Error (%d) writing key=\"%s\" data=\"%s\"", res, record.idx, record.data); if (rc == 0) rc = res; break; } txn_records++; if (txn_records >= TRUST_DB_REBUILD_TXN_RECORDS) { res = trust_db_commit_write_txn_for_env( target_env, &txn, "bulk trust DB import"); if (res) { rc = res; break; } txns_committed++; txn_records = 0; } } } while (!fd_fgets_eof_r(st) && !stop); if (txn != NULL) { if (rc == 0 && !stop) { rc = trust_db_commit_write_txn_for_env(target_env, &txn, "bulk trust DB import"); if (rc == 0) txns_committed++; } else { abort_transaction(txn); txn = NULL; } } if (rc == 0 && !stop && txns_committed) msg(LOG_DEBUG, "Trust database bulk import committed %ld records in %lu transactions", *entries, txns_committed); fd_fgets_destroy(st); // calls munmap, memfd is closed by backend_close return rc; } static int do_memfd_update_to_dbi(int memfd, MDB_dbi target_dbi, long *entries) { return do_memfd_update_to_dbi_in_env(env, memfd, target_dbi, MDB_maxkeysize, entries); } int do_memfd_update(int memfd, long *entries) { struct trust_db_generation *gen; int rc; gen = trust_db_generation_acquire(); if (gen == NULL) return 2; rc = do_memfd_update_to_dbi(memfd, gen->handle, entries); trust_db_generation_release(gen); if (rc == 0) refresh_active_generation_metadata(); return rc; } /* * create_database - Populate the LMDB trust database from loaded backends. * @with_sync: Non-zero forces an mdb_env_sync call to flush data immediately * after populating the records. A zero value leaves flushing to * the environment's normal durability policy. * * Each backend in the manager exposes its cached data through a memfd * snapshot. The function iterates over every backend and imports records * using do_memfd_update(), which writes in bounded LMDB transactions during * full rebuilds. Processing stops early when the global stop flag becomes * true or a backend import fails. * * Returns 0 when no backend reports an error and stop is not signaled. * Non-zero indicates that processing was interrupted or that a helper * reported a failure while storing records. Helper routines log detailed * errors. */ static int create_database_for_generation(struct trust_db_generation *gen, int with_sync, conf_t *config) { msg(LOG_INFO, "Creating trust database"); int rc = 0; int retries = 0; for (;;) { log_lmdb_state(LOG_DEBUG, "trust DB rebuild before import", 0); for (backend_entry *be = backend_get_first(); be != NULL && !stop; be = be->next ) { msg(LOG_INFO, "Loading trust data from %s backend", be->backend->name); if (be->backend->memfd != -1) { rc = do_memfd_update_to_dbi(be->backend->memfd, gen->handle, &be->backend->entries); if (rc) { msg(LOG_ERR, "Failed to import trust data from %s backend", be->backend->name); break; } } } log_lmdb_state(rc ? LOG_ERR : LOG_DEBUG, "trust DB rebuild after import", rc == WRITE_DB_MAP_FULL ? MDB_MAP_FULL : 0); if (rc == WRITE_DB_MAP_FULL && config->do_auto_db_sizing && retries == 0) { int grown; lock_update_thread(); grown = grow_map_after_full(config); unlock_update_thread(); if (grown == 0 && delete_all_entries_db(gen->handle) == 0) { retries++; rc = 0; continue; } } break; } if (stop) return 1; // Flush everything to disk if (with_sync) mdb_env_sync(env, 1); return rc; } static int create_database(int with_sync, conf_t *config) { struct trust_db_generation *gen; int rc; gen = trust_db_generation_acquire(); if (gen == NULL) return 2; rc = create_database_for_generation(gen, with_sync, config); trust_db_generation_release(gen); if (rc == 0) { refresh_active_generation_metadata(); check_db_size(config); } return rc; } /* * create_candidate_generation - open an empty named DB for the next rebuild. * @candidate: Destination for the new candidate generation. * * Returns 0 on success or an LMDB/errno-style error code. */ static int create_candidate_generation(struct trust_db_generation **candidate) { struct trust_db_generation *gen; char name[TRUST_DB_GENERATION_NAME_SIZE]; MDB_txn *txn = NULL; unsigned long generation; int rc; pthread_mutex_lock(&generation_lock); generation = next_generation; rc = trust_db_candidate_name(name); pthread_mutex_unlock(&generation_lock); if (rc) return rc; gen = trust_db_generation_alloc(generation, name); if (gen == NULL) return ENOMEM; rc = mdb_txn_begin(env, NULL, 0, &txn); if (rc) goto out_free; rc = mdb_dbi_open(txn, gen->name, MDB_CREATE|MDB_DUPSORT, &gen->handle); if (rc) goto out_abort; rc = mdb_txn_commit(txn); txn = NULL; if (rc) goto out_free; rc = delete_all_entries_db(gen->handle); if (rc) goto out_drop; *candidate = gen; return 0; out_abort: mdb_txn_abort(txn); out_free: free(gen); return rc; out_drop: drop_generation_database(gen); free(gen); return rc; } /* * drop_candidate_generation - remove an unpublished candidate DB. * @candidate: Candidate generation that must not become visible to readers. * * Returns nothing. */ static void drop_candidate_generation(struct trust_db_generation *candidate) { int rc; if (candidate == NULL) return; rc = drop_generation_database(candidate); if (rc) msg(LOG_WARNING, "Could not drop failed trust DB generation %lu (%s): %s", candidate->generation, candidate->name, mdb_strerror(rc)); free(candidate); } /* * refresh_active_generation_metadata - rewrite metadata for the active DB. * * Returns 0 on success or an LMDB error code. This is used after startup or * direct single-generation imports so reports contain current entry counts. */ static int refresh_active_generation_metadata(void) { struct trust_db_generation *gen; MDB_txn *txn = NULL; int rc; gen = trust_db_generation_acquire(); if (gen == NULL) return EINVAL; rc = mdb_txn_begin(env, NULL, 0, &txn); if (rc) goto out_release; rc = trust_db_metadata_write(txn, gen); if (rc) { mdb_txn_abort(txn); goto out_release; } rc = mdb_txn_commit(txn); out_release: trust_db_generation_release(gen); return rc; } /* * publish_candidate_generation - publish a rebuilt trust DB generation. * @candidate: Fully populated candidate generation. * * Returns 0 on success. On failure the active generation is unchanged and the * caller still owns @candidate. */ static int publish_candidate_generation(struct trust_db_generation *candidate) { struct trust_db_generation *old; MDB_txn *txn = NULL; int rc; candidate->publish_time = time(NULL); rc = mdb_txn_begin(env, NULL, 0, &txn); if (rc) return rc; rc = trust_db_metadata_write(txn, candidate); if (rc) { mdb_txn_abort(txn); return rc; } rc = mdb_txn_commit(txn); if (rc) return rc; pthread_mutex_lock(&generation_lock); old = active_generation; active_generation = candidate; dbi = candidate->handle; if (next_generation <= candidate->generation) next_generation = candidate->generation + 1; if (old) { old->retired_time = time(NULL); old->next = retired_generations; retired_generations = old; } pthread_mutex_unlock(&generation_lock); msg(LOG_INFO, "Published trust DB generation %lu (%s) with %ld entries", candidate->generation, candidate->name, candidate->entries); trust_db_reclaim_retired(); return 0; } /* * check_data_presence - Look up an LMDB record and compare its stored data. * @read: active read handle. * @index: Key used for the LMDB lookup. * @data: Data string expected to be present for the key. * @matched: Updated with the number of duplicate records inspected. * * Returns 1 when an exact match is discovered, or 0 if the supplied data * cannot be located. Errors encountered by the LMDB read are logged separately. */ static int check_data_presence(struct trust_db_read_handle *handle, const char *index, const char *data, int *matched) { int found = 0; int error; char *record; int operation = READ_DATA; int cnt = 0; while (1) { error = 0; record = NULL; record = trust_db_read_record(handle, index, operation, &error); if (error) msg(LOG_DEBUG, "Error when reading from DB!"); if (!record) break; // check strings if (strcmp(data, record) == 0) { found = 1; } free(record); cnt++; if (found) break; if (operation == READ_DATA) operation = READ_DATA_DUP; } *matched = cnt; return found; } long backend_added_entries = 0; /* * check_from_memfd - Compare backend memfd contents with the LMDB database. * @read: active read handle used for all lookups. * @memfd: File descriptor providing newline-delimited backend records. * @entries: Location where the number of processed records is stored. * * Returns the number of discrepancies discovered between backend data and * the local LMDB copy while incrementing backend_added_entries for newly * observed records. Logs diagnostic information for missing or mismatched * entries. */ static long check_from_memfd(struct trust_db_read_handle *read, int memfd, long *entries) { *entries = 0; long problems = 0; struct stat sb; char buff[BUFFER_SIZE]; fd_fgets_state_t *st = fd_fgets_init(); if (st == NULL) { msg(LOG_ERR, "Failed to initialize buffered memfd reader"); return 1; } // On any failure, fall back to descriptor based reads lseek(memfd, 0, SEEK_SET); /* rewind in case */ if (fstat(memfd, &sb) == 0) { void *base = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, memfd, 0); if (base != MAP_FAILED) fd_setvbuf_r(st,base,sb.st_size,MEM_MMAP_FILE); } do { int res = fd_fgets_r(st, buff, sizeof(buff), memfd); if (res == -1) { msg(LOG_ERR, "fd_fgets_r on memfd (%s)", strerror(errno)); break; } else if (res > 0) { (*entries)++; char *end = fapolicyd_strnchr(buff, '\n', BUFFER_SIZE); if (end == NULL) { msg(LOG_ERR, "Too long line?"); continue; } int size = end - buff; *end = '\0'; // its better to parse it from the end because // there can be space in file name int delims = 0; char *delim = NULL; for (int i = size-1 ; i >= 0 ; i--) { if (isspace((unsigned char)buff[i])) { delim = &buff[i]; delims++; } if (delims >= MAX_DELIMS) { buff[i] = '\0'; break; } } if (delim == NULL) { msg(LOG_ERR, "Malformed backend record: %s", buff); continue; } // We have everything, now do the check char *index = buff; char *data = delim + 1; int matched = 0; int found = check_data_presence(read, index, data, &matched); if (!found) { problems++; // missing in db // recently added file if (matched == 0) { msg(LOG_DEBUG, "%s is not in the trust database", index); backend_added_entries++; } // updated file // data miscompare if (matched > 0) { msg(LOG_DEBUG, "Trust data miscompare for %s", index); } } } } while (!fd_fgets_eof_r(st) && !stop); fd_fgets_destroy(st); // calls munmap, memfd is closed by backend_close return problems; } /* * check_database_copy - Validate LMDB contents against backend snapshots. * * Iterates each backend and invokes check_from_memfd to compare the cached * backend view with the local LMDB store. Summaries of the totals and * detected discrepancies are logged for diagnostics. * * Returns 0 when the databases agree, 1 when differences or an early stop are * encountered, and -1 when an unrecoverable error occurs. */ static int check_database_copy(const conf_t *config) { struct trust_db_read_handle read; msg(LOG_INFO, "Checking if the trust database up to date"); if (trust_db_read_open(&read)) return -1; long problems = 0; long backend_total_entries = 0; backend_added_entries = 0; for (backend_entry *be = backend_get_first(); be != NULL && !stop; be = be->next) { msg(LOG_INFO, "Importing trust data from %s backend", be->backend->name); if (be->backend->memfd != -1) { problems += check_from_memfd(&read, be->backend->memfd, &be->backend->entries); backend_total_entries += be->backend->entries; } else { msg(LOG_ERR, "%s backend does not provide a memfd snapshot", be->backend->name); problems++; } } trust_db_read_close(&read); if (stop) return 1; long db_total_entries = get_number_of_entries(); // Is something wrong? if (db_total_entries == -1) return -1; msg(LOG_INFO, "Entries in trust DB: %ld", db_total_entries); // Check if database is getting full and warn check_db_size(config); msg(LOG_INFO, "Loaded trust info from all backends (without duplicates): %ld", backend_total_entries); // do not print 0 if (backend_added_entries > 0) msg(LOG_INFO, "New trust database entries: %ld", backend_added_entries); // db contains records that are not present in backends anymore long removed = labs(db_total_entries - (backend_total_entries - backend_added_entries)); // do not print 0 if (removed > 0) msg(LOG_INFO, "Removed trust database entries: %ld", removed); problems += removed; if (problems) { msg(LOG_WARNING, "Found %ld problematic trust database entries", problems); return 1; } else msg(LOG_INFO, "Trust database checks OK"); return 0; } /* * This function removes the trust database files. */ int unlink_db(void) { int rc, ret_val = 0; char path[64]; snprintf(path, sizeof(path), "%s/data.mdb", data_dir); rc = unlink(path); if (rc == -1 && errno != ENOENT) { msg(LOG_ERR, "Could not unlink %s (%s)", path, strerror(errno)); ret_val = 1; } snprintf(path, sizeof(path), "%s/lock.mdb", data_dir); rc = unlink(path); if (rc == -1 && errno != ENOENT) { msg(LOG_ERR, "Could not unlink %s (%s)", path, strerror(errno)); ret_val = 1; } snprintf(path, sizeof(path), "%s/db.ver", data_dir); rc = unlink(path); if (rc == -1 && errno != ENOENT) { msg(LOG_ERR, "Could not unlink %s (%s)", path, strerror(errno)); ret_val = 1; } return ret_val; } /* * DB version 1 = unique keys (0.8 - 0.9.2) * DB version 2 = allow duplicate keys (0.9.3 - ) * * This function is used to detect if we are using version1 of the database. * If so, we have to delete the database and rebuild it. We cannot mix * database versions because lmdb doesn't do that. * Returns 0 success and 1 for failure. */ static int migrate_database(void) { int fd; char vpath[64]; snprintf(vpath, sizeof(vpath), "%s/db.ver", data_dir); fd = open(vpath, O_RDONLY); if (fd < 0) { msg(LOG_INFO, "Trust database migration will be performed."); // Then we have a version1 db since it does not track versions if (unlink_db()) return 1; // Create the new, db version tracker and write current version fd = open(vpath, O_CREAT|O_EXCL|O_WRONLY, 0640); if (fd < 0) { msg(LOG_ERR, "Failed writing db version %s", strerror(errno)); return 1; } write(fd, "2", 1); close(fd); return 0; } else { // We have a version file, read it and check the version int rc = read(fd, vpath, 2); close(fd); if ((rc > 0) && (vpath[0] == '2')) return 0; } return 1; } /* * This function is responsible for getting the database ready to use. * It will first check to see if a database is populated. If so, then * it will verify it against the backend database just in case something * has changed. If the database does not exist, then it will create one. * It returns 0 on success and a non-zero on failure. */ int init_database(conf_t *config) { int rc; msg(LOG_INFO, "Initializing the trust database"); startup_compaction_target_mb = 0; startup_compaction_fallback_mb = 0; rc = database_update_controls_init(); if (rc) { msg(LOG_ERR, "Failed to initialize database update controls (%d)", rc); return rc; } if (migrate_database()) return 1; /* One-shot utilisation-driven sizing */ if (config->do_auto_db_sizing && autosize_database(config, AUTOSIZE_STARTUP_INSPECTION)) msg(LOG_INFO, "autosize: map size recomputed to %u MiB", config->db_max_size); if ((rc = backend_init(config))) { msg(LOG_ERR, "Failed to load trust data from backend (%d)", rc); return rc; } if ((rc = backend_load(config))) { msg(LOG_ERR, "Failed to load data from backend (%d)", rc); backend_close(); return rc; } if (startup_compaction_target_mb) { rc = compact_trust_database_at_startup(config); if (rc) { backend_close(); return rc; } } if (env == NULL && (rc = init_db(config))) { msg(LOG_ERR, "Cannot open the trust database, init_db() (%d)", rc); backend_close(); return rc; } rc = database_empty(); if (rc > 0) { if ((rc = create_database(/*with_sync*/1, config))) { msg(LOG_ERR, "Failed to create trust database, create_database() (%d)", rc); close_db(0); return rc; } } else { // check if our internal database is synced rc = check_database_copy(config); if (rc > 0) { rc = update_database(config, 1); if (rc) msg(LOG_ERR, "Failed updating the trust database"); } } // Conserve memory by dumping unneeded resources backend_close(); if (rc == 0) rc = database_update_thread_start(config); return rc; } /* * read_trust_db - run trust lookup and optional integrity checks. * @read: private LMDB read handle for this lookup. * @lookup: path, file info, fd and error return storage. * * Returns 0 when no data is found or integrity failed, and 1 when the file is * found and trustworthy. Callers must check lookup->error before trusting the * result because not-found and integrity failure are both untrusted. */ static int read_trust_db(struct trust_db_read_handle *read, struct trust_db_lookup *lookup) { int do_integrity = 0, mode = READ_TEST_KEY; integrity_t integrity = decision_config_integrity(NULL); unsigned int rpm_sha256_only = decision_config_rpm_sha256_only(NULL); char *res; int retry = 0; char sha_xattr[FILE_DIGEST_STRING_MAX]; char calc_digest[FILE_DIGEST_STRING_MAX]; struct lmdb_record record; const char *path = lookup->path; struct file_info *info = lookup->info; int fd = lookup->fd; int *error = lookup->error; file_hash_alg_t calc_alg = FILE_HASH_ALG_NONE; file_hash_alg_t ima_alg = FILE_HASH_ALG_NONE; int have_ima_hash = 0; int ima_rehash_attempted = 0; if (integrity != IN_NONE && info) { do_integrity = 1; mode = READ_DATA; sha_xattr[0] = 0; // Make sure we can't re-use stack value calc_digest[0] = 0; } retry_res: retry++; if (retry >= 128) { msg(LOG_ERR, "Checked 128 duplicates for %s " "and there is no match. Breaking the cycle.", path); *error = 1; return 0; } res = trust_db_read_record(read, path, mode, error); // For subjects we do a limited check because the process had to // pass some kind of trust check to even be started and we do not // have an open fd to the file. if (!do_integrity) { if (res == NULL) return 0; free(res); return 1; } else { // record not found if (res == NULL) return 0; if (parse_lmdb_record(res, &record)) { free(res); *error = 1; return 1; } // Need to do the compare and free res free(res); // prepare for next reading mode = READ_DATA_DUP; /* * rpm_sha256_only is enforced during RPM ingestion, but a stale * LMDB generation can still contain old SHA1/MD5 records after * a config change or failed rebuild. Digest-based integrity * modes must not trust those records when the floor is enabled. */ if (rpm_sha256_only && (integrity == IN_SHA256 || integrity == IN_IMA) && file_hash_length(record.alg) < SHA256_LEN) goto retry_res; if (integrity == IN_SIZE) { // match! if (record.size == info->size) { return 1; } else { goto retry_res; } } else if (integrity == IN_IMA) { int rc = 1; char *hash = NULL; // read xattr only once, even when weak records are skipped if (!have_ima_hash) { rc = get_ima_hash(fd, &ima_alg, sha_xattr); if (rc) have_ima_hash = 1; } if (!have_ima_hash) { *error = 1; return 0; } if ((record.size == info->size) && (strcmp(record.digest, sha_xattr) == 0)) { file_info_cache_digest(info, ima_alg); strncpy(info->digest, sha_xattr, FILE_DIGEST_STRING_MAX - 1); info->digest[FILE_DIGEST_STRING_MAX - 1] = 0; return 1; } else if (!ima_rehash_attempted && ima_alg != FILE_HASH_ALG_NONE) { ima_rehash_attempted = 1; /* * Rehash using the IMA algorithm to separate * metadata drift from content changes. This maps * the enum to the hashing helper and caches the * result for the FILE_HASH attribute to avoid * repeating the costly recomputation. */ hash = get_hash_from_fd2(fd, info->size, ima_alg); if (hash) { strncpy(calc_digest, hash, FILE_DIGEST_STRING_MAX - 1); calc_digest[FILE_DIGEST_STRING_MAX - 1] = 0; free(hash); file_info_cache_digest(info, ima_alg); strncpy(info->digest, calc_digest, FILE_DIGEST_STRING_MAX - 1); info->digest[FILE_DIGEST_STRING_MAX - 1] = 0; if ((record.size == info->size) && (strcmp(record.digest, calc_digest) == 0)) return 1; } else { *error = 1; return 0; } } log_ima_mismatch(path, record.alg, ima_alg); goto retry_res; } else if (integrity == IN_SHA256) { /* * The name is historical; recomputation follows the * stored digest algorithm (for example SHA512) while * legacy fragments still default to SHA256 via * parse_lmdb_record(). */ size_t digest_len = file_hash_length(record.alg) * 2; char *hash = NULL; // Calculate once per digest algorithm seen in duplicates. if (calc_alg != record.alg) { hash = get_hash_from_fd2(fd, info->size, record.alg); if (hash) { strncpy(calc_digest, hash, FILE_DIGEST_STRING_MAX - 1); calc_digest[FILE_DIGEST_STRING_MAX - 1] = 0; if (digest_len < FILE_DIGEST_STRING_MAX) calc_digest[digest_len] = 0; free(hash); file_info_cache_digest(info, record.alg); strncpy(info->digest, calc_digest, FILE_DIGEST_STRING_MAX - 1); info->digest[FILE_DIGEST_STRING_MAX - 1] = 0; calc_alg = record.alg; } else { *error = 1; return 0; } } if ((record.size == info->size) && (strcmp(record.digest, calc_digest) == 0)) return 1; else goto retry_res; } } *error = 1; return 0; } /* * check_trust_database_with_read - run trust lookup using caller's read handle. * @read: active trust DB read handle. * @path: filesystem path to check. * @info: optional file metadata for integrity checks. * @fd: optional open file descriptor for integrity checks. * * Returns 1 if trusted, 0 if untrusted, and -1 on read or integrity error. */ static int check_trust_database_with_read(struct trust_db_read_handle *read, const char *path, struct file_info *info, int fd) { int retval = 0, error; int res; struct trust_db_lookup lookup; lookup.path = path; lookup.info = info; lookup.fd = fd; lookup.error = &error; error = 0; res = read_trust_db(read, &lookup); if (error) retval = -1; else if (res) retval = 1; else if (lib64_symlink || lib_symlink || bin_symlink || sbin_symlink) { // If we are on a system that symlinks the top level // directories to /usr, then let's try again without the /usr // dir. There shouldn't be many packages that have this // problem. These are sorted from most likely to least. if (strncmp(path, "/usr/", 5) == 0) { if ((lib64_symlink && strncmp(&path[5], "lib64/", 6) == 0) || (lib_symlink && strncmp(&path[5], "lib/", 4) == 0) || (bin_symlink && strncmp(&path[5], "bin/", 4) == 0) || (sbin_symlink && strncmp(&path[5], "sbin/", 5) == 0)) { // We have a symlink, retry lookup.path = &path[4]; error = 0; res = read_trust_db(read, &lookup); if (error) retval = -1; else if (res) retval = 1; } } } return retval; } /* * check_trust_database - check trust using the daemon's live DB state. * @path: filesystem path to check. * @info: optional file metadata for integrity checks. * @fd: optional open file descriptor for integrity checks. * * This is the normal daemon lookup path. It takes the database update read * lock, opens a read handle on the current generation, and records timing and * trust lookup metrics. fapolicyd-cli uses check_trust_database_readonly() * when it needs a private read-only environment. * * Returns 1 if trusted, 0 if untrusted, and -1 on read or integrity error. */ int check_trust_database(const char *path, struct file_info *info, int fd) { int retval; struct trust_db_read_handle read; struct decision_timing_span lock_timing; struct decision_timing_span read_timing; struct decision_timing_span total_timing; trust_metric_add(&trust_metrics.lookups, 1); decision_timing_trust_db_stage_begin(DECISION_TIMING_TRUST_DB_TOTAL, &total_timing); decision_timing_trust_db_stage_begin( DECISION_TIMING_TRUST_DB_LOCK_WAIT, &lock_timing); database_update_read_lock(); decision_timing_stage_end(&lock_timing); decision_timing_trust_db_stage_begin(DECISION_TIMING_TRUST_DB_READ, &read_timing); if (trust_db_read_open(&read)) { retval = -1; goto out_unlock; } retval = check_trust_database_with_read(&read, path, info, fd); trust_db_read_close(&read); out_unlock: decision_timing_stage_end(&read_timing); database_update_read_unlock(); decision_timing_stage_end(&total_timing); return retval; } /* * database_readonly_lookup_finish - close read-only CLI trust lookup state. * @void: no arguments are required. * * This state is used only by fapolicyd-cli and is separate from the daemon's * live LMDB environment. * * Returns nothing. */ void database_readonly_lookup_finish(void) { if (readonly_lookup_env) mdb_env_close(readonly_lookup_env); readonly_lookup_env = NULL; readonly_lookup_dbi = 0; readonly_lookup_open = 0; } /* * database_readonly_lookup_start - open a private read-only trust DB handle. * @void: no arguments are required. * * This state is used only by fapolicyd-cli. The CLI opens its own read-only * LMDB environment so path checks do not initialize or lock the daemon's live * environment. * * Returns 0 on success or an LMDB error code. */ int database_readonly_lookup_start(void) { MDB_txn *txn = NULL; char active_name[TRUST_DB_GENERATION_NAME_SIZE]; int rc; database_readonly_lookup_finish(); snprintf(active_name, sizeof(active_name), "%s", db); rc = mdb_env_create(&readonly_lookup_env); if (rc) return rc; rc = mdb_env_set_maxdbs(readonly_lookup_env, TRUST_DB_MAX_NAMED_DBS); if (rc) goto error; /* * This stays read-only, but it must use LMDB's reader table. A live * CLI transaction that bypasses locking is invisible to daemon reloads, * which may otherwise drop retired named DBs while the CLI still reads * their old pages. Pass the trust DB mode because a locked read-only * open may recreate lock.mdb when it is missing. */ rc = mdb_env_open(readonly_lookup_env, data_dir, MDB_RDONLY, 0660); if (rc) goto error; rc = mdb_txn_begin(readonly_lookup_env, NULL, MDB_RDONLY, &txn); if (rc) goto error; rc = database_read_active_name(txn, active_name, sizeof(active_name)); if (rc) goto out_abort; rc = mdb_dbi_open(txn, active_name, MDB_DUPSORT, &readonly_lookup_dbi); if (rc) goto out_abort; /* * LMDB DBI handles opened in a transaction stay private until that * transaction commits. Aborting here would close readonly_lookup_dbi * before check_trust_database_readonly() starts its lookup transaction. */ rc = mdb_txn_commit(txn); if (rc) goto error; MDB_maxkeysize = mdb_env_get_maxkeysize(readonly_lookup_env); lib_symlink = is_link("/lib"); lib64_symlink = is_link("/lib64"); bin_symlink = is_link("/bin"); sbin_symlink = is_link("/sbin"); readonly_lookup_open = 1; return 0; out_abort: mdb_txn_abort(txn); error: database_readonly_lookup_finish(); return rc; } /* * check_trust_database_readonly - check trust through read-only CLI LMDB state. * @path: filesystem path to check. * @info: optional file metadata for integrity checks. * @fd: optional open file descriptor for integrity checks. * * This entry point is used only by fapolicyd-cli after * database_readonly_lookup_start(). It deliberately avoids daemon update * locks and daemon LMDB handles. * * Returns 1 if trusted, 0 if untrusted, and -1 on read or integrity error. */ int check_trust_database_readonly(const char *path, struct file_info *info, int fd) { struct trust_db_read_handle read = { 0 }; int retval; int rc; if (!readonly_lookup_open) return -1; rc = mdb_txn_begin(readonly_lookup_env, NULL, MDB_RDONLY, &read.txn); if (rc) { msg(LOG_ERR, "txn_begin:%s", mdb_strerror(rc)); return -1; } rc = mdb_cursor_open(read.txn, readonly_lookup_dbi, &read.cursor); if (rc) { msg(LOG_ERR, "cursor_open:%s", mdb_strerror(rc)); mdb_txn_abort(read.txn); return -1; } retval = check_trust_database_with_read(&read, path, info, fd); mdb_cursor_close(read.cursor); mdb_txn_abort(read.txn); return retval; } void close_database(void) { database_update_thread_stop(); // we can close db when we are really sure update_thread does not exist close_db(1); database_update_controls_destroy(); backend_close(); } /* * database_open_for_tests - Open LMDB for isolated unit test execution. * @config: Configuration providing map size and integrity mode. * * Returns 0 on success or the init_db return code on failure. */ int database_open_for_tests(conf_t *config) { int rc; rc = database_update_controls_init(); if (rc) return rc; return init_db(config); } /* * database_close_for_tests - Close LMDB state opened via test helper API. * * Returns: none. */ void database_close_for_tests(void) { close_db(0); database_update_controls_destroy(); } /* * publish_memfd_for_tests - publish one memfd as a new generation. * @memfd: Backend-style trust records to import into the candidate DB. * @config: Test configuration used for sizing reports. * @startup_rebuild: Non-zero to use startup generation numbering. * * Returns 0 on successful publication or a non-zero import/publish error. */ static int publish_memfd_for_tests(int memfd, conf_t *config, int startup_rebuild) { struct trust_db_generation *candidate = NULL; long entries = 0; int rc; if (startup_rebuild) trust_db_reset_next_generation(1); rc = create_candidate_generation(&candidate); if (rc) return rc; rc = do_memfd_update_to_dbi(memfd, candidate->handle, &entries); if (rc) { drop_candidate_generation(candidate); return rc; } rc = publish_candidate_generation(candidate); if (rc) { drop_candidate_generation(candidate); return rc; } check_db_size(config); request_object_cache_flush(); return 0; } /* * database_publish_memfd_for_tests - publish one memfd as a new generation. * @memfd: Backend-style trust records to import into the candidate DB. * @config: Test configuration used for sizing reports. * * Returns 0 on successful publication or a non-zero import/publish error. */ int database_publish_memfd_for_tests(int memfd, conf_t *config) { return publish_memfd_for_tests(memfd, config, 0); } /* * database_publish_startup_memfd_for_tests - publish a startup rebuild. * @memfd: Backend-style trust records to import into the candidate DB. * @config: Test configuration used for sizing reports. * * Returns 0 on successful publication or a non-zero import/publish error. */ int database_publish_startup_memfd_for_tests(int memfd, conf_t *config) { return publish_memfd_for_tests(memfd, config, 1); } /* * database_nested_lookup_for_tests - start a lookup while one read txn is open. * @path: Path expected to exist in the active trust database. * * Returns the nested check_trust_database() result, or -1 when the setup read * transaction cannot be opened. */ int database_nested_lookup_for_tests(const char *path) { struct trust_db_read_handle read; int rc; if (trust_db_read_open(&read)) return -1; rc = check_trust_database(path, NULL, -1); trust_db_read_close(&read); return rc; } /* * database_drop_candidate_after_import_for_tests - simulate rebuild failure. * @memfd: Backend-style trust records to import before dropping candidate. * * Returns the import result. The candidate generation is always unpublished. */ int database_drop_candidate_after_import_for_tests(int memfd) { struct trust_db_generation *candidate = NULL; long entries = 0; int rc; rc = create_candidate_generation(&candidate); if (rc) return rc; rc = do_memfd_update_to_dbi(memfd, candidate->handle, &entries); drop_candidate_generation(candidate); return rc; } /* * database_compact_memfd_for_tests - swap a rebuilt environment from memfd. * @memfd: Backend-style trust records for the replacement environment. * @config: Test configuration used for map sizing. * * Returns 0 on successful environment replacement. This bypasses the backend * manager but still exercises offline build, read-back validation, controlled * close/swap/reopen, generation publication, and old-environment preservation * on failures in the same code paths used by production. */ int database_compact_memfd_for_tests(int memfd, conf_t *config) { struct offline_lmdb candidate = { .trust_generation = trust_db_next_generation_number(), .env_generation = lmdb_environment_next_generation(), }; int rc; rc = open_offline_lmdb(&candidate, config); if (rc) goto out_failure; rc = do_memfd_update_to_dbi_in_env(candidate.env, memfd, candidate.trust_dbi, candidate.maxkeysize, &candidate.entries); if (rc) goto out_close; rc = write_offline_metadata(&candidate); if (rc) goto out_close; rc = mdb_env_sync(candidate.env, 1); if (rc) goto out_close; mdb_env_close(candidate.env); candidate.env = NULL; rc = validate_offline_lmdb(&candidate); if (rc) goto out_remove; rc = publish_offline_lmdb(config, &candidate); if (rc) goto out_remove; remove_lmdb_environment_dir(candidate.tmpdir); check_db_size(config); request_object_cache_flush(); return 0; out_close: if (candidate.env) mdb_env_close(candidate.env); out_remove: remove_lmdb_environment_dir(candidate.tmpdir); out_failure: return rc; } void *database_generation_hold_for_tests(void) { return trust_db_generation_acquire(); } void database_generation_release_for_tests(void *cookie) { trust_db_generation_release(cookie); } void database_reclaim_generations_for_tests(void) { trust_db_reclaim_retired(); } /* * database_generation_snapshot - copy trust DB generation state. * @report: Destination report snapshot. * * Returns 0. */ int database_generation_snapshot(database_generation_report_t *report) { trust_db_generation_report_snapshot(report); return 0; } int database_generation_report_for_tests( database_generation_test_report_t *report) { return database_generation_snapshot(report); } /* * database_autosize_target_mb_for_tests - compute autosize target. * @active_pages: Pages used by the published trust DB. * @env_allocated_pages: LMDB environment high-water pages. * @map_pages: Current map size in pages. * @page_size: LMDB page size. * * Returns the recommended map size in MiB for unit tests that exercise sizing * policy without creating very large LMDB fixtures. */ unsigned int database_autosize_target_mb_for_tests(unsigned long active_pages, unsigned long env_allocated_pages, unsigned long map_pages, unsigned long page_size) { struct trust_db_sizing_state state; memset(&state, 0, sizeof(state)); state.active_pages = active_pages; state.allocated_pages = env_allocated_pages; state.map_pages = map_pages; state.page_size = page_size; state.current_mb = pages_to_mb(map_pages, page_size); complete_lmdb_sizing_state(&state); return autosize_effective_target_mb(&state, AUTOSIZE_RELOAD_PREFLIGHT); } /* * database_autosize_retry_mb_for_tests - compute auto retry grow target. * @old_mb: Current configured map size. * @active_pages: Pages used by the published trust DB. * @map_pages: Current live LMDB map size in pages. * @page_size: LMDB page size. * * Returns the MDB_MAP_FULL retry target used by auto sizing. */ unsigned int database_autosize_retry_mb_for_tests(unsigned long old_mb, unsigned long active_pages, unsigned long map_pages, unsigned long page_size) { struct trust_db_sizing_state state; memset(&state, 0, sizeof(state)); state.active_pages = active_pages; state.allocated_pages = active_pages; state.map_pages = map_pages; state.page_size = page_size; state.current_mb = pages_to_mb(map_pages, page_size); complete_lmdb_sizing_state(&state); return autosize_reload_grow_from_state_mb(old_mb, &state); } /* * update_database - reload backend snapshots into a new trust DB generation. * @config: Active daemon configuration. * @startup_rebuild: Non-zero when init_database() is rebuilding at startup. * * It returns 0 on success and non-zero on error. */ static int update_database(conf_t *config, int startup_rebuild) { int rc; struct trust_db_generation *candidate = NULL; msg(LOG_INFO, "Updating trust database"); msg(LOG_DEBUG, "Loading trust database backends"); /* * backend loading/reloading should be done in upper level */ if (stop) return 1; lock_update_thread(); log_lmdb_state(LOG_DEBUG, "trust DB reload start", 0); rc = autosize_reload_preflight(config); if (rc) { msg(LOG_ERR, "Trust database reload preflight failed (%d)", rc); unlock_update_thread(); return UPDATE_DB_PRESERVED; } log_lmdb_state(LOG_DEBUG, "trust DB reload after preflight", 0); unlock_update_thread(); if (startup_rebuild) trust_db_reset_next_generation(1); rc = create_candidate_generation(&candidate); if (rc) { msg(LOG_ERR, "Cannot create candidate trust DB generation (%d)", rc); return UPDATE_DB_PRESERVED; } if (stop) { drop_candidate_generation(candidate); return 1; } if (!stop) rc = create_database_for_generation(candidate, /*with_sync*/0, config); else rc = 1; log_lmdb_state(rc ? LOG_ERR : LOG_DEBUG, "trust DB reload after rebuild", rc == WRITE_DB_MAP_FULL ? MDB_MAP_FULL : 0); if (rc) { msg(LOG_ERR, "Failed to create the trust database (%d)", rc); drop_candidate_generation(candidate); return UPDATE_DB_PRESERVED; } rc = publish_candidate_generation(candidate); if (rc) { msg(LOG_ERR, "Failed to publish trust DB generation: %s", mdb_strerror(rc)); drop_candidate_generation(candidate); return UPDATE_DB_PRESERVED; } check_db_size(config); if (!stop) request_object_cache_flush(); mdb_env_sync(env, 1); return 0; } /* * record_trust_reload_failure - count a failed trust database reload. * @void: no arguments are required. * * Failed trust reloads currently keep compatibility behavior. The counter * gives later high-security profiles a single place to drive fail-closed or * degraded decisions. * * Returns nothing. */ static void record_trust_reload_failure(void) { failure_action_record(FAILURE_REASON_TRUST_RELOAD_FAILURE); } /* * database_reload_from_backends - reload backend snapshots into LMDB. * @config: active daemon configuration. * * The update controller owns request coalescing and FIFO dispatch. This * function owns the database work behind a reload: refreshing backend * snapshots, applying any auto-size plan, and publishing a complete LMDB * trust DB generation only after the rebuild succeeds. * * Returns 0 on success and non-zero when the previous DB was preserved or a * fatal reload error occurred. */ int database_reload_from_backends(conf_t *config) { msg(LOG_INFO, "It looks like there was an update of the system... Syncing DB."); int rc = 0; unsigned int old_db_max_size = config->db_max_size; unsigned int old_reload_floor_mb = autosize_reload_floor_mb; backend_close(); /* One-shot utilisation-driven sizing */ if (config->do_auto_db_sizing && autosize_database(config, AUTOSIZE_LIVE_INSPECTION)) { msg(LOG_INFO, "autosize: map size recomputed to %u MiB", config->db_max_size); if (config->db_max_size < old_db_max_size) { unsigned long old_trust_generation; unsigned long old_env_generation; /* * This is a map-size reopen, not controlled * compaction. If high-water usage needs physical file * rewrite, status reports recommend the explicit admin * compaction command instead of hiding a swap in reload. */ old_trust_generation = trust_db_current_generation_number(); lmdb_environment_snapshot(&old_env_generation, NULL); if (old_env_generation == 0) old_env_generation = 1; lock_update_thread(); close_env(1); rc = init_db_with_generations(config, old_trust_generation, old_env_generation); if (rc) { int reopen_rc; msg(LOG_ERR, "Cannot open the resized trust database, init_db() (%d)", rc); config->db_max_size = old_db_max_size; autosize_reload_floor_mb = old_reload_floor_mb; reopen_rc = init_db_with_generations(config, old_trust_generation, old_env_generation); if (reopen_rc) { msg(LOG_ERR, "Cannot reopen previous trust database, init_db() (%d)", reopen_rc); rc = reopen_rc; } else msg(LOG_ERR, "Previous trust database preserved after reload sizing failure"); } unlock_update_thread(); if (rc) { if (stop) goto out; record_trust_reload_failure(); goto out; } } else if (config->db_max_size > old_db_max_size) { lock_update_thread(); rc = mdb_env_set_mapsize(env, (size_t)config->db_max_size * MEGABYTE); unlock_update_thread(); if (rc) { config->db_max_size = old_db_max_size; autosize_reload_floor_mb = old_reload_floor_mb; msg(LOG_ERR, "env_set_mapsize error: %s", mdb_strerror(rc)); record_trust_reload_failure(); goto out; } } } if ((rc = backend_init(config))) { msg(LOG_ERR, "Failed to load trust data from backend (%d)", rc); record_trust_reload_failure(); goto out; } if ((rc = backend_load(config))) { msg(LOG_ERR, "Failed to load data from backend (%d)", rc); record_trust_reload_failure(); goto out; } if ((rc = update_database(config, 0))) { msg(LOG_ERR, "Cannot update trust database!"); if (stop) goto out; record_trust_reload_failure(); if (rc == UPDATE_DB_PRESERVED) { msg(LOG_ERR, "Previous trust database preserved after reload failure"); goto out; } msg(LOG_ERR, "Trust database reload failed with active DB under daemon control"); goto out; } msg(LOG_INFO, "Updated"); out: // Conserve memory backend_close(); return rc; } /* * database_reload_for_tests - run the trust reload path directly. * @config: test configuration containing backend and LMDB settings. * * Returns 0 on success and non-zero when reload failed. */ int database_reload_for_tests(conf_t *config) { return database_reload_from_backends(config); } /* * database_compact_from_backends - perform admin trust DB compaction. * @config: active daemon configuration. * * This is intentionally separate from database_reload_from_backends(). A * reload publishes a logical trust DB generation inside the current LMDB * environment; compaction rebuilds a whole replacement environment and briefly * closes the live one after the replacement has validated. Keeping the entry * points separate makes operator intent clear in logs and avoids hidden * filesystem swaps on SIGHUP. */ void database_compact_from_backends(conf_t *config) { int rc; msg(LOG_INFO, "Admin requested trust DB LMDB compaction"); backend_close(); if ((rc = backend_init(config))) { msg(LOG_ERR, "Failed to load trust data from backend (%d)", rc); record_trust_reload_failure(); goto out; } if ((rc = backend_load(config))) { msg(LOG_ERR, "Failed to load data from backend (%d)", rc); record_trust_reload_failure(); goto out; } rc = compact_trust_database(config, "admin"); if (rc) { record_trust_reload_failure(); msg(LOG_ERR, "Trust DB compaction request failed; previous environment preserved"); } else { msg(LOG_INFO, "Trust DB compaction request completed"); } out: backend_close(); } /*********************************************************************** * This section of functions are used by the command line utility to * iterate across the database to verify each entry. It will be a read * only operation. ***********************************************************************/ static walkdb_entry_t wdb_entry; static MDB_env *walk_env; static MDB_txn *walk_txn; static MDB_cursor *walk_cursor; /* * walk_database_set_entry_type - classify the current walker key. * * Long paths are written as a SHA-512 hex string including its terminating * NUL. Direct path keys never include a NUL, so the stored size and trailing * byte let the CLI avoid treating a digest as a filesystem pathname. * * Returns nothing. */ static void walk_database_set_entry_type(void) { const char *path = wdb_entry.path.mv_data; wdb_entry.path_is_hashed = wdb_entry.path.mv_size == (SHA512_LEN * 2) + 1 && path && path[wdb_entry.path.mv_size - 1] == '\0'; } /* * walk_database_reset - close private read-only CLI walk state. * @void: no arguments are required. * * The walker owns these handles. They are intentionally separate from the * daemon's global LMDB environment so cleanup here never closes or mutates * daemon state. * * Returns nothing. */ static void walk_database_reset(void) { if (walk_cursor) mdb_cursor_close(walk_cursor); if (walk_txn) mdb_txn_abort(walk_txn); if (walk_env) mdb_env_close(walk_env); walk_cursor = NULL; walk_txn = NULL; walk_env = NULL; memset(&wdb_entry, 0, sizeof(wdb_entry)); } /* * walk_database_start - open the trust DB for CLI verification. * @config: Unused legacy argument kept for the CLI walker API. * * This must stay a read-only walk. Do not call init_db() or * trust_db_read_open() here: those paths open the daemon-style environment and * may take write-side LMDB state while setting up handles. The CLI verifier * only needs a private read transaction and cursor over the active named DB. * * Returns WALK_DATABASE_SUCCESS, WALK_DATABASE_EMPTY, or WALK_DATABASE_ERROR. */ int walk_database_start(conf_t *config) { MDB_dbi walk_dbi; char active_name[TRUST_DB_GENERATION_NAME_SIZE]; int rc; (void)config; walk_database_reset(); snprintf(active_name, sizeof(active_name), "%s", db); rc = mdb_env_create(&walk_env); if (rc) { printf("Cannot create the trust database environment\n"); return WALK_DATABASE_ERROR; } rc = mdb_env_set_maxdbs(walk_env, TRUST_DB_MAX_NAMED_DBS); if (rc) goto error; /* * This stays read-only, but it must use LMDB's reader table. The walk * keeps one transaction open across iteration, so MDB_NOLOCK would let a * daemon reload reclaim old named DB pages behind the verifier's cursor. * Pass the trust DB mode because a locked read-only open may recreate * lock.mdb when it is missing. */ rc = mdb_env_open(walk_env, data_dir, MDB_RDONLY, 0660); if (rc) goto error; rc = mdb_txn_begin(walk_env, NULL, MDB_RDONLY, &walk_txn); if (rc) goto error; rc = database_read_active_name(walk_txn, active_name, sizeof(active_name)); if (rc) goto error; rc = mdb_dbi_open(walk_txn, active_name, MDB_DUPSORT, &walk_dbi); if (rc) goto error; rc = mdb_cursor_open(walk_txn, walk_dbi, &walk_cursor); if (rc) goto error; /* * Keep walk_txn open for the cursor lifetime. The returned MDB_val * buffers point into LMDB-managed memory and remain valid until the * cursor/transaction is closed by walk_database_reset(). */ rc = mdb_cursor_get(walk_cursor, &wdb_entry.path, &wdb_entry.data, MDB_FIRST); if (rc == 0) { walk_database_set_entry_type(); /* Keep handles open so walk_database_next() can continue. */ return WALK_DATABASE_SUCCESS; } if (rc == MDB_NOTFOUND) { printf("The trust database is empty - nothing to do\n"); walk_database_reset(); return WALK_DATABASE_EMPTY; } error: if (rc) puts(mdb_strerror(rc)); walk_database_reset(); return WALK_DATABASE_ERROR; } /* * walk_database_get_entry - return the current walker entry. * @void: no arguments are required. * * The entry's MDB_val buffers are owned by LMDB and are valid until the next * walk_database_next() call or walk_database_finish(). * * Returns a pointer to the current walker entry. */ walkdb_entry_t *walk_database_get_entry(void) { return &wdb_entry; } /* * walk_database_next - advance the CLI verification cursor. * @void: no arguments are required. * * Returns WALK_DATABASE_NEXT for another entry, WALK_DATABASE_DONE at the * end of the walk, or WALK_DATABASE_NEXT_ERROR on an LMDB cursor error. */ int walk_database_next(void) { int rc; rc = mdb_cursor_get(walk_cursor, &wdb_entry.path, &wdb_entry.data, MDB_NEXT); if (rc == 0) { walk_database_set_entry_type(); return WALK_DATABASE_NEXT; } if (rc != MDB_NOTFOUND) { puts(mdb_strerror(rc)); return WALK_DATABASE_NEXT_ERROR; } return WALK_DATABASE_DONE; } /* * walk_database_finish - close the CLI verification cursor. * @void: no arguments are required. * * Returns nothing. */ void walk_database_finish(void) { walk_database_reset(); } /* * database_walk_reader_slots_for_tests - report walker LMDB reader table use. * @void: no arguments are required. * * Returns the number of reader slots LMDB has allocated for the active walker * environment, or -1 when no walker is active or LMDB cannot report state. */ int database_walk_reader_slots_for_tests(void) { MDB_envinfo info; if (walk_env == NULL || walk_txn == NULL) return -1; if (mdb_env_info(walk_env, &info)) return -1; return (int)info.me_numreaders; } fapolicyd-2.0/src/library/database.h000066400000000000000000000114571524071644000175100ustar00rootroot00000000000000/* * database.h - Header file for trust database * Copyright (c) 2018-22 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * Radovan Sroka */ #ifndef DATABASE_HEADER #define DATABASE_HEADER #include #include #include #include "conf.h" #include "file.h" #include "gcc-attributes.h" typedef struct { MDB_val path; MDB_val data; /* Long paths are represented by a non-reversible SHA-512 key. */ int path_is_hashed; } walkdb_entry_t; #define TRUST_DB_METADATA_NAME "trust.meta" #define TRUST_DB_METADATA_KEY "current" #define TRUST_DB_GENERATION_NAME_SIZE 64 typedef struct { unsigned long generation; long entries; time_t publish_time; unsigned long lmdb_generation; time_t lmdb_publish_time; unsigned int retired_count; unsigned long oldest_retired_age; unsigned long max_reclaim_delay; } database_generation_report_t; void lock_update_thread(void); void unlock_update_thread(void); const char *lookup_tsource(unsigned int tsource) __attribute_const__; int preconstruct_fifo(const conf_t *config) __nonnull ((1)); int init_database(conf_t *config) __nonnull ((1)); int do_memfd_update(int memfd, long *entries) __nonnull ((2)) __attr_fd_arg_read (1); /* * fd is only consumed when info enables integrity checks. Path-only lookups * may pass a sentinel fd because no file descriptor is available. */ int check_trust_database(const char *path, struct file_info *info, int fd) __nonnull ((1)); void set_reload_trust_database(void); void close_database(void); void database_config_report(FILE *f); void database_utilization_report(FILE *f, const conf_t *config); void database_report(FILE *f); void database_metrics_report_reset(FILE *f, int reset); int database_generation_snapshot(database_generation_report_t *report) __nonnull ((1)); int unlink_db(void) __wur; void unlink_fifo(void); unsigned get_default_db_max_size(void); int database_read_active_name(MDB_txn *txn, char *name, size_t name_size) __nonnull ((1, 2)); void lock_rule(void); void unlock_rule(void); // Database verification functions enum walk_database_status { WALK_DATABASE_SUCCESS = 0, WALK_DATABASE_ERROR = 1, WALK_DATABASE_EMPTY = 2, }; enum walk_database_next_status { WALK_DATABASE_DONE = 0, WALK_DATABASE_NEXT = 1, WALK_DATABASE_NEXT_ERROR = -1, }; int walk_database_start(conf_t *config) __nonnull ((1)); walkdb_entry_t *walk_database_get_entry(void); int walk_database_next(void); void walk_database_finish(void); int database_readonly_lookup_start(void); int check_trust_database_readonly(const char *path, struct file_info *info, int fd) __nonnull ((1)); void database_readonly_lookup_finish(void); // Functions for unit test use typedef database_generation_report_t database_generation_test_report_t; int database_set_location(const char *dir, const char *name); int database_open_for_tests(conf_t *config) __nonnull ((1)); void database_close_for_tests(void); int database_publish_memfd_for_tests(int memfd, conf_t *config) __nonnull ((2)); int database_publish_startup_memfd_for_tests(int memfd, conf_t *config) __nonnull ((2)); int database_nested_lookup_for_tests(const char *path) __nonnull ((1)); int database_drop_candidate_after_import_for_tests(int memfd); int database_reload_for_tests(conf_t *config) __nonnull ((1)); int database_compact_memfd_for_tests(int memfd, conf_t *config) __nonnull ((2)); void *database_generation_hold_for_tests(void); void database_generation_release_for_tests(void *cookie); void database_reclaim_generations_for_tests(void); int database_generation_report_for_tests( database_generation_test_report_t *report) __nonnull ((1)); int database_walk_reader_slots_for_tests(void); unsigned int database_autosize_target_mb_for_tests(unsigned long active_pages, unsigned long env_allocated_pages, unsigned long map_pages, unsigned long page_size); unsigned int database_autosize_retry_mb_for_tests(unsigned long old_mb, unsigned long active_pages, unsigned long map_pages, unsigned long page_size); #define RELOAD_TRUSTDB_COMMAND '1' #define FLUSH_CACHE_COMMAND '2' #define RELOAD_RULES_COMMAND '3' #define COMPACT_TRUSTDB_COMMAND '4' #endif fapolicyd-2.0/src/library/deb-backend.c000066400000000000000000000307341524071644000200550ustar00rootroot00000000000000/* * deb-backend.c - Debian package database backend * Copyright (c) 2023-26 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "backend-manager.h" #include "conf.h" #include "fapolicyd-backend.h" #include "filter.h" #include "file.h" #include "message.h" #include "md5-backend.h" static const char kDebBackend[] = "debdb"; static int deb_init_backend(void); static int deb_load_list(const conf_t *); static int deb_destroy_backend(void); int do_deb_init_backend(void); int do_deb_load_list(const conf_t *, int memfd); int do_deb_destroy_backend(void); backend deb_backend = { kDebBackend, deb_init_backend, deb_load_list, deb_destroy_backend, -1, -1, }; #define BUFFER_SIZE 4096 static const char *deb_loader_path = LIBEXECDIR "/fapolicyd-deb-loader"; static int deb_loader_timeout_ms = BACKEND_LOADER_TIMEOUT_MS; // ================================================================ // These functions are copied from dpkg source v1.21.1 // For some reason they segfault when i call :/ int parse_filehash_buffer(struct varbuf *buf, struct pkginfo *pkg, struct pkgbin *pkgbin) { char *thisline, *nextline; const char *pkgname = pkg_name(pkg, pnaw_nonambig); const char *buf_end = buf->buf + buf->used; for (thisline = buf->buf; thisline < buf_end; thisline = nextline) { struct fsys_namenode *namenode; char *endline, *hash_end, *filename; endline = memchr(thisline, '\n', buf_end - thisline); if (endline == NULL) { msg(LOG_ERR, "control file '%s' for package '%s' is " "missing final newline\n", HASHFILE, pkgname); return 1; } /* The md5sum hash has a constant length. */ hash_end = thisline + kMd5HexSize; filename = hash_end + 2; if (filename + 1 > endline) { msg(LOG_ERR, "control file '%s' for package '%s' is " "missing value\n", HASHFILE, pkgname); return 1; } if (hash_end[0] != ' ' || hash_end[1] != ' ') { msg(LOG_ERR, "control file '%s' for package '%s' is " "missing value separator\n", HASHFILE, pkgname); return 1; } hash_end[0] = '\0'; /* Where to start next time around. */ nextline = endline + 1; /* Strip trailing ‘/’. */ if (endline > thisline && endline[-1] == '/') endline--; *endline = '\0'; if (endline == thisline) { msg(LOG_ERR, "control file '%s' for package '%s' " "contains empty filename\n", HASHFILE, pkgname); return 1; } /* Add the file to the list. */ namenode = fsys_hash_find_node(filename, 0); namenode->newhash = nfstrsave(thisline); } return 0; } void parse_filehash2(struct pkginfo *pkg, struct pkgbin *pkgbin) { const char *hashfile; struct varbuf buf = VARBUF_INIT; struct dpkg_error err = DPKG_ERROR_INIT; hashfile = pkg_infodb_get_file(pkg, pkgbin, HASHFILE); if (file_slurp(hashfile, &buf, &err) < 0 && err.syserrno != ENOENT) msg(LOG_ERR, "loading control file '%s' for package '%s'", HASHFILE, pkg_name(pkg, pnaw_nonambig)); if (buf.used > 0) parse_filehash_buffer(&buf, pkg, pkgbin); varbuf_destroy(&buf); } // End of functions copied from dpkg. // ======================================================================= /* * deb_add_file_to_backend - add one dpkg file after filter evaluation. * * Paths excluded by fapolicyd-filter.conf are skipped before opening or * hashing the file, matching the rpm backend's trust-source pruning. * Returns the MD5 backend result for included paths, or MD5_BACKEND_SKIPPED * when the filter excludes the path. */ static md5_backend_result_t deb_add_file_to_backend(const char *path, const char *hash, struct _hash_record **hashtable) { filter_rc_t f_res = filter_check(path); if (f_res != FILTER_ALLOW) { if (f_res == FILTER_ERR_DEPTH) msg(LOG_WARNING, "filter nesting exceeds MAX_FILTER_DEPTH for %s; excluding", path); return MD5_BACKEND_SKIPPED; } return add_file_to_backend_by_md5(path, hash, hashtable, SRC_DEB, &deb_backend); } /* * deb_backend_add_file_for_tests - exercise debdb add/filter behavior. * @path: package-owned path to evaluate. * @hash: package MD5 string for the path. * @hashtable: duplicate-tracking table used by the MD5 backend helper. * Returns the same result as the internal debdb add path. */ md5_backend_result_t deb_backend_add_file_for_tests(const char *path, const char *hash, struct _hash_record **hashtable) { return deb_add_file_to_backend(path, hash, hashtable); } int do_deb_load_list(const conf_t *conf, int memfd) { const char *control_file = "md5sums"; struct _hash_record *hashtable = NULL; struct _hash_record **hashtable_ptr = &hashtable; long entries = 0; if (memfd < 0) { msg(LOG_ERR, "Invalid memfd supplied to deb loader"); return 1; } deb_backend.memfd = memfd; deb_backend.entries = -1; struct pkg_array array; pkg_array_init_from_hash(&array); msg(LOG_INFO, "Computing hashes for %d packages.", array.n_pkgs); fsys_hash_reset(); int rc = 0; for (int i = 0; i < array.n_pkgs; i++) { struct pkginfo *package = array.pkgs[i]; if (package->status != PKG_STAT_INSTALLED) { continue; } printf("\x1b[2K\rPackage %d / %d : %s", i + 1, array.n_pkgs, package->set->name); if (pkg_infodb_has_file(package, &package->installed, control_file)) pkg_infodb_get_file(package, &package->installed, control_file); ensure_packagefiles_available(package); // Should not need this copy of code ... parse_filehash2(package, &package->installed); // This is causing segfault in linked lib :/ // parse_filehash(package, &package->installed); // ensure_diversions(); struct fsys_namenode_list *file = package->files; if (!file) { // Package does not have any files. continue; } // Loop over all files in the package, adding them to debdb. while (file) { struct fsys_namenode *namenode = file->namenode; // Get the hash and path of the file. const char *hash = (namenode->newhash == NULL) ? namenode->oldhash : namenode->newhash; const char *path = (namenode->divert && !namenode->divert->camefrom) ? namenode->divert->useinstead->name : namenode->name; if (hash != NULL) { md5_backend_result_t add_rc = deb_add_file_to_backend(path, hash, hashtable_ptr); if (add_rc == MD5_BACKEND_FATAL) { rc = 1; goto out; } if (add_rc == MD5_BACKEND_ADDED) entries++; } file = file->next; } } out: struct _hash_record *item, *tmp; HASH_ITER(hh, hashtable, item, tmp) { HASH_DEL(hashtable, item); free((void *)item->key); free((void *)item); } pkg_array_destroy(&array); if (rc == 0) deb_backend.entries = entries; return rc; } static int deb_load_list(const conf_t *conf) { int sv[2]; if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, sv) < 0) { msg(LOG_ERR, "socketpair failed"); return 1; } posix_spawn_file_actions_t actions; posix_spawn_file_actions_init(&actions); // dup sv[1] to FD 3 for the child; SOCK_CLOEXEC ensures both // sv[0] and sv[1] are closed on exec, and dup2 clears CLOEXEC // on the target FD 3. When sv[1] is already 3, dup2(3,3) is a // no-op that does NOT clear CLOEXEC, so move sv[1] to a // different FD to avoid the overlap. if (sv[1] == 3) { int tmp = fcntl(sv[1], F_DUPFD_CLOEXEC, 4); if (tmp < 0) { msg(LOG_ERR, "fcntl F_DUPFD_CLOEXEC failed"); close(sv[0]); close(sv[1]); posix_spawn_file_actions_destroy(&actions); return 1; } close(sv[1]); sv[1] = tmp; } posix_spawn_file_actions_adddup2(&actions, sv[1], 3); char *argv[] = { "fapolicyd-deb-loader", NULL }; char *custom_env[] = { "FAPO_SOCK_FD=3", NULL }; pid_t pid = -1; int spawn_rc = posix_spawn(&pid, deb_loader_path, &actions, NULL, argv, custom_env); close(sv[1]); // Parent doesn't write if (spawn_rc == 0) { msg(LOG_DEBUG, "fapolicyd-deb-loader spawned with pid: %d", pid); if (backend_wait_for_loader(sv[0], deb_loader_timeout_ms)) { char err_buff[BUFFER_SIZE]; msg(LOG_ERR, "deb loader snapshot wait failed (%s)", strerror_r(errno, err_buff, BUFFER_SIZE)); close(sv[0]); posix_spawn_file_actions_destroy(&actions); return 1; } struct msghdr _msg = {0}; struct iovec iov = { .iov_base = (char[1]){0}, .iov_len = 1 }; union { struct cmsghdr align; char buf[CMSG_SPACE(sizeof(int))]; } cmsgbuf = {0}; _msg.msg_iov = &iov; _msg.msg_iovlen = 1; _msg.msg_control = cmsgbuf.buf; _msg.msg_controllen = sizeof cmsgbuf.buf; ssize_t rc; do { rc = recvmsg(sv[0], &_msg, MSG_DONTWAIT); } while (rc < 0 && errno == EINTR); if (rc < 0) { char err_buff[BUFFER_SIZE]; msg(LOG_ERR, "recvmsg failed (%s)", strerror_r(errno, err_buff, BUFFER_SIZE)); close(sv[0]); posix_spawn_file_actions_destroy(&actions); return 1; } close(sv[0]); struct cmsghdr *c = CMSG_FIRSTHDR(&_msg); if (!c || c->cmsg_type != SCM_RIGHTS) { msg(LOG_ERR, "missing fd"); posix_spawn_file_actions_destroy(&actions); return 1; } int memfd; memcpy(&memfd, CMSG_DATA(c), sizeof memfd); // Mark entries unknown until a fresh snapshot is available. deb_backend.entries = -1; if (fcntl(memfd, F_SETFD, FD_CLOEXEC) == -1) { char err_buff[BUFFER_SIZE]; msg(LOG_WARNING, "Failed to set CLOEXEC on deb memfd (%s)", strerror_r(errno, err_buff, BUFFER_SIZE)); } // Pass the memfd to the backend representation. deb_backend.memfd = memfd; } else { close(sv[0]); msg(LOG_ERR, "posix_spawn failed: %s\n", strerror(spawn_rc)); } posix_spawn_file_actions_destroy(&actions); return spawn_rc; } /* * deb_backend_load_from_path_for_tests - exercise deb loader IPC errors. * @conf: test configuration. * @loader_path: helper executable path to spawn. * @timeout_ms: maximum snapshot wait used by the test. * * Returns the deb backend load result without terminating the caller. */ int deb_backend_load_from_path_for_tests(const conf_t *conf, const char *loader_path, int timeout_ms) { const char *previous_loader_path = deb_loader_path; int previous_timeout_ms = deb_loader_timeout_ms; int rc; deb_loader_path = loader_path; deb_loader_timeout_ms = timeout_ms; rc = deb_load_list(conf); deb_loader_path = previous_loader_path; deb_loader_timeout_ms = previous_timeout_ms; return rc; } static int deb_init_backend(void) { return 0; } int do_deb_init_backend(void) { dpkg_program_init(kDebBackend); msg(LOG_INFO, "Loading debdb backend"); enum modstatdb_rw status = msdbrw_readonly; status = modstatdb_open(msdbrw_readonly); if (status != msdbrw_readonly) { msg(LOG_ERR, "Could not open database for reading. Status %d", status); dpkg_program_done(); return 1; } if (filter_init()) { msg(LOG_ERR, "Failed initializing filter for debdb backend"); modstatdb_shutdown(); dpkg_program_done(); return 1; } if (filter_load_file(NULL)) { msg(LOG_ERR, "Failed loading filter for debdb backend"); filter_destroy(); modstatdb_shutdown(); dpkg_program_done(); return 1; } return 0; } static int deb_destroy_backend(void) { return 0; } int do_deb_destroy_backend(void) { dpkg_program_done(); modstatdb_shutdown(); filter_destroy(); return 0; } fapolicyd-2.0/src/library/decision-config.c000066400000000000000000000145121524071644000207720ustar00rootroot00000000000000/* * decision-config.c - immutable decision configuration generations * * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #include "config.h" #include #include #include #include #include "decision-config.h" #include "message.h" /* * decision_config - immutable fields that can change a decision result. * * A permission event pins one generation before event construction and keeps * using it through trust checks and the final fanotify response. Reloads * publish a new object instead of mutating the active one, so a decision never * observes permissive mode from one reload and digest policy from another. */ struct decision_config { unsigned int permissive; integrity_t integrity; unsigned int rpm_sha256_only; unsigned int generation; time_t effective_since; struct decision_config *previous; }; static struct decision_config default_decision_config = { .permissive = 0, .integrity = IN_NONE, .rpm_sha256_only = 0, .generation = 0, .effective_since = 0, .previous = NULL, }; static _Atomic(struct decision_config *) active_decision_config = &default_decision_config; static atomic_uint next_generation; static pthread_mutex_t publish_lock = PTHREAD_MUTEX_INITIALIZER; static __thread const struct decision_config *pinned_decision_config; static const struct decision_config *decision_config_active(void) { struct decision_config *config; config = atomic_load_explicit(&active_decision_config, memory_order_acquire); if (config == NULL) return &default_decision_config; return config; } /* * decision_config_publish - publish a new immutable decision config. * @config: parsed daemon configuration to snapshot. * Returns 0 on success and 1 on allocation or argument failure. */ int decision_config_publish(const conf_t *config) { struct decision_config *snapshot; time_t now; if (config == NULL) return 1; snapshot = calloc(1, sizeof(*snapshot)); if (snapshot == NULL) { msg(LOG_ERR, "Cannot allocate decision config generation"); return 1; } snapshot->permissive = config->permissive ? 1 : 0; snapshot->integrity = config->integrity; snapshot->rpm_sha256_only = config->rpm_sha256_only ? 1 : 0; now = time(NULL); snapshot->effective_since = now == (time_t)-1 ? 0 : now; pthread_mutex_lock(&publish_lock); snapshot->generation = atomic_fetch_add_explicit(&next_generation, 1, memory_order_relaxed) + 1; snapshot->previous = atomic_load_explicit(&active_decision_config, memory_order_relaxed); atomic_store_explicit(&active_decision_config, snapshot, memory_order_release); pthread_mutex_unlock(&publish_lock); msg(LOG_INFO, "Decision config generation %u published", snapshot->generation); return 0; } /* * decision_config_pin - pin the active config for the current decision. * Returns the config generation that the current thread will use. */ const struct decision_config *decision_config_pin(void) { const struct decision_config *config = decision_config_active(); pinned_decision_config = config; return config; } /* * decision_config_unpin - clear a pinned config for the current thread. * @config: generation previously returned by decision_config_pin(). * Returns nothing. */ void decision_config_unpin(const struct decision_config *config) { if (pinned_decision_config == config) pinned_decision_config = NULL; } /* * decision_config_current - return pinned generation or active generation. * Returns a valid decision config object. */ const struct decision_config *decision_config_current(void) { if (pinned_decision_config) return pinned_decision_config; return decision_config_active(); } unsigned int decision_config_generation(const struct decision_config *config) { if (config == NULL) config = decision_config_current(); return config->generation; } /* * decision_config_effective_since - return the config activation time. * @config: config generation to inspect, or NULL for the current generation. * Returns the time @config became active, or zero if it is unknown. */ time_t decision_config_effective_since(const struct decision_config *config) { if (config == NULL) config = decision_config_current(); return config->effective_since; } unsigned int decision_config_permissive(const struct decision_config *config) { if (config == NULL) config = decision_config_current(); return config->permissive; } integrity_t decision_config_integrity(const struct decision_config *config) { if (config == NULL) config = decision_config_current(); return config->integrity; } /* * decision_config_rpm_sha256_only - return the active RPM digest floor flag. * @config: config generation to inspect, or NULL for the current generation. * Returns 1 when RPM trust records must be SHA256 or stronger, otherwise 0. */ unsigned int decision_config_rpm_sha256_only( const struct decision_config *config) { if (config == NULL) config = decision_config_current(); return config->rpm_sha256_only; } unsigned int decision_config_active_generation(void) { return decision_config_generation(decision_config_active()); } /* * decision_config_active_effective_since - return active activation time. * Returns the time the active config became effective, or zero if unknown. */ time_t decision_config_active_effective_since(void) { return decision_config_effective_since(decision_config_active()); } /* * decision_config_destroy - free published config generations during shutdown. * * Generations are not reclaimed on reload because a future worker may still * have pinned the old one. Shutdown is the only point where all decision users * are gone and the list can be released without an epoch scheme. */ void decision_config_destroy(void) { struct decision_config *config, *previous; pthread_mutex_lock(&publish_lock); config = atomic_exchange_explicit(&active_decision_config, &default_decision_config, memory_order_acq_rel); while (config && config != &default_decision_config) { previous = config->previous; free(config); config = previous; } atomic_store_explicit(&next_generation, 0, memory_order_relaxed); pthread_mutex_unlock(&publish_lock); pinned_decision_config = NULL; } fapolicyd-2.0/src/library/decision-config.h000066400000000000000000000023721524071644000210000ustar00rootroot00000000000000/* * decision-config.h - immutable decision configuration generations * * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef DECISION_CONFIG_HEADER #define DECISION_CONFIG_HEADER #include #include "conf.h" struct decision_config; int decision_config_publish(const conf_t *config); const struct decision_config *decision_config_pin(void); void decision_config_unpin(const struct decision_config *config); const struct decision_config *decision_config_current(void); unsigned int decision_config_generation(const struct decision_config *config); time_t decision_config_effective_since(const struct decision_config *config); unsigned int decision_config_permissive(const struct decision_config *config); integrity_t decision_config_integrity(const struct decision_config *config); unsigned int decision_config_rpm_sha256_only( const struct decision_config *config); unsigned int decision_config_active_generation(void); time_t decision_config_active_effective_since(void); void decision_config_destroy(void); #endif fapolicyd-2.0/src/library/decision-context.h000066400000000000000000000061531524071644000212200ustar00rootroot00000000000000/* * decision-context.h - per-decision mutable state * * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at your * option) any later version. */ #ifndef DECISION_CONTEXT_HEADER #define DECISION_CONTEXT_HEADER #include #include #include #include #include "decision-defer.h" #include "lru.h" #include "message.h" struct udev; struct conf; struct file_device_cache { dev_t device; char *devname; }; /* * decision_policy_counters - policy metrics updated by decision processing. * * Ruleset generation is deliberately not part of this block because rules can * be published before the daemon allocates the runtime decision context. */ struct decision_policy_counters { atomic_ulong allowed; atomic_ulong denied; atomic_ulong allowed_by_rule; atomic_ulong allowed_by_fallthrough; atomic_ulong fallthrough_open; atomic_ulong fallthrough_execute; atomic_ulong fallthrough_trusted; atomic_ulong fallthrough_untrusted; atomic_ulong fallthrough_trust_unknown; atomic_ulong fallthrough_executable; atomic_ulong fallthrough_programmatic; atomic_ulong fallthrough_sharedlib; atomic_ulong fallthrough_unknown_ftype; atomic_ulong fallthrough_other_ftype; }; /* * decision_context - mutable state owned by one decision processor. * * Each decision worker owns one active instance. Keeping the hot-path caches, * logging buffer, counters, and defer queue behind this object keeps worker * ownership explicit. */ struct decision_context { unsigned int worker_id; Queue *subject_cache; Queue *object_cache; unsigned long object_cache_flush_generation; char *working_buffer; bool object_cache_warned; atomic_uint early_subject_cache_evictions; atomic_uint building_tracer_evict_count; atomic_uint building_stale_evict_count; struct message_rate_limit building_tracer_rate_limit; struct message_rate_limit building_stale_rate_limit; struct decision_defer_queue defer_queue; struct decision_defer_metrics last_defer_metrics; struct decision_policy_counters policy_counters; /* File helpers keep mutable parser/cache state private to a worker. */ struct udev *file_udev; magic_t magic_fast; magic_t magic_full; struct file_device_cache device_cache; /* Reporting walks registered worker contexts without owning them. */ struct decision_context *report_next; bool report_registered; }; typedef void (*decision_context_iter_fn)(struct decision_context *ctx, void *data); struct decision_context *decision_context_current(void); void decision_context_set_current(struct decision_context *ctx); void decision_context_close_file_helpers(struct decision_context *ctx); void decision_context_set_worker_id(struct decision_context *ctx, unsigned int worker_id); struct decision_context *decision_context_create(const struct conf *config); void decision_context_destroy(struct decision_context *ctx); void decision_context_for_each(decision_context_iter_fn iter, void *data); #endif fapolicyd-2.0/src/library/decision-defer.c000066400000000000000000000376641524071644000206270ustar00rootroot00000000000000/* * decision-defer.c - bounded subject-slot deferral for decision events * * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at your * option) any later version. */ /* * Overview * -------- * * Subject deferral protects a subject cache slot while the process currently * occupying that slot is still building startup pattern state. The decision * worker computes an incoming event's subject slot before calling new_event(). * If the same slot already contains a different pid whose subject state is * before STATE_FULL, processing the incoming event would make new_event() * evict the in-progress subject. Instead, the decision worker copies the * incoming decision_event_t into this fixed-size defer array. Traced or stale * BUILDING occupants are the exception: event.c evicts those subjects and * lets the incoming event process normally because waiting may never * release the slot. * * A slot is the subject cache hash index. An entry is one position in this * defer array. Multiple deferred entries can target the same slot. Entries are * selected by age: pop_slot() returns the oldest deferred event for one * released subject slot, while pop_any() returns the oldest event regardless * of slot and is used during shutdown cleanup. * * The decision worker owns this array. No producer writes to it, and no other * thread pops from it. The report-owning worker can snapshot/reset metrics, * so each public operation serializes that observation with mutations. This * keeps fd ownership explicit: a deferred entry owns the fanotify permission * fd in its embedded decision_event_t until the entry is popped for normal * processing or shutdown replies to it. * * The array is intentionally bounded. If it is full, callers must fall back to * the historical eviction behavior so memory use and blocked kernel permission * events remain bounded. */ #include "config.h" #include #include #include #include #include #include #include "decision-defer.h" #include "string-util.h" struct decision_defer_entry { /* Event envelope and permission fd owned while this entry is used. */ decision_event_t event; /* Monotonic time when the event entered the defer array. */ uint64_t deferred_ns; /* Insertion sequence number used to choose the oldest matching entry. */ uint64_t order; /* Non-zero when this array entry currently owns a deferred event. */ int used; }; /* * defer_lock - acquire exclusive access to one defer queue. * @defer: initialized queue to lock. * * Returns zero when the queue is locked, or -1 with errno set otherwise. */ static int defer_lock(struct decision_defer_queue *defer) { int rc; if (defer == NULL || !defer->lock_initialized) { errno = EINVAL; return -1; } rc = pthread_mutex_lock(&defer->lock); if (rc) { errno = rc; return -1; } return 0; } /* * defer_unlock - release one defer queue after a state operation. * @defer: locked queue to release. * * Returns nothing. */ static void defer_unlock(struct decision_defer_queue *defer) { (void)pthread_mutex_unlock(&defer->lock); } /* * defer_now_ns - read monotonic time for defer age reporting. * Returns monotonic nanoseconds, or zero if the clock cannot be read. */ static uint64_t defer_now_ns(void) { struct timespec ts; if (clock_gettime(CLOCK_MONOTONIC, &ts)) return 0; return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; } /* * decision_defer_init - allocate a fixed defer array. * @defer: queue state to initialize. * @subj_cache_size: configured subject cache size. * * The defer array is intentionally bounded. It scales from the configured * subject cache size and has a small floor so tiny test configurations still * exercise deferral without repeated allocation. * * Returns 0 on success and -1 on allocation or argument failure. */ int decision_defer_init(struct decision_defer_queue *defer, unsigned int subj_cache_size) { unsigned int capacity; int rc; if (defer == NULL) { errno = EINVAL; return -1; } memset(defer, 0, sizeof(*defer)); rc = pthread_mutex_init(&defer->lock, NULL); if (rc) { errno = rc; return -1; } defer->lock_initialized = 1; capacity = subj_cache_size / DECISION_DEFER_RATIO; if (capacity < DECISION_DEFER_MIN) capacity = DECISION_DEFER_MIN; defer->entries = calloc(capacity, sizeof(struct decision_defer_entry)); if (defer->entries == NULL) { pthread_mutex_destroy(&defer->lock); memset(defer, 0, sizeof(*defer)); return -1; } defer->capacity = capacity; return 0; } /* * decision_defer_destroy - release defer array storage. * @defer: queue state to destroy. * Returns nothing. */ void decision_defer_destroy(struct decision_defer_queue *defer) { if (defer == NULL) return; if (!defer->lock_initialized) { free(defer->entries); memset(defer, 0, sizeof(*defer)); return; } if (defer_lock(defer)) return; free(defer->entries); defer->entries = NULL; defer_unlock(defer); pthread_mutex_destroy(&defer->lock); memset(defer, 0, sizeof(*defer)); } /* * oldest_entry - find the oldest deferred event matching a slot. * @defer: queue state to inspect. * @slot: matching subject slot, or DECISION_EVENT_NO_SLOT for any slot. * * This is a bounded O(capacity) linear scan. The defer array is intentionally * small and fixed-size, and callers avoid this scan entirely when current is * zero. The caller holds defer->lock. * * Returns the oldest matching entry, or NULL when none exists. */ static struct decision_defer_entry *oldest_entry( struct decision_defer_queue *defer, unsigned int slot) { struct decision_defer_entry *oldest = NULL; unsigned int i; if (defer == NULL || defer->current == 0) return NULL; for (i = 0; i < defer->capacity; i++) { struct decision_defer_entry *entry = &defer->entries[i]; // Empty entries are available for reuse and never considered. if (!entry->used) continue; // Slot-specific callers only want events blocked by that slot. if (slot != DECISION_EVENT_NO_SLOT && entry->event.subject_slot != slot) continue; // Lower insertion order means older deferred request. if (oldest == NULL || entry->order < oldest->order) oldest = entry; } return oldest; } /* * decision_defer_push - store one event in the defer array. * @defer: queue state receiving the event. * @event: event to copy into the defer array. * * The deferred copy owns the event fd until it is popped for processing or * shutdown cleanup handles it. * * Returns 0 on success and -1 with ENOSPC when the array is full. */ int decision_defer_push(struct decision_defer_queue *defer, const decision_event_t *event) { unsigned int i; int rc = -1; if (defer == NULL || event == NULL) { errno = EINVAL; return -1; } if (defer_lock(defer)) return -1; if (defer->current == defer->capacity) { errno = ENOSPC; goto out; } for (i = 0; i < defer->capacity; i++) { struct decision_defer_entry *entry = &defer->entries[i]; if (entry->used) continue; // Store by value; the defer entry now owns the event fd. entry->event = *event; // A parked event has not completed any subject slot yet. entry->event.completed_subject_slot = DECISION_EVENT_NO_SLOT; entry->deferred_ns = defer_now_ns(); entry->order = defer->next_order++; entry->used = 1; defer->current++; defer->deferred_events++; if (defer->current > defer->max_depth) defer->max_depth = defer->current; rc = 0; goto out; } errno = ENOSPC; out: defer_unlock(defer); return rc; } /* * pop_entry - remove one deferred entry. * @defer: queue state owning the entry. * @entry: entry to remove. * @event: destination for the deferred event. * Returns 1 when an entry was removed, 0 otherwise. */ static int pop_entry(struct decision_defer_queue *defer, struct decision_defer_entry *entry, decision_event_t *event) { if (defer == NULL || entry == NULL || event == NULL) return 0; // Transfer fd ownership back to the caller for processing or shutdown. *event = entry->event; /* * Marking the entry unused is enough. The next push overwrites every * field before setting used again, and oldest_entry() ignores unused * slots. Avoid clearing the full embedded fanotify metadata on every * pop because high-churn workloads can pop many entries. */ entry->used = 0; defer->current--; return 1; } /* * decision_defer_pop_slot - remove the oldest deferred event for one slot. * @defer: queue state to pop from. * @slot: subject cache slot that is no longer blocking. * @event: destination for the deferred event. * * Returns 1 when an event was removed, 0 when no matching event exists. */ int decision_defer_pop_slot(struct decision_defer_queue *defer, unsigned int slot, decision_event_t *event) { int rc; if (defer == NULL || event == NULL || defer_lock(defer)) return 0; rc = pop_entry(defer, oldest_entry(defer, slot), event); defer_unlock(defer); return rc; } /* * decision_defer_pop_if - remove the oldest deferred event matching a test. * @defer: queue state to pop from. * @match: predicate called for each deferred event. * @ctx: caller data passed to @match. * @event: destination for the deferred event. * * The match predicate may inspect external state before deciding whether an * event is ready. The oldest matching event is removed so deferral preserves * arrival order among events that can run. * * Returns 1 when an event was removed, 0 when no matching event exists. */ int decision_defer_pop_if(struct decision_defer_queue *defer, decision_defer_match_fn match, void *ctx, decision_event_t *event) { struct decision_defer_entry *oldest = NULL; unsigned int i; unsigned int seen = 0; int rc = 0; if (defer == NULL || match == NULL || event == NULL || defer_lock(defer)) return 0; if (defer->current == 0) goto out; for (i = 0; i < defer->capacity; i++) { struct decision_defer_entry *entry = &defer->entries[i]; if (!entry->used) continue; seen++; if (match(&entry->event, ctx) && (oldest == NULL || entry->order < oldest->order)) oldest = entry; if (seen == defer->current) break; } rc = pop_entry(defer, oldest, event); out: defer_unlock(defer); return rc; } /* * decision_defer_pop_any - remove the oldest deferred event. * @defer: queue state to pop from. * @event: destination for the deferred event. * * Returns 1 when an event was removed, 0 when the defer array is empty. */ int decision_defer_pop_any(struct decision_defer_queue *defer, decision_event_t *event) { int rc; if (defer == NULL || event == NULL || defer_lock(defer)) return 0; rc = pop_entry(defer, oldest_entry(defer, DECISION_EVENT_NO_SLOT), event); defer_unlock(defer); return rc; } /* * decision_defer_count_fallback - count one full-array fallback. * @defer: queue state whose counter should be incremented. * Returns nothing. */ void decision_defer_count_fallback(struct decision_defer_queue *defer) { if (defer == NULL || defer_lock(defer)) return; defer->fallbacks++; defer_unlock(defer); } /* * decision_defer_has_events - test whether deferred events are queued. * @defer: queue state to inspect. * * Returns non-zero when the queue has at least one deferred event. */ int decision_defer_has_events(struct decision_defer_queue *defer) { int has_events; if (defer == NULL || defer_lock(defer)) return 0; has_events = defer->current != 0; defer_unlock(defer); return has_events; } /* * oldest_age_ns - compute age of the oldest currently deferred event. * @defer: queue state to inspect. * Returns age in nanoseconds, or zero when there are no deferred events. */ static uint64_t oldest_age_ns(struct decision_defer_queue *defer) { struct decision_defer_entry *entry; uint64_t now; entry = oldest_entry(defer, DECISION_EVENT_NO_SLOT); if (entry == NULL || entry->deferred_ns == 0) return 0; now = defer_now_ns(); if (now < entry->deferred_ns) return 0; return now - entry->deferred_ns; } /* * decision_defer_metrics_snapshot_reset - copy defer metrics. * @defer: queue state to read. * @metrics: destination for the snapshot. * @reset: non-zero resets interval counters after copying. * * Current depth and capacity are state. Max depth restarts at the current * depth after reset so reports never claim a max below live occupancy. */ void decision_defer_metrics_snapshot_reset(struct decision_defer_queue *defer, struct decision_defer_metrics *metrics, int reset) { if (metrics == NULL) return; memset(metrics, 0, sizeof(*metrics)); if (defer == NULL || defer_lock(defer)) return; metrics->capacity = defer->capacity; metrics->current_depth = defer->current; metrics->deferred_events = defer->deferred_events; metrics->max_depth = defer->max_depth; metrics->fallbacks = defer->fallbacks; metrics->oldest_age_ns = oldest_age_ns(defer); if (reset) { defer->deferred_events = 0; defer->max_depth = defer->current; defer->fallbacks = 0; } defer_unlock(defer); } /* * decision_defer_config_report - write defer capacity sized at startup. * @f: report stream. * @metrics: metrics snapshot to report. * Returns nothing. */ void decision_defer_config_report(FILE *f, const struct decision_defer_metrics *metrics) { if (f == NULL || metrics == NULL) return; fprintf(f, "Per worker Subject defer array size: %u\n", metrics->capacity); } /* * decision_defer_fallback_report - write defer fallback health indicator. * @f: report stream. * @metrics: metrics snapshot to report. * Returns nothing. */ void decision_defer_fallback_report(FILE *f, const struct decision_defer_metrics *metrics) { if (f == NULL || metrics == NULL) return; fprintf(f, "Subject defer fallbacks: %lu\n", metrics->fallbacks); } /* * decision_defer_age_report - write oldest deferred event age. * @f: report stream. * @metrics: metrics snapshot to report. * Returns nothing. */ void decision_defer_age_report(FILE *f, const struct decision_defer_metrics *metrics) { char age[32]; if (f == NULL || metrics == NULL) return; fapolicyd_format_ns(metrics->oldest_age_ns, age, sizeof(age)); fprintf(f, "Subject defer oldest age: %s\n", age); } /* * decision_defer_health_report - write defer health indicators. * @f: report stream. * @metrics: metrics snapshot to report. * Returns nothing. */ void decision_defer_health_report(FILE *f, const struct decision_defer_metrics *metrics) { decision_defer_fallback_report(f, metrics); decision_defer_age_report(f, metrics); } /* * decision_defer_metrics_report - write defer activity metrics. * @f: output stream. * @metrics: metrics snapshot to report. * Returns nothing. */ void decision_defer_metrics_report(FILE *f, const struct decision_defer_metrics *metrics) { if (f == NULL || metrics == NULL) return; fprintf(f, "Subject deferred events: %lu\n", metrics->deferred_events); fprintf(f, "Subject defer max depth: %u\n", metrics->max_depth); fprintf(f, "Subject defer fallbacks: %lu\n", metrics->fallbacks); } /* * decision_defer_metrics_report_worker - write one worker's defer counters. * @f: output stream. * @worker_id: decision worker that owns these counters. * @metrics: defer metrics snapshot to report. * Returns nothing. */ void decision_defer_metrics_report_worker(FILE *f, unsigned int worker_id, const struct decision_defer_metrics *metrics) { char age[32]; if (f == NULL || metrics == NULL) return; fapolicyd_format_ns(metrics->oldest_age_ns, age, sizeof(age)); fprintf(f, " Decision worker %u Subject deferred events: %lu\n", worker_id, metrics->deferred_events); fprintf(f, " Decision worker %u Subject defer max depth: %u\n", worker_id, metrics->max_depth); fprintf(f, " Decision worker %u Subject defer fallbacks: %lu\n", worker_id, metrics->fallbacks); fprintf(f, " Decision worker %u Subject defer current depth: %u\n", worker_id, metrics->current_depth); fprintf(f, " Decision worker %u Subject defer oldest age: %s\n", worker_id, age); } fapolicyd-2.0/src/library/decision-defer.h000066400000000000000000000052211524071644000206140ustar00rootroot00000000000000/* * decision-defer.h - bounded subject-slot deferral for decision events * * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at your * option) any later version. */ #ifndef DECISION_DEFER_HEADER #define DECISION_DEFER_HEADER #include #include #include #include "decision-event.h" #define DECISION_DEFER_RATIO 16 #define DECISION_DEFER_MIN 16 struct decision_defer_metrics { unsigned int capacity; unsigned int current_depth; unsigned long deferred_events; unsigned int max_depth; unsigned long fallbacks; uint64_t oldest_age_ns; }; struct decision_defer_entry; struct decision_defer_queue { /* Serializes worker mutations with report snapshot/reset operations. */ pthread_mutex_t lock; int lock_initialized; struct decision_defer_entry *entries; unsigned int capacity; unsigned int current; unsigned long deferred_events; unsigned int max_depth; unsigned long fallbacks; uint64_t next_order; }; typedef int (*decision_defer_match_fn)(const decision_event_t *event, void *ctx); int decision_defer_init(struct decision_defer_queue *defer, unsigned int subj_cache_size); void decision_defer_destroy(struct decision_defer_queue *defer); int decision_defer_push(struct decision_defer_queue *defer, const decision_event_t *event); int decision_defer_pop_slot(struct decision_defer_queue *defer, unsigned int slot, decision_event_t *event); int decision_defer_pop_if(struct decision_defer_queue *defer, decision_defer_match_fn match, void *ctx, decision_event_t *event); int decision_defer_pop_any(struct decision_defer_queue *defer, decision_event_t *event); void decision_defer_count_fallback(struct decision_defer_queue *defer); int decision_defer_has_events(struct decision_defer_queue *defer); void decision_defer_metrics_snapshot_reset(struct decision_defer_queue *defer, struct decision_defer_metrics *metrics, int reset); void decision_defer_config_report(FILE *f, const struct decision_defer_metrics *metrics); void decision_defer_fallback_report(FILE *f, const struct decision_defer_metrics *metrics); void decision_defer_age_report(FILE *f, const struct decision_defer_metrics *metrics); void decision_defer_health_report(FILE *f, const struct decision_defer_metrics *metrics); void decision_defer_metrics_report(FILE *f, const struct decision_defer_metrics *metrics); void decision_defer_metrics_report_worker(FILE *f, unsigned int worker_id, const struct decision_defer_metrics *metrics); #endif fapolicyd-2.0/src/library/decision-event.h000066400000000000000000000054111524071644000206510ustar00rootroot00000000000000/* * decision-event.h - internal event envelope for policy decisions * * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at your * option) any later version. */ #ifndef DECISION_EVENT_HEADER #define DECISION_EVENT_HEADER #include #include #include #define DECISION_EVENT_NO_SLOT UINT_MAX #define DECISION_EVENT_NO_WORKER UINT_MAX /* * decision_event - userspace envelope for one fanotify permission event. * * This is the "event envelope" referenced by the queue, defer, and policy * decision paths. It is called an envelope because it carries the original * kernel fanotify metadata, including the permission fd, together with the * daemon-only bookkeeping that must travel with that kernel event before an * event_t can be built or while processing is deferred. * * event_t is the constructed event used for rule evaluation. decision_event_t * is the outer carrier used while the raw fanotify event is queued, deferred, * timed, and finally answered. */ typedef struct decision_event { /* * Original fanotify metadata from the kernel. Permission event fd * ownership stays with this envelope until reply_event() answers it * or shutdown cleanup closes it. */ struct fanotify_event_metadata metadata; /* * Userspace queue timestamp used by decision timing. It is zero when * timing was not armed at enqueue time and must be preserved while an * event is deferred. */ uint64_t enqueue_ns; /* * Subject cache slot computed from metadata.pid using the same key * function as the subject cache. DECISION_EVENT_NO_SLOT means it has * not been computed yet. */ unsigned int subject_slot; /* * Slot that became unblocked while this event was processed. * DECISION_EVENT_NO_SLOT means no deferred event should be released. */ unsigned int completed_subject_slot; /* * Decision worker chosen by the fanotify dispatcher. This is recorded * before enqueue so later debugging can confirm which worker owned the * reply path for this permission fd. */ unsigned int worker_index; } decision_event_t; /* * decision_event_init - wrap one fanotify metadata record. * @event: decision event to initialize. * @metadata: fanotify metadata copied into the wrapper. * Returns nothing. */ static inline void decision_event_init(decision_event_t *event, const struct fanotify_event_metadata *metadata) { event->metadata = *metadata; event->enqueue_ns = 0; event->subject_slot = DECISION_EVENT_NO_SLOT; event->completed_subject_slot = DECISION_EVENT_NO_SLOT; event->worker_index = DECISION_EVENT_NO_WORKER; } #endif fapolicyd-2.0/src/library/decision-timing.c000066400000000000000000002533751524071644000210300ustar00rootroot00000000000000/* * decision-timing.c - bounded decision timing diagnostics * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ /* * Overview * -------- * * Decision timing is an opt-in diagnostic window for explaining where * fapolicyd spends time while callers are blocked on fanotify permission * events. It is meant for QE, stress runs, field diagnosis and sizing work, * not for permanent always-on tracing. * * Normal operation keeps timing disabled. When disabled, the decision path * copies one armed flag into thread-local state for each dequeued event, and * the inline stage helpers return without calling clock_gettime(), updating * histograms, or touching shared counters. A privileged manual start request * resets the bounded metric blocks and arms collection. A stop request * disarms collection, snapshots the aggregates and writes TIMING_REPORT. * Queue wait is measured separately from dequeue-to-reply decision time so * reports can distinguish backlog from slow work inside a decision. * * Each worker owns a padded block of stage metrics. A stage records only a * count, total nanoseconds, max nanoseconds and fixed latency buckets. The * daemon intentionally does not store one record per decision; that keeps * memory bounded for stress tests that may generate millions of events and * avoids turning the measurement system into the workload. * * Stage rows are operation histograms. They may be nested and some helpers * are lazy, so rows are not expected to add up to decision:total. Lazy * helper costs that can be caused by either rule evaluation or response * formatting use a thread-local "driver" so the report can show evaluation * versus response attribution. * * The report path does the expensive work after collection stops: aggregate * worker blocks, rank stages, derive bucket percentiles and emit short * observations about queueing, helper cost, tail latency and debug-heavy * response formatting. * * Assumption: normal deployments leave timing_collection=off. Manual timing * runs are short diagnostic windows, and worker-local blocks are kept from * the beginning so future decision-worker pools do not contend on one global * histogram. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include "decision-timing.h" #include "gcc-attributes.h" #include "message.h" #include "paths.h" #define DECISION_TIMING_MAX_WORKERS 32 #define DECISION_TIMING_BUCKETS 14 #define DECISION_TIMING_STAGE_WIDTH 48 #define DECISION_TIMING_PHASE_WIDTH 16 #define DECISION_TIMING_HELPER_WIDTH 44 #define DECISION_TIMING_DRIVER_WIDTH 32 #define DECISION_TIMING_TAIL_STAGE_LIMIT 5 #define NSEC_PER_SEC 1000000000ULL #define IDLE_WORKLOAD_RATE_MULTIPLIER 2.0 #define RESPONSE_FORMATTING_DOMINANT 50.0 #define TRUST_DB_LOCK_TINY_SHARE 1.0 #define HASH_RARE_SHARE 10.0 /* Fixed-size aggregate for one stage in one worker's timing block. */ struct decision_timing_stage_metrics { atomic_ullong count; atomic_ullong total_ns; atomic_ullong max_ns; atomic_ullong buckets[DECISION_TIMING_BUCKETS]; }; /* * Keep worker blocks cache-line separated so future decision workers can * update their own histograms without false sharing. */ struct decision_timing_worker_block { struct decision_timing_stage_metrics stages[DECISION_TIMING_STAGE_COUNT]; } __attribute__((aligned(64))); struct decision_timing_stage_snapshot { unsigned long long count; unsigned long long total_ns; unsigned long long max_ns; unsigned long long buckets[DECISION_TIMING_BUCKETS]; }; struct decision_timing_worker_snapshot { unsigned int worker_id; struct decision_timing_stage_snapshot stages[DECISION_TIMING_STAGE_COUNT]; }; struct decision_timing_stage_order { unsigned int stages[DECISION_TIMING_STAGE_COUNT]; unsigned int count; }; struct decision_timing_report_ctx { FILE *f; const struct decision_timing_stage_snapshot *totals; const struct decision_timing_worker_snapshot *workers; const struct decision_timing_stage_order *order; unsigned long long decisions; unsigned long long duration_ns; unsigned int worker_count; unsigned int max_queue_depth; unsigned int q_size; }; struct decision_timing_named_stage { decision_timing_stage_t stage; const char *name; }; struct decision_timing_helper_row { const char *name; decision_timing_stage_t eval_stage; decision_timing_stage_t response_stage; bool by_driver; }; struct decision_timing_tail_row { decision_timing_stage_t stage; unsigned long long over_10ms; unsigned long long over_50ms; }; enum decision_timing_stop_reason { DECISION_TIMING_STOP_MANUAL, DECISION_TIMING_STOP_OVERFLOW }; static const unsigned long long bucket_limits_ns[DECISION_TIMING_BUCKETS - 1] = { 1000ULL, 5000ULL, 10000ULL, 50000ULL, 100000ULL, 500000ULL, 1000000ULL, 5000000ULL, 10000000ULL, 25000000ULL, 50000000ULL, 100000000ULL, 250000000ULL }; static const char *bucket_names[DECISION_TIMING_BUCKETS] = { "<=1us", "<=5us", "<=10us", "<=50us", "<=100us", "<=500us", "<=1ms", "<=5ms", "<=10ms", "<=25ms", "<=50ms", "<=100ms", "<=250ms", ">250ms" }; static const char *stage_names[DECISION_TIMING_STAGE_COUNT] = { "decision:total", "time_in_queue:total", "event_build:total", "event_build:cache_flush", "event_build:proc_fingerprint", "evaluation:proc_detail_lookup", "event_build:fd_stat", "evaluation:fd_path_resolution", "evaluation:mime_detection:total", "evaluation:mime_detection:fast_classification", "evaluation:mime_detection:gather_elf", "evaluation:mime_detection:libmagic_fallback", "response:mime_detection:total", "response:mime_detection:fast_classification", "response:mime_detection:gather_elf", "response:mime_detection:libmagic_fallback", "evaluation:hash_ima:total", "evaluation:hash_sha:total", "evaluation:trust_db_lookup:total", "evaluation:trust_db_lookup:lock_wait", "evaluation:trust_db_lookup:read", "response:trust_db_lookup:total", "response:trust_db_lookup:lock_wait", "response:trust_db_lookup:read", "evaluation:lock_wait", "evaluation:total", "response:total", "response:syslog_debug_format:total", "response:audit_metadata:total", "response:fanotify_write" }; /* * decision_timing_mode_name - return a timing mode name. * @mode: timing_collection_t value to describe. * Returns a printable mode name. */ static const char *decision_timing_mode_name(timing_collection_t mode) { switch (mode) { case TIMING_COLLECTION_OFF: return "off"; case TIMING_COLLECTION_MANUAL: return "manual"; } return "unknown"; } /* * config_timing_mode - atomically read the active timing mode. * @config: active daemon configuration. * Returns the configured timing collection mode. */ static timing_collection_t config_timing_mode(const conf_t *config) { return __atomic_load_n(&config->timing_collection, __ATOMIC_RELAXED); } static struct decision_timing_worker_block workers[DECISION_TIMING_MAX_WORKERS]; static atomic_bool timing_armed; /* * active_workers is one today. The storage and report aggregation already * support more workers so timing remains local when the decision path grows. */ static atomic_uint active_workers = 1; static atomic_uint arm_requests; static atomic_uint stop_requests; static atomic_int arm_request_pid = -1; static atomic_int arm_request_uid = -1; static atomic_int stop_request_pid = -1; static atomic_int stop_request_uid = -1; static atomic_long last_arm_time; static atomic_long last_stop_time; static atomic_long run_start_time; static atomic_ullong run_start_mono_ns; static atomic_ullong run_stop_mono_ns; static atomic_uint run_max_queue_depth; static atomic_uint saved_max_queue_depth; static atomic_bool queue_depth_active; static atomic_bool queue_depth_restore_requests; static decision_timing_queue_depth_reset_fn queue_depth_reset; static decision_timing_queue_depth_restore_fn queue_depth_restore; static void *queue_depth_ctx; static atomic_uint overflow_stop_requests; static atomic_int stop_reason = DECISION_TIMING_STOP_MANUAL; static atomic_int stop_reason_stage = -1; static atomic_bool missing_helper_driver_logged; __thread struct decision_timing_context decision_timing_tls; static const char *format_report_time(long when, char *buf, size_t buf_len) __attr_access ((__write_only__, 2, 3)); static void format_count(unsigned long long value, char *buf, size_t buf_len) __attr_access ((__write_only__, 2, 3)); static void format_scaled_time(double value, const char *unit, char *buf, size_t buf_len) __attr_access ((__write_only__, 3, 4)); static void format_human_duration(unsigned long long ns, char *buf, size_t buf_len) __attr_access ((__write_only__, 2, 3)); static void format_hms_duration(unsigned long long ns, char *buf, size_t buf_len) __attr_access ((__write_only__, 2, 3)); /* * ns_now - read monotonic time in nanoseconds. * Returns monotonic nanoseconds, or 0 if the clock cannot be read. */ static uint64_t ns_now(void) { struct timespec ts; if (clock_gettime(CLOCK_MONOTONIC, &ts)) return 0; return (uint64_t)ts.tv_sec * NSEC_PER_SEC + (uint64_t)ts.tv_nsec; } /* * bucket_for_duration - find the latency bucket for a duration. * @ns: elapsed nanoseconds. * Returns the bucket index. */ static unsigned int bucket_for_duration(uint64_t ns) { unsigned int i; for (i = 0; i < DECISION_TIMING_BUCKETS - 1; i++) { if (ns <= bucket_limits_ns[i]) return i; } return DECISION_TIMING_BUCKETS - 1; } /* * update_max - atomically retain the highest observed value. * @max: metric to update. * @value: candidate maximum. * Returns nothing. */ static void update_max(atomic_ullong *max, unsigned long long value) { unsigned long long old; old = atomic_load_explicit(max, memory_order_relaxed); while (value > old && !atomic_compare_exchange_weak_explicit(max, &old, value, memory_order_relaxed, memory_order_relaxed)) ; } /* * metric_add_unless_overflow - add to a counter without wrapping. * @value: counter to update. * @add: value to add. * Returns true on success, false if the addition would overflow. */ static bool metric_add_unless_overflow(atomic_ullong *value, unsigned long long add) { unsigned long long old; old = atomic_load_explicit(value, memory_order_relaxed); for (;;) { if (old > ULLONG_MAX - add) return false; if (atomic_compare_exchange_weak_explicit(value, &old, old + add, memory_order_relaxed, memory_order_relaxed)) return true; } } /* * decision_timing_overflow_stop - stop collection before counters wrap. * @stage: stage whose counters would overflow. * Returns nothing. * * The report is written from decision_timing_process_requests() so the hot * path only disarms collection and records why the run stopped. */ static void decision_timing_overflow_stop(decision_timing_stage_t stage) { decision_timing_tls.armed = false; if (!atomic_exchange_explicit(&timing_armed, false, memory_order_acq_rel)) return; atomic_store_explicit(&last_stop_time, (long)time(NULL), memory_order_relaxed); atomic_store_explicit(&run_stop_mono_ns, ns_now(), memory_order_relaxed); atomic_store_explicit(&stop_reason, DECISION_TIMING_STOP_OVERFLOW, memory_order_relaxed); atomic_store_explicit(&stop_reason_stage, (int)stage, memory_order_relaxed); atomic_fetch_add_explicit(&overflow_stop_requests, 1, memory_order_relaxed); msg(LOG_WARNING, "Decision timing stopped because %s counters would overflow", stage_names[stage]); } /* * record_stage - update the current worker's aggregate for one stage. * @stage: stage to update. * @ns: elapsed nanoseconds. * Returns nothing. */ static void record_stage(decision_timing_stage_t stage, uint64_t ns) { struct decision_timing_stage_metrics *metrics; unsigned int bucket; unsigned int worker_id; if (stage >= DECISION_TIMING_STAGE_COUNT) return; worker_id = decision_timing_tls.worker_id; if (worker_id >= DECISION_TIMING_MAX_WORKERS) return; metrics = &workers[worker_id].stages[stage]; bucket = bucket_for_duration(ns); if (!metric_add_unless_overflow(&metrics->count, 1)) goto overflow; if (!metric_add_unless_overflow(&metrics->total_ns, ns)) goto overflow; if (!metric_add_unless_overflow(&metrics->buckets[bucket], 1)) goto overflow; update_max(&metrics->max_ns, ns); return; overflow: decision_timing_overflow_stop(stage); } /* * reset_worker_blocks - clear all per-worker timing aggregates. * Returns nothing. */ static void reset_worker_blocks(void) { unsigned int worker, stage, bucket; for (worker = 0; worker < DECISION_TIMING_MAX_WORKERS; worker++) { for (stage = 0; stage < DECISION_TIMING_STAGE_COUNT; stage++) { struct decision_timing_stage_metrics *metrics = &workers[worker].stages[stage]; atomic_store_explicit(&metrics->count, 0, memory_order_relaxed); atomic_store_explicit(&metrics->total_ns, 0, memory_order_relaxed); atomic_store_explicit(&metrics->max_ns, 0, memory_order_relaxed); for (bucket = 0; bucket < DECISION_TIMING_BUCKETS; bucket++) atomic_store_explicit(&metrics->buckets[bucket], 0, memory_order_relaxed); } } } /* * snapshot_stage - add one stage's metrics into an aggregate snapshot. * @dst: aggregate snapshot to update. * @src: live per-worker metrics. * Returns nothing. */ static void snapshot_stage(struct decision_timing_stage_snapshot *dst, const struct decision_timing_stage_metrics *src) { unsigned int i; unsigned long long max; dst->count += atomic_load_explicit(&src->count, memory_order_relaxed); dst->total_ns += atomic_load_explicit(&src->total_ns, memory_order_relaxed); max = atomic_load_explicit(&src->max_ns, memory_order_relaxed); if (max > dst->max_ns) dst->max_ns = max; for (i = 0; i < DECISION_TIMING_BUCKETS; i++) dst->buckets[i] += atomic_load_explicit(&src->buckets[i], memory_order_relaxed); } /* * open_timing_report - open timing report file for overwrite without symlinks. * Return codes: * >= 0 - writable file descriptor for TIMING_REPORT * -1 - open or validation failed (errno set) */ static int open_timing_report(void) { struct stat st; int tfd; tfd = open(TIMING_REPORT, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0640); if (tfd < 0) return -1; if (fstat(tfd, &st) == -1 || !S_ISREG(st.st_mode)) { close(tfd); errno = EINVAL; return -1; } return tfd; } /* * format_report_time - format a wall-clock control timestamp. * @when: timestamp to format. * @buf: destination buffer. * @buf_len: size of @buf. * Returns @buf on success, or NULL when @buf cannot be initialized. */ static const char *format_report_time(long when, char *buf, size_t buf_len) { struct tm tm; time_t t = (time_t)when; if (buf == NULL || buf_len == 0) return NULL; if (when <= 0) { strncpy(buf, "never", buf_len - 1); buf[buf_len - 1] = 0; return buf; } if (localtime_r(&t, &tm) == NULL || strftime(buf, buf_len, "%Y-%m-%d %H:%M:%S %z", &tm) == 0) { strncpy(buf, "unavailable", buf_len - 1); buf[buf_len - 1] = 0; } return buf; } /* * format_count - format an integer count with thousands separators. * @value: count to format. * @buf: destination buffer. * @buf_len: size of @buf. * Returns nothing. */ static void format_count(unsigned long long value, char *buf, size_t buf_len) { char tmp[32], grouped[48]; size_t src, dst, group = 0; if (buf_len == 0) return; snprintf(tmp, sizeof(tmp), "%llu", value); src = strlen(tmp); dst = src + (src ? (src - 1) / 3 : 0); if (dst >= sizeof(grouped)) { strncpy(buf, tmp, buf_len - 1); buf[buf_len - 1] = 0; return; } grouped[dst] = 0; while (src > 0) { if (group == 3) { grouped[--dst] = ','; group = 0; } grouped[--dst] = tmp[--src]; group++; } strncpy(buf, grouped, buf_len - 1); buf[buf_len - 1] = 0; } /* * format_scaled_time - format a scaled time value with compact precision. * @value: scaled value. * @unit: unit suffix. * @buf: destination buffer. * @buf_len: size of @buf. * Returns nothing. */ static void format_scaled_time(double value, const char *unit, char *buf, size_t buf_len) { if (value >= 100.0) snprintf(buf, buf_len, "%.0f %s", value, unit); else if (value >= 10.0) snprintf(buf, buf_len, "%.1f %s", value, unit); else snprintf(buf, buf_len, "%.2f %s", value, unit); } /* * format_human_duration - format nanoseconds for human report output. * @ns: duration in nanoseconds. * @buf: destination buffer. * @buf_len: size of @buf. * Returns nothing. */ static void format_human_duration(unsigned long long ns, char *buf, size_t buf_len) { if (ns < 1000ULL) snprintf(buf, buf_len, "%llu ns", ns); else if (ns < 1000000ULL) format_scaled_time((double)ns / 1000.0, "us", buf, buf_len); else if (ns < 1000000000ULL) format_scaled_time((double)ns / 1000000.0, "ms", buf, buf_len); else format_scaled_time((double)ns / 1000000000.0, "s", buf, buf_len); } /* * format_hms_duration - format nanoseconds as H:MM:SS. * @ns: duration in nanoseconds. * @buf: destination buffer. * @buf_len: size of @buf. * Returns nothing. */ static void format_hms_duration(unsigned long long ns, char *buf, size_t buf_len) { unsigned long long seconds = ns / 1000000000ULL; unsigned long long hours = seconds / 3600ULL; unsigned long long minutes = (seconds % 3600ULL) / 60ULL; seconds %= 60ULL; snprintf(buf, buf_len, "%llu:%02llu:%02llu", hours, minutes, seconds); } /* * bucket_cumulative_count - count observations up to a latency bucket. * @src: snapshot to inspect. * @bucket: inclusive bucket index. * Returns the cumulative count. */ static unsigned long long bucket_cumulative_count( const struct decision_timing_stage_snapshot *src, unsigned int bucket) { unsigned long long count = 0; unsigned int i; if (bucket >= DECISION_TIMING_BUCKETS) bucket = DECISION_TIMING_BUCKETS - 1; for (i = 0; i <= bucket; i++) count += src->buckets[i]; return count; } /* * percent_of_count - calculate a percentage from two counts. * @value: numerator. * @total: denominator. * Returns the percentage, or zero when @total is zero. */ static double percent_of_count(unsigned long long value, unsigned long long total) { if (total == 0) return 0.0; return ((double)value * 100.0) / (double)total; } /* * percentile_bucket - estimate a percentile from fixed latency buckets. * @src: snapshot to inspect. * @percentile: percentile to estimate, from 1 to 100. * Returns a human bucket label. */ static const char *percentile_bucket( const struct decision_timing_stage_snapshot *src, unsigned int percentile) { unsigned long long cumulative = 0, target; unsigned int i; if (src->count == 0) return "n/a"; if (percentile > 100) percentile = 100; if (percentile == 0) percentile = 1; target = (src->count * percentile + 99) / 100; if (target == 0) target = 1; for (i = 0; i < DECISION_TIMING_BUCKETS; i++) { cumulative += src->buckets[i]; if (cumulative >= target) return bucket_names[i]; } return bucket_names[DECISION_TIMING_BUCKETS - 1]; } /* * percentile_bucket_index - estimate a percentile bucket index. * @src: snapshot to inspect. * @percentile: percentile to estimate, from 1 to 100. * Returns a bucket index, or DECISION_TIMING_BUCKETS when no samples exist. */ static unsigned int percentile_bucket_index( const struct decision_timing_stage_snapshot *src, unsigned int percentile) { unsigned long long cumulative = 0, target; unsigned int i; if (src->count == 0) return DECISION_TIMING_BUCKETS; if (percentile > 100) percentile = 100; if (percentile == 0) percentile = 1; target = (src->count * percentile + 99) / 100; if (target == 0) target = 1; for (i = 0; i < DECISION_TIMING_BUCKETS; i++) { cumulative += src->buckets[i]; if (cumulative >= target) return i; } return DECISION_TIMING_BUCKETS - 1; } /* * bucket_count_above - count observations above a latency threshold bucket. * @src: snapshot to inspect. * @bucket: bucket at or below the threshold. * Returns observations in buckets above @bucket. */ static unsigned long long bucket_count_above( const struct decision_timing_stage_snapshot *src, unsigned int bucket) { if (src->count == 0) return 0; if (bucket >= DECISION_TIMING_BUCKETS - 1) return 0; return src->count - bucket_cumulative_count(src, bucket); } /* * sample_has_tail - test whether a sample has high-end tail observations. * @src: snapshot to inspect. * Returns true if any observation is above 10ms. */ static bool sample_has_tail(const struct decision_timing_stage_snapshot *src) { return bucket_count_above(src, 8) != 0; } /* * write_tail_counts - write compact high-end tail counts. * @f: output stream. * @src: snapshot to inspect. * @label: true to prefix the line with "tail:". * Returns nothing. */ static void write_tail_counts(FILE *f, const struct decision_timing_stage_snapshot *src, bool label) { static const struct { const char *name; unsigned int bucket; } tails[] = { { ">10ms", 8 }, { ">25ms", 9 }, { ">50ms", 10 }, { ">100ms", 11 }, { ">250ms", 12 }, }; char count[32]; unsigned int i; bool any = false; if (label) fprintf(f, "tail:"); for (i = 0; i < sizeof(tails) / sizeof(tails[0]); i++) { unsigned long long value = bucket_count_above(src, tails[i].bucket); if (value == 0) continue; format_count(value, count, sizeof(count)); fprintf(f, "%s%s %s/%.1f%%", any ? ", " : (label ? " " : ""), tails[i].name, count, percent_of_count(value, src->count)); any = true; } fputc('\n', f); } /* * write_tail_summary - write labeled high-end tail counts. * @f: output stream. * @src: snapshot to inspect. * Returns nothing. */ static void write_tail_summary(FILE *f, const struct decision_timing_stage_snapshot *src) { write_tail_counts(f, src, true); } /* * sort_stages_by_total - rank observed stages by total time descending. * @totals: aggregate stage snapshots. * @order: output order. * Returns nothing. */ static void sort_stages_by_total( const struct decision_timing_stage_snapshot *totals, struct decision_timing_stage_order *order) { unsigned int i, j; order->count = 0; for (i = 0; i < DECISION_TIMING_STAGE_COUNT; i++) { if (totals[i].count) order->stages[order->count++] = i; } for (i = 0; i < order->count; i++) { for (j = i + 1; j < order->count; j++) { unsigned int left = order->stages[i]; unsigned int right = order->stages[j]; if (totals[right].total_ns > totals[left].total_ns) { order->stages[i] = right; order->stages[j] = left; } } } } /* * find_slowest_stage - find the observed stage with the largest max. * @totals: aggregate stage snapshots. * @stage_out: output stage index. * Returns true when a stage was found. */ static bool find_slowest_stage( const struct decision_timing_stage_snapshot *totals, unsigned int *stage_out) { unsigned int i, stage = 0; unsigned long long max = 0; for (i = 1; i < DECISION_TIMING_STAGE_COUNT; i++) { if (totals[i].count && totals[i].max_ns > max) { max = totals[i].max_ns; stage = i; } } if (stage == 0) return false; *stage_out = stage; return true; } /* * stage_observed - test whether a stage has any samples. * @ctx: report context. * @stage: stage to inspect. * Returns true when the stage has at least one observation. */ static bool stage_observed(const struct decision_timing_report_ctx *ctx, decision_timing_stage_t stage) { if (stage >= DECISION_TIMING_STAGE_COUNT) return false; return ctx->totals[stage].count != 0; } /* * stage_avg_ns - calculate average observed latency for a stage. * @sample: stage aggregate. * Returns average nanoseconds, or zero for an empty stage. */ static unsigned long long stage_avg_ns( const struct decision_timing_stage_snapshot *sample) { if (sample->count == 0) return 0; return sample->total_ns / sample->count; } /* * stage_snapshot_add - add one stage snapshot to an aggregate. * @dst: aggregate snapshot to update. * @src: snapshot to add. * Returns nothing. */ static void stage_snapshot_add(struct decision_timing_stage_snapshot *dst, const struct decision_timing_stage_snapshot *src) { unsigned int i; dst->count += src->count; dst->total_ns += src->total_ns; if (src->max_ns > dst->max_ns) dst->max_ns = src->max_ns; for (i = 0; i < DECISION_TIMING_BUCKETS; i++) dst->buckets[i] += src->buckets[i]; } /* * helper_snapshot - build a combined snapshot for one helper row. * @ctx: report context. * @row: helper row to aggregate. * @dst: output aggregate. * Returns nothing. */ static void helper_snapshot(const struct decision_timing_report_ctx *ctx, const struct decision_timing_helper_row *row, struct decision_timing_stage_snapshot *dst) { memset(dst, 0, sizeof(*dst)); if (row->eval_stage < DECISION_TIMING_STAGE_COUNT) stage_snapshot_add(dst, &ctx->totals[row->eval_stage]); if (row->response_stage < DECISION_TIMING_STAGE_COUNT) stage_snapshot_add(dst, &ctx->totals[row->response_stage]); } /* * stage_calls_per_decision - calculate a stage's calls per decision. * @ctx: report context. * @stage: stage to inspect. * Returns calls per timed decision. */ static double stage_calls_per_decision( const struct decision_timing_report_ctx *ctx, decision_timing_stage_t stage) { if (ctx->decisions == 0) return 0.0; return (double)ctx->totals[stage].count / (double)ctx->decisions; } /* * sample_calls_per_decision - calculate calls per decision for a snapshot. * @ctx: report context. * @sample: aggregate sample. * Returns calls per timed decision. */ static double sample_calls_per_decision( const struct decision_timing_report_ctx *ctx, const struct decision_timing_stage_snapshot *sample) { if (ctx->decisions == 0) return 0.0; return (double)sample->count / (double)ctx->decisions; } /* * stage_amortized_ns - calculate stage time amortized over all decisions. * @ctx: report context. * @stage: stage to inspect. * Returns nanoseconds per timed decision. */ static unsigned long long stage_amortized_ns( const struct decision_timing_report_ctx *ctx, decision_timing_stage_t stage) { if (ctx->decisions == 0) return 0; return ctx->totals[stage].total_ns / ctx->decisions; } /* * sample_amortized_ns - calculate sample time amortized over all decisions. * @ctx: report context. * @sample: aggregate sample. * Returns nanoseconds per timed decision. */ static unsigned long long sample_amortized_ns( const struct decision_timing_report_ctx *ctx, const struct decision_timing_stage_snapshot *sample) { if (ctx->decisions == 0) return 0; return sample->total_ns / ctx->decisions; } /* * stage_time_share - calculate what share one stage is of another stage. * @ctx: report context. * @part: stage that contributes time. * @whole: stage that represents the larger total. * Returns percentage share, or zero when the whole stage has no total. */ static double stage_time_share(const struct decision_timing_report_ctx *ctx, decision_timing_stage_t part, decision_timing_stage_t whole) { if (ctx->totals[whole].total_ns == 0) return 0.0; return ((double)ctx->totals[part].total_ns * 100.0) / (double)ctx->totals[whole].total_ns; } /* * find_largest_named_stage - find observed named row with largest total time. * @ctx: report context. * @rows: stage list to inspect. * @row_count: number of entries in @rows. * @row_out: selected row index. * Returns true when an observed row was found. */ static bool find_largest_named_stage( const struct decision_timing_report_ctx *ctx, const struct decision_timing_named_stage *rows, unsigned int row_count, unsigned int *row_out) { unsigned int i, best = 0; unsigned long long total = 0; for (i = 0; i < row_count; i++) { decision_timing_stage_t stage = rows[i].stage; if (!stage_observed(ctx, stage)) continue; if (ctx->totals[stage].total_ns > total) { total = ctx->totals[stage].total_ns; best = i; } } if (total == 0) return false; *row_out = best; return true; } /* * write_overall_latency - write human total decision latency summary. * @f: output stream. * @total: total decision latency snapshot. * Returns nothing. */ static void write_overall_latency(FILE *f, const struct decision_timing_stage_snapshot *total) { char avg[32], max[32]; unsigned long long avg_ns = 0; fprintf(f, "\nOverall decision latency:\n"); if (total->count == 0) { fprintf(f, " no decisions observed\n"); return; } avg_ns = total->total_ns / total->count; format_human_duration(avg_ns, avg, sizeof(avg)); format_human_duration(total->max_ns, max, sizeof(max)); fprintf(f, " avg %s, max %s\n", avg, max); fprintf(f, " p50 bucket %s, p95 bucket %s, p99 bucket %s\n", percentile_bucket(total, 50), percentile_bucket(total, 95), percentile_bucket(total, 99)); fprintf(f, " <=50us %.1f%%, <=100us %.1f%%, <=500us %.1f%%, " "<=1ms %.1f%%, >10ms %.1f%%\n", percent_of_count(bucket_cumulative_count(total, 3), total->count), percent_of_count(bucket_cumulative_count(total, 4), total->count), percent_of_count(bucket_cumulative_count(total, 5), total->count), percent_of_count(bucket_cumulative_count(total, 6), total->count), percent_of_count(bucket_count_above(total, 8), total->count)); if (sample_has_tail(total)) { fprintf(f, " "); write_tail_summary(f, total); } } /* * write_queueing - write queue wait summary. * @ctx: report context. * Returns nothing. */ static void write_queueing(const struct decision_timing_report_ctx *ctx) { const struct decision_timing_stage_snapshot *queue = &ctx->totals[DECISION_TIMING_STAGE_QUEUE_WAIT]; char avg[32], max[32], total[32]; fprintf(ctx->f, "\nQueueing:\n"); if (queue->count == 0) { fprintf(ctx->f, " not observed\n"); fprintf(ctx->f, " max queue depth: %u\n", ctx->max_queue_depth); return; } format_human_duration(stage_avg_ns(queue), avg, sizeof(avg)); format_human_duration(queue->max_ns, max, sizeof(max)); format_human_duration(queue->total_ns, total, sizeof(total)); fprintf(ctx->f, " avg wait: %s\n", avg); fprintf(ctx->f, " max wait: %s\n", max); fprintf(ctx->f, " p95 bucket: %s\n", percentile_bucket(queue, 95)); fprintf(ctx->f, " total queued time: %s\n", total); fprintf(ctx->f, " max queue depth: %u\n", ctx->max_queue_depth); } /* * write_worker_skew - write per-worker timing skew summary. * @ctx: report context. * Returns nothing. */ static void write_worker_skew(const struct decision_timing_report_ctx *ctx) { unsigned int i; unsigned long long min_decisions = ULLONG_MAX; unsigned long long max_decisions = 0; if (ctx->workers == NULL || ctx->worker_count <= 1) return; for (i = 0; i < ctx->worker_count; i++) { const struct decision_timing_stage_snapshot *decision = &ctx->workers[i].stages[DECISION_TIMING_STAGE_TOTAL]; if (decision->count < min_decisions) min_decisions = decision->count; if (decision->count > max_decisions) max_decisions = decision->count; } fprintf(ctx->f, "\nPer-worker timing skew:\n"); if (max_decisions == 0) { fprintf(ctx->f, " no decisions observed per worker\n"); return; } fprintf(ctx->f, " decisions min=%llu max=%llu spread=%llu\n", min_decisions, max_decisions, max_decisions - min_decisions); fprintf(ctx->f, " Worker %10s %12s %10s %10s %12s\n", "Decisions", "Total", "Avg", "Max", "Queue p95"); for (i = 0; i < ctx->worker_count; i++) { const struct decision_timing_worker_snapshot *worker = &ctx->workers[i]; const struct decision_timing_stage_snapshot *decision = &worker->stages[DECISION_TIMING_STAGE_TOTAL]; const struct decision_timing_stage_snapshot *queue = &worker->stages[DECISION_TIMING_STAGE_QUEUE_WAIT]; char decisions[32], total[32], avg[32], max[32]; format_count(decision->count, decisions, sizeof(decisions)); format_human_duration(decision->total_ns, total, sizeof(total)); format_human_duration(stage_avg_ns(decision), avg, sizeof(avg)); format_human_duration(decision->max_ns, max, sizeof(max)); fprintf(ctx->f, " %6u %10s %12s %10s %10s %12s\n", worker->worker_id, decisions, total, avg, max, percentile_bucket(queue, 95)); } } /* * write_phase_row - write one phase timing row. * @ctx: report context. * @name: displayed phase name. * @stage: stage that stores the phase total. * @note: optional note for the phase row. * Returns nothing. */ static void write_phase_row(const struct decision_timing_report_ctx *ctx, const char *name, decision_timing_stage_t stage, const char *note) { const struct decision_timing_stage_snapshot *sample = &ctx->totals[stage]; char calls[32], total[32], avg[32], max[32]; format_count(sample->count, calls, sizeof(calls)); format_human_duration(sample->total_ns, total, sizeof(total)); format_human_duration(stage_avg_ns(sample), avg, sizeof(avg)); format_human_duration(sample->max_ns, max, sizeof(max)); fprintf(ctx->f, "%-*s %10s %10.2f %10s %10s %10s %12s %s\n", DECISION_TIMING_PHASE_WIDTH, name, calls, stage_calls_per_decision(ctx, stage), total, avg, max, percentile_bucket(sample, 95), note ? note : ""); } /* * write_response_format_note - explain debug formatting share when visible. * @ctx: report context. * Returns nothing. */ static void write_response_format_note( const struct decision_timing_report_ctx *ctx) { char format_total[32], response_total[32]; if (!stage_observed(ctx, DECISION_TIMING_STAGE_RESPONSE_TOTAL) || !stage_observed(ctx, DECISION_TIMING_STAGE_SYSLOG_DEBUG_FORMAT)) return; if (stage_time_share(ctx, DECISION_TIMING_STAGE_SYSLOG_DEBUG_FORMAT, DECISION_TIMING_STAGE_RESPONSE_TOTAL) < RESPONSE_FORMATTING_DOMINANT) return; format_human_duration( ctx->totals[DECISION_TIMING_STAGE_SYSLOG_DEBUG_FORMAT].total_ns, format_total, sizeof(format_total)); format_human_duration( ctx->totals[DECISION_TIMING_STAGE_RESPONSE_TOTAL].total_ns, response_total, sizeof(response_total)); fprintf(ctx->f, "\nResponse note:\n"); fprintf(ctx->f, " response:syslog_debug_format accounts for %s of %s response time.\n", format_total, response_total); fprintf(ctx->f, " In manual/debug-heavy runs this may overstate daemon-mode " "response cost.\n"); } /* * write_phase_timing - write the high-level decision phase table. * @ctx: report context. * Returns nothing. */ static void write_phase_timing(const struct decision_timing_report_ctx *ctx) { static const struct decision_timing_named_stage phases[] = { { DECISION_TIMING_STAGE_EVENT_BUILD, "event_build" }, { DECISION_TIMING_STAGE_RULE_EVALUATION, "evaluation" }, { DECISION_TIMING_STAGE_RESPONSE_TOTAL, "response" }, }; unsigned int i; bool any = false; fprintf(ctx->f, "\nDecision phase timing:\n"); fprintf(ctx->f, "%-*s %10s %10s %10s %10s %10s %12s %s\n", DECISION_TIMING_PHASE_WIDTH, "Phase", "Calls", "Calls/Dec", "Total", "Avg", "Max", "p95 bucket", "Notes"); for (i = 0; i < sizeof(phases) / sizeof(phases[0]); i++) { const char *note = ""; decision_timing_stage_t stage = phases[i].stage; if (!stage_observed(ctx, stage)) continue; if (stage == DECISION_TIMING_STAGE_RESPONSE_TOTAL && stage_time_share(ctx, DECISION_TIMING_STAGE_SYSLOG_DEBUG_FORMAT, DECISION_TIMING_STAGE_RESPONSE_TOTAL) >= RESPONSE_FORMATTING_DOMINANT) note = "syslog/debug-heavy"; write_phase_row(ctx, phases[i].name, stage, note); any = true; } if (!any) fprintf(ctx->f, " not observed\n"); write_response_format_note(ctx); } /* * write_helper_attribution_intro - explain helper driver attribution. * @f: output stream. * Returns nothing. */ static void write_helper_attribution_intro(FILE *f) { fprintf(f, "\nLazy helper attribution:\n"); fprintf(f, " Helper timings are attributed to the active logical driver: " "evaluation or response.\n"); fprintf(f, " Combined totals are evaluation + response.\n"); } static const struct decision_timing_helper_row helper_rows[] = { { "mime_detection:total", DECISION_TIMING_STAGE_EVAL_MIME_DETECTION, DECISION_TIMING_STAGE_RESPONSE_MIME_DETECTION, true }, { "mime_detection:fast_classification", DECISION_TIMING_STAGE_EVAL_MIME_FAST_CLASSIFICATION, DECISION_TIMING_STAGE_RESPONSE_MIME_FAST_CLASSIFICATION, true }, { "mime_detection:gather_elf", DECISION_TIMING_STAGE_EVAL_MIME_GATHER_ELF, DECISION_TIMING_STAGE_RESPONSE_MIME_GATHER_ELF, true }, { "mime_detection:libmagic_fallback", DECISION_TIMING_STAGE_EVAL_MIME_LIBMAGIC_FALLBACK, DECISION_TIMING_STAGE_RESPONSE_MIME_LIBMAGIC_FALLBACK, true }, { "trust_db_lookup:total", DECISION_TIMING_STAGE_EVAL_TRUST_DB_LOOKUP, DECISION_TIMING_STAGE_RESPONSE_TRUST_DB_LOOKUP, true }, { "trust_db_lookup:read", DECISION_TIMING_STAGE_EVAL_TRUST_DB_READ, DECISION_TIMING_STAGE_RESPONSE_TRUST_DB_READ, true }, { "trust_db_lookup:lock_wait", DECISION_TIMING_STAGE_EVAL_TRUST_DB_LOCK_WAIT, DECISION_TIMING_STAGE_RESPONSE_TRUST_DB_LOCK_WAIT, true }, { "hash_ima:total", DECISION_TIMING_STAGE_HASH_IMA, DECISION_TIMING_STAGE_COUNT, false }, { "hash_sha:total", DECISION_TIMING_STAGE_HASH_SHA, DECISION_TIMING_STAGE_COUNT, false }, { "proc_detail_lookup", DECISION_TIMING_STAGE_PROC_STATUS_EXE_LOOKUP, DECISION_TIMING_STAGE_COUNT, false }, }; #define HELPER_ROW_MIME_TOTAL 0 #define HELPER_ROW_MIME_FAST 1 #define HELPER_ROW_MIME_GATHER 2 #define HELPER_ROW_MIME_LIBMAGIC 3 #define HELPER_ROW_TRUST_TOTAL 4 #define HELPER_ROW_TRUST_READ 5 #define HELPER_ROW_TRUST_LOCK 6 #define HELPER_ROW_HASH_IMA 7 #define HELPER_ROW_HASH_SHA 8 #define HELPER_ROW_PROC_DETAIL 9 static const unsigned int helper_total_rows[] = { HELPER_ROW_MIME_TOTAL, HELPER_ROW_TRUST_TOTAL, HELPER_ROW_HASH_IMA, HELPER_ROW_HASH_SHA, HELPER_ROW_PROC_DETAIL }; /* * find_largest_helper - find helper total row with largest combined time. * @ctx: report context. * @row_out: selected helper_rows index. * @sample_out: selected combined sample. * Returns true when an observed helper was found. */ static bool find_largest_helper(const struct decision_timing_report_ctx *ctx, unsigned int *row_out, struct decision_timing_stage_snapshot *sample_out) { struct decision_timing_stage_snapshot sample; unsigned int i, best = 0; unsigned long long total = 0; for (i = 0; i < sizeof(helper_total_rows) / sizeof(helper_total_rows[0]); i++) { unsigned int row = helper_total_rows[i]; helper_snapshot(ctx, &helper_rows[row], &sample); if (sample.total_ns > total) { total = sample.total_ns; best = row; if (sample_out) *sample_out = sample; } } if (total == 0) return false; if (row_out) *row_out = best; return true; } /* * write_helper_driver_row - write one helper driver attribution row. * @ctx: report context. * @row: helper row to display. * Returns true when the row was observed. */ static bool write_helper_driver_row( const struct decision_timing_report_ctx *ctx, const struct decision_timing_helper_row *row) { struct decision_timing_stage_snapshot combined; char eval[32], response[32], total[32]; double response_share = 0.0; size_t name_len; int eval_width = 12; if (!row->by_driver) return false; helper_snapshot(ctx, row, &combined); if (combined.count == 0) return false; format_human_duration(ctx->totals[row->eval_stage].total_ns, eval, sizeof(eval)); format_human_duration(ctx->totals[row->response_stage].total_ns, response, sizeof(response)); format_human_duration(combined.total_ns, total, sizeof(total)); if (combined.total_ns) response_share = ((double)ctx->totals[row->response_stage].total_ns * 100.0) / (double)combined.total_ns; name_len = strlen(row->name); if (name_len > DECISION_TIMING_DRIVER_WIDTH) eval_width -= name_len - DECISION_TIMING_DRIVER_WIDTH; if (eval_width < 1) eval_width = 1; fprintf(ctx->f, "%-*s %*s %15s %12s %10.1f%%\n", DECISION_TIMING_DRIVER_WIDTH, row->name, eval_width, eval, response, total, response_share); return true; } /* * write_helper_by_driver - write phase-specific helper attribution. * @ctx: report context. * Returns nothing. */ static void write_helper_by_driver( const struct decision_timing_report_ctx *ctx) { unsigned int i; bool any = false; fprintf(ctx->f, "\nLazy helper attribution by driver:\n"); fprintf(ctx->f, "%-*s %12s %15s %12s %10s\n", DECISION_TIMING_DRIVER_WIDTH, "Helper", "Eval total", "Response total", "Combined", "Response %"); for (i = 0; i < sizeof(helper_rows) / sizeof(helper_rows[0]); i++) any |= write_helper_driver_row(ctx, &helper_rows[i]); if (!any) fprintf(ctx->f, " not observed\n"); } /* * write_helper_row - write one combined lazy helper attribution row. * @ctx: report context. * @row: helper row to display. * Returns true when the row was observed. */ static bool write_helper_row(const struct decision_timing_report_ctx *ctx, const struct decision_timing_helper_row *row) { struct decision_timing_stage_snapshot sample; char calls[32], total[32], avg[32], amortized[32], max[32]; helper_snapshot(ctx, row, &sample); if (sample.count == 0) return false; format_count(sample.count, calls, sizeof(calls)); format_human_duration(sample.total_ns, total, sizeof(total)); format_human_duration(stage_avg_ns(&sample), avg, sizeof(avg)); format_human_duration(sample_amortized_ns(ctx, &sample), amortized, sizeof(amortized)); format_human_duration(sample.max_ns, max, sizeof(max)); fprintf(ctx->f, "%-*s %10s %10.2f %10s %10s %10s %10s %12s\n", DECISION_TIMING_HELPER_WIDTH, row->name, calls, sample_calls_per_decision(ctx, &sample), total, avg, amortized, max, percentile_bucket(&sample, 95)); return true; } /* * write_lazy_helpers - write grouped lazy helper attribution rows. * @ctx: report context. * Returns nothing. */ static void write_lazy_helpers(const struct decision_timing_report_ctx *ctx) { unsigned int i; bool any = false; fprintf(ctx->f, "\nCombined lazy helper attribution:\n"); fprintf(ctx->f, "%-*s %10s %10s %10s %10s %10s %10s %12s\n", DECISION_TIMING_HELPER_WIDTH, "Helper path", "Calls", "Calls/Dec", "Total", "Avg/call", "Amort/Dec", "Max", "p95 bucket"); for (i = 0; i < sizeof(helper_rows) / sizeof(helper_rows[0]); i++) { if (write_helper_row(ctx, &helper_rows[i])) any = true; } if (!any) fprintf(ctx->f, " not observed\n"); } /* * write_idle_observation - compare wall-clock and active decision rates. * @ctx: report context. * Returns true when an observation was written. */ static bool write_idle_observation( const struct decision_timing_report_ctx *ctx) { const struct decision_timing_stage_snapshot *decision = &ctx->totals[DECISION_TIMING_STAGE_TOTAL]; double wall_rate, active_rate; if (ctx->duration_ns == 0 || decision->total_ns == 0 || ctx->decisions == 0) return false; wall_rate = (double)ctx->decisions / ((double)ctx->duration_ns / (double)NSEC_PER_SEC); active_rate = (double)ctx->decisions / ((double)decision->total_ns / (double)NSEC_PER_SEC); if (active_rate < wall_rate * IDLE_WORKLOAD_RATE_MULTIPLIER) return false; fprintf(ctx->f, " The workload was mostly idle: wall-clock rate is %.1f/sec, " "active decision rate is %.1f/sec.\n", wall_rate, active_rate); return true; } /* * write_queueing_observation - describe queue depth and wait behavior. * @ctx: report context. * Returns true when an observation was written. */ static bool write_queueing_observation( const struct decision_timing_report_ctx *ctx) { const struct decision_timing_stage_snapshot *queue = &ctx->totals[DECISION_TIMING_STAGE_QUEUE_WAIT]; const char *p95; char max[32]; double fullness; if (queue->count == 0) return false; p95 = percentile_bucket(queue, 95); format_human_duration(queue->max_ns, max, sizeof(max)); if (ctx->q_size == 0) { fprintf(ctx->f, " Queueing: p95 wait %s, max wait %s, max queue depth %u.\n", p95, max, ctx->max_queue_depth); return true; } fullness = ((double)ctx->max_queue_depth * 100.0) / (double)ctx->q_size; if (ctx->max_queue_depth <= 1) { fprintf(ctx->f, " Queueing was minimal: max queue depth %u of %u, " "p95 wait %s, max wait %s.\n", ctx->max_queue_depth, ctx->q_size, p95, max); } else if (fullness < 25.0 && percentile_bucket_index(queue, 95) <= 4) { fprintf(ctx->f, " Queueing was low with small bursts: max " "queue depth %u of %u (%.1f%%), p95 wait %s, " "max wait %s.\n", ctx->max_queue_depth, ctx->q_size, fullness, p95, max); } else if (fullness < 50.0) { fprintf(ctx->f, " Queueing showed moderate bursts: max queue depth " "%u of %u (%.1f%%), p95 wait %s, max wait %s.\n", ctx->max_queue_depth, ctx->q_size, fullness, p95, max); } else if (fullness < 80.0) { fprintf(ctx->f, " Queueing showed significant backlog pressure: " "max queue depth %u of %u (%.1f%%), p95 wait %s, " "max wait %s.\n", ctx->max_queue_depth, ctx->q_size, fullness, p95, max); } else { fprintf(ctx->f, " Queueing approached capacity: max queue depth %u " "of %u (%.1f%%), p95 wait %s, max wait %s.\n", ctx->max_queue_depth, ctx->q_size, fullness, p95, max); } return true; } /* * write_response_observation - describe response formatting dominance. * @ctx: report context. * Returns true when an observation was written. */ static bool write_response_observation( const struct decision_timing_report_ctx *ctx) { char debug_total[32], response_total[32]; if (!stage_observed(ctx, DECISION_TIMING_STAGE_RESPONSE_TOTAL) || !stage_observed(ctx, DECISION_TIMING_STAGE_SYSLOG_DEBUG_FORMAT)) return false; if (stage_time_share(ctx, DECISION_TIMING_STAGE_SYSLOG_DEBUG_FORMAT, DECISION_TIMING_STAGE_RESPONSE_TOTAL) < RESPONSE_FORMATTING_DOMINANT) return false; format_human_duration( ctx->totals[DECISION_TIMING_STAGE_SYSLOG_DEBUG_FORMAT].total_ns, debug_total, sizeof(debug_total)); format_human_duration( ctx->totals[DECISION_TIMING_STAGE_RESPONSE_TOTAL].total_ns, response_total, sizeof(response_total)); fprintf(ctx->f, " Manual/debug response formatting dominates response time: " "%s of %s response time.\n", debug_total, response_total); return true; } /* * write_mime_observations - describe MIME helper cost shares. * @ctx: report context. * Returns true when any observation was written. */ static bool write_mime_observations( const struct decision_timing_report_ctx *ctx) { struct decision_timing_stage_snapshot mime; struct decision_timing_stage_snapshot fallback; struct decision_timing_stage_snapshot fast; struct decision_timing_stage_snapshot gather; struct decision_timing_stage_snapshot largest_sample; unsigned int largest; char total[32], amortized[32]; bool any = false; helper_snapshot(ctx, &helper_rows[HELPER_ROW_MIME_TOTAL], &mime); if (mime.count == 0) return false; if (find_largest_helper(ctx, &largest, &largest_sample) && largest == HELPER_ROW_MIME_TOTAL) { format_human_duration(largest_sample.total_ns, total, sizeof(total)); fprintf(ctx->f, " MIME detection is the largest helper cost (%s).\n", total); any = true; } helper_snapshot(ctx, &helper_rows[HELPER_ROW_MIME_LIBMAGIC], &fallback); if (fallback.count == 0) return any; helper_snapshot(ctx, &helper_rows[HELPER_ROW_MIME_FAST], &fast); helper_snapshot(ctx, &helper_rows[HELPER_ROW_MIME_GATHER], &gather); format_human_duration(sample_amortized_ns(ctx, &fallback), amortized, sizeof(amortized)); if (fallback.total_ns >= fast.total_ns && fallback.total_ns >= gather.total_ns) { fprintf(ctx->f, " libmagic fallback is the biggest MIME contributor: " "%.1f%% of MIME calls, %.1f%% of MIME time, %s " "amortized per decision.\n", percent_of_count(fallback.count, mime.count), percent_of_count(fallback.total_ns, mime.total_ns), amortized); } else { fprintf(ctx->f, " libmagic fallback accounts for %.1f%% of MIME calls, " "%.1f%% of MIME time, %s amortized per decision.\n", percent_of_count(fallback.count, mime.count), percent_of_count(fallback.total_ns, mime.total_ns), amortized); } return true; } /* * write_hash_observation - describe rare integrity measurement cost. * @ctx: report context. * @stage: stage that stores the integrity measurement. * @name: report helper name. * Returns true when an observation was written. */ static bool write_hash_observation(const struct decision_timing_report_ctx *ctx, decision_timing_stage_t stage, const char *name) { const struct decision_timing_stage_snapshot *hash = &ctx->totals[stage]; double call_share; char avg[32], amortized[32]; if (!stage_observed(ctx, stage) || ctx->decisions == 0) return false; call_share = percent_of_count(hash->count, ctx->decisions); if (call_share >= HASH_RARE_SHARE) return false; format_human_duration(stage_avg_ns(hash), avg, sizeof(avg)); format_human_duration(stage_amortized_ns(ctx, stage), amortized, sizeof(amortized)); fprintf(ctx->f, " %s is rare but expensive: %.1f%% of decisions, " "%s avg when called, %s amortized per decision.\n", name, call_share, avg, amortized); return true; } /* * write_trust_db_observation - describe trust DB lock versus read cost. * @ctx: report context. * Returns true when an observation was written. */ static bool write_trust_db_observation( const struct decision_timing_report_ctx *ctx) { struct decision_timing_stage_snapshot lock; struct decision_timing_stage_snapshot read; double lock_share; helper_snapshot(ctx, &helper_rows[HELPER_ROW_TRUST_LOCK], &lock); helper_snapshot(ctx, &helper_rows[HELPER_ROW_TRUST_READ], &read); if (lock.count == 0 || read.count == 0) return false; if (read.total_ns == 0) return false; lock_share = ((double)lock.total_ns * 100.0) / (double)read.total_ns; if (lock_share > TRUST_DB_LOCK_TINY_SHARE) return false; fprintf(ctx->f, " trust DB lock wait is negligible; trust DB read time is " "the relevant cost.\n"); return true; } /* * write_tldr_mime - write a compact MIME helper timing finding. * @ctx: report context. * Returns true when a finding was written. */ static bool write_tldr_mime(const struct decision_timing_report_ctx *ctx) { struct decision_timing_stage_snapshot mime; struct decision_timing_stage_snapshot fallback; struct decision_timing_stage_snapshot fast; struct decision_timing_stage_snapshot gather; struct decision_timing_stage_snapshot largest_sample; unsigned int largest; char total[32]; helper_snapshot(ctx, &helper_rows[HELPER_ROW_MIME_TOTAL], &mime); if (mime.count == 0) return false; if (!find_largest_helper(ctx, &largest, &largest_sample) || largest != HELPER_ROW_MIME_TOTAL) return false; helper_snapshot(ctx, &helper_rows[HELPER_ROW_MIME_LIBMAGIC], &fallback); helper_snapshot(ctx, &helper_rows[HELPER_ROW_MIME_FAST], &fast); helper_snapshot(ctx, &helper_rows[HELPER_ROW_MIME_GATHER], &gather); format_human_duration(largest_sample.total_ns, total, sizeof(total)); if (fallback.count && fallback.total_ns >= fast.total_ns && fallback.total_ns >= gather.total_ns) fprintf(ctx->f, " - MIME detection dominates helper time (%s); " "libmagic fallback is the biggest contributor.\n", total); else fprintf(ctx->f, " - MIME detection is the largest helper cost (%s).\n", total); return true; } /* * write_tldr_response - write a compact response formatting finding. * @ctx: report context. * Returns true when a finding was written. */ static bool write_tldr_response( const struct decision_timing_report_ctx *ctx) { char debug_total[32], response_total[32]; if (!stage_observed(ctx, DECISION_TIMING_STAGE_RESPONSE_TOTAL) || !stage_observed(ctx, DECISION_TIMING_STAGE_SYSLOG_DEBUG_FORMAT)) return false; if (stage_time_share(ctx, DECISION_TIMING_STAGE_SYSLOG_DEBUG_FORMAT, DECISION_TIMING_STAGE_RESPONSE_TOTAL) < RESPONSE_FORMATTING_DOMINANT) return false; format_human_duration( ctx->totals[DECISION_TIMING_STAGE_SYSLOG_DEBUG_FORMAT].total_ns, debug_total, sizeof(debug_total)); format_human_duration( ctx->totals[DECISION_TIMING_STAGE_RESPONSE_TOTAL].total_ns, response_total, sizeof(response_total)); fprintf(ctx->f, " - Manual/debug response formatting accounts for %s " "of %s response time.\n", debug_total, response_total); return true; } /* * write_tldr_queueing - write a compact queueing finding. * @ctx: report context. * Returns true when a finding was written. */ static bool write_tldr_queueing( const struct decision_timing_report_ctx *ctx) { const struct decision_timing_stage_snapshot *queue = &ctx->totals[DECISION_TIMING_STAGE_QUEUE_WAIT]; const char *p95; char max[32]; double fullness = 0.0; if (queue->count == 0) return false; p95 = percentile_bucket(queue, 95); format_human_duration(queue->max_ns, max, sizeof(max)); if (ctx->q_size) fullness = ((double)ctx->max_queue_depth * 100.0) / (double)ctx->q_size; if (ctx->q_size && fullness < 25.0 && percentile_bucket_index(queue, 95) <= 4) fprintf(ctx->f, " - Queueing is healthy; max queue depth %u of %u, " "p95 wait %s.\n", ctx->max_queue_depth, ctx->q_size, p95); else if (ctx->q_size) fprintf(ctx->f, " - Queueing pressure reached max depth %u of %u " "(%.1f%%), p95 wait %s, max wait %s.\n", ctx->max_queue_depth, ctx->q_size, fullness, p95, max); else fprintf(ctx->f, " - Queueing p95 wait %s, max wait %s, " "max queue depth %u.\n", p95, max, ctx->max_queue_depth); return true; } /* * write_tldr - write dominant timing findings near the report top. * @ctx: report context. * Returns nothing. */ static void write_tldr(const struct decision_timing_report_ctx *ctx) { unsigned int findings = 0; fprintf(ctx->f, "\nTL;DR:\n"); if (write_tldr_mime(ctx)) findings++; if (write_tldr_response(ctx)) findings++; if (write_tldr_queueing(ctx)) findings++; if (findings == 0) fprintf(ctx->f, " - No dominant timing findings observed.\n"); } /* * write_derived_observations - write deterministic report observations. * @ctx: report context. * Returns nothing. */ static void write_derived_observations( const struct decision_timing_report_ctx *ctx) { bool any = false; fprintf(ctx->f, "\nDerived observations:\n"); any |= write_queueing_observation(ctx); any |= write_idle_observation(ctx); any |= write_response_observation(ctx); any |= write_mime_observations(ctx); any |= write_hash_observation(ctx, DECISION_TIMING_STAGE_HASH_IMA, "hash_ima"); any |= write_hash_observation(ctx, DECISION_TIMING_STAGE_HASH_SHA, "hash_sha"); any |= write_trust_db_observation(ctx); if (!any) fprintf(ctx->f, " none\n"); } /* * write_stage_table - write observed stages ranked by total time. * @ctx: report context. * Returns nothing. */ static void write_stage_table(const struct decision_timing_report_ctx *ctx) { unsigned int i; fprintf(ctx->f, "\nDetailed stage timing, sorted by total time:\n"); fprintf(ctx->f, "%-*s %10s %14s %10s %10s %10s %12s\n", DECISION_TIMING_STAGE_WIDTH, "Stage", "Calls", "Calls/Dec", "Total", "Avg", "Max", "p95 bucket"); for (i = 0; i < ctx->order->count; i++) { unsigned int stage = ctx->order->stages[i]; char calls[32], total[32], avg[32], max[32]; unsigned long long avg_ns; double calls_per_decision = 0.0; if (ctx->decisions) calls_per_decision = (double)ctx->totals[stage].count / (double)ctx->decisions; avg_ns = stage_avg_ns(&ctx->totals[stage]); format_count(ctx->totals[stage].count, calls, sizeof(calls)); format_human_duration(ctx->totals[stage].total_ns, total, sizeof(total)); format_human_duration(avg_ns, avg, sizeof(avg)); format_human_duration(ctx->totals[stage].max_ns, max, sizeof(max)); fprintf(ctx->f, "%-*s %10s %14.2f %10s %10s %10s %12s\n", DECISION_TIMING_STAGE_WIDTH, stage_names[stage], calls, calls_per_decision, total, avg, max, percentile_bucket(&ctx->totals[stage], 95)); } } /* * tail_stage_parent_prefix - find the parent prefix for a :total stage. * @name: stage name. * @len: destination for parent prefix length. * Returns true if @name is a parent stage. */ static bool tail_stage_parent_prefix(const char *name, size_t *len) { static const char suffix[] = ":total"; size_t name_len = strlen(name); size_t suffix_len = sizeof(suffix) - 1; if (name_len <= suffix_len) return false; if (strcmp(name + name_len - suffix_len, suffix) != 0) return false; *len = name_len - suffix_len; return true; } /* * tail_stage_is_parent - test whether one stage name is a parent of another. * @parent: possible parent stage name. * @child: possible child stage name. * Returns true when @child is under @parent's :total prefix. */ static bool tail_stage_is_parent(const char *parent, const char *child) { size_t len; if (!tail_stage_parent_prefix(parent, &len)) return false; return strncmp(parent, child, len) == 0 && child[len] == ':'; } /* * tail_counts_near - test whether two tail counts are nearly the same. * @a: first count. * @b: second count. * Returns true when the counts differ by no more than five percent. */ static bool tail_counts_near(unsigned long long a, unsigned long long b) { unsigned long long high, low; if (a == 0 || b == 0) return false; high = a > b ? a : b; low = a > b ? b : a; return (high - low) * 100 <= high * 5; } /* * tail_row_duplicate - suppress near-identical parent/child tail rows. * @selected: selected rows. * @selected_count: number of selected rows. * @candidate: row being considered. * Returns true when @candidate would add duplicate parent/child noise. */ static bool tail_row_duplicate(const struct decision_timing_tail_row *selected, unsigned int selected_count, const struct decision_timing_tail_row *candidate) { const char *candidate_name = stage_names[candidate->stage]; unsigned int i; for (i = 0; i < selected_count; i++) { const char *selected_name = stage_names[selected[i].stage]; if (!tail_counts_near(selected[i].over_10ms, candidate->over_10ms)) continue; if (tail_stage_is_parent(selected_name, candidate_name) || tail_stage_is_parent(candidate_name, selected_name)) return true; } return false; } /* * sort_tail_rows - rank stage tail rows by high-end occurrence count. * @ctx: report context. * @rows: rows to sort. * @count: number of rows. * Returns nothing. */ static void sort_tail_rows(const struct decision_timing_report_ctx *ctx, struct decision_timing_tail_row *rows, unsigned int count) { unsigned int i; for (i = 1; i < count; i++) { struct decision_timing_tail_row row = rows[i]; unsigned int j = i; while (j > 0) { const struct decision_timing_tail_row *prev = &rows[j - 1]; bool move = false; if (row.over_10ms > prev->over_10ms) move = true; else if (row.over_10ms == prev->over_10ms && row.over_50ms > prev->over_50ms) move = true; else if (row.over_10ms == prev->over_10ms && row.over_50ms == prev->over_50ms && ctx->totals[row.stage].total_ns > ctx->totals[prev->stage].total_ns) move = true; else if (row.over_10ms == prev->over_10ms && row.over_50ms == prev->over_50ms && ctx->totals[row.stage].total_ns == ctx->totals[prev->stage].total_ns && row.stage < prev->stage) move = true; if (!move) break; rows[j] = rows[j - 1]; j--; } rows[j] = row; } } /* * write_stage_tail_summary - write limited tail summaries for hot stage rows. * @ctx: report context. * Returns nothing. */ static void write_stage_tail_summary( const struct decision_timing_report_ctx *ctx) { struct decision_timing_tail_row rows[DECISION_TIMING_STAGE_COUNT]; struct decision_timing_tail_row selected[DECISION_TIMING_STAGE_COUNT]; unsigned int i, count = 0, selected_count = 0; for (i = 0; i < DECISION_TIMING_STAGE_COUNT; i++) { unsigned long long over_10ms = bucket_count_above(&ctx->totals[i], 8); if (over_10ms == 0) continue; rows[count].stage = i; rows[count].over_10ms = over_10ms; rows[count].over_50ms = bucket_count_above(&ctx->totals[i], 10); count++; } if (count == 0) return; sort_tail_rows(ctx, rows, count); for (i = 0; i < count; i++) { if (selected_count >= DECISION_TIMING_TAIL_STAGE_LIMIT && rows[i].over_50ms == 0) continue; if (tail_row_duplicate(selected, selected_count, &rows[i])) continue; selected[selected_count++] = rows[i]; } if (selected_count == 0) return; fprintf(ctx->f, "\nStage tail summary:\n"); for (i = 0; i < selected_count; i++) { decision_timing_stage_t stage = selected[i].stage; fprintf(ctx->f, " %s: ", stage_names[stage]); write_tail_counts(ctx->f, &ctx->totals[stage], false); } } /* * write_not_observed - list stages that were not observed. * @ctx: report context. * Returns nothing. */ static void write_not_observed(const struct decision_timing_report_ctx *ctx) { unsigned int i; bool any = false; fprintf(ctx->f, "\nNot observed:\n "); for (i = 1; i < DECISION_TIMING_STAGE_COUNT; i++) { if (ctx->totals[i].count) continue; fprintf(ctx->f, "%s%s", any ? ", " : "", stage_names[i]); any = true; } if (!any) fprintf(ctx->f, "none"); fputc('\n', ctx->f); } /* * write_notes - write a short interpretation footer. * @ctx: report context. * Returns nothing. */ static void write_notes(const struct decision_timing_report_ctx *ctx) { static const struct decision_timing_named_stage phases[] = { { DECISION_TIMING_STAGE_EVENT_BUILD, "event_build" }, { DECISION_TIMING_STAGE_RULE_EVALUATION, "evaluation" }, { DECISION_TIMING_STAGE_RESPONSE_TOTAL, "response" }, }; static const struct decision_timing_named_stage daemon_phases[] = { { DECISION_TIMING_STAGE_EVENT_BUILD, "event_build" }, { DECISION_TIMING_STAGE_RULE_EVALUATION, "evaluation" }, }; struct decision_timing_stage_snapshot helper_sample; unsigned int stage, row; char duration[32]; fprintf(ctx->f, "\nNotes:\n"); if (stage_observed(ctx, DECISION_TIMING_STAGE_QUEUE_WAIT)) { format_human_duration( ctx->totals[DECISION_TIMING_STAGE_QUEUE_WAIT].total_ns, duration, sizeof(duration)); fprintf(ctx->f, " Largest queued-time contributor: time_in_queue:total (%s)\n", duration); } if (find_largest_helper(ctx, &row, &helper_sample)) { format_human_duration(helper_sample.total_ns, duration, sizeof(duration)); fprintf(ctx->f, " Largest helper contributor: %s (%s)\n", helper_rows[row].name, duration); } if (find_largest_named_stage(ctx, phases, sizeof(phases) / sizeof(phases[0]), &row)) { stage = phases[row].stage; format_human_duration(ctx->totals[stage].total_ns, duration, sizeof(duration)); if (stage == DECISION_TIMING_STAGE_RESPONSE_TOTAL && stage_time_share(ctx, DECISION_TIMING_STAGE_SYSLOG_DEBUG_FORMAT, DECISION_TIMING_STAGE_RESPONSE_TOTAL) >= RESPONSE_FORMATTING_DOMINANT) { fprintf(ctx->f, " Largest manual/debug phase contributor: " "response (%s, syslog/debug-heavy)\n", duration); if (find_largest_named_stage(ctx, daemon_phases, sizeof(daemon_phases) / sizeof(daemon_phases[0]), &row)) { stage = daemon_phases[row].stage; format_human_duration( ctx->totals[stage].total_ns, duration, sizeof(duration)); fprintf(ctx->f, " Largest daemon-relevant decision phase contributor: %s (%s)\n", daemon_phases[row].name, duration); } } else { fprintf(ctx->f, " Largest decision phase contributor: %s (%s)\n", phases[row].name, duration); } } if (find_slowest_stage(ctx->totals, &stage)) { format_human_duration(ctx->totals[stage].max_ns, duration, sizeof(duration)); fprintf(ctx->f, " Slowest observed row by max: %s (%s)\n", stage_names[stage], duration); } } /* * write_report_sections - write the report detail sections after run summary. * @ctx: report context. * Returns nothing. */ static void write_report_sections(const struct decision_timing_report_ctx *ctx) { write_tldr(ctx); write_overall_latency(ctx->f, &ctx->totals[DECISION_TIMING_STAGE_TOTAL]); write_queueing(ctx); write_worker_skew(ctx); write_phase_timing(ctx); write_helper_attribution_intro(ctx->f); write_helper_by_driver(ctx); write_lazy_helpers(ctx); write_derived_observations(ctx); write_stage_table(ctx); write_stage_tail_summary(ctx); write_not_observed(ctx); write_notes(ctx); // Keep repeated report output separated when generated in a loop. fputs("\n", ctx->f); } #ifdef TEST_DECISION_TIMING_REPORT /* * decision_timing_test_write_report - format a synthetic timing report. * @f: output stream. * @samples: synthetic stage samples. * @sample_count: number of entries in @samples. * @input: synthetic run-level inputs. * Returns nothing. */ void decision_timing_test_write_report(FILE *f, const struct decision_timing_test_stage_sample *samples, unsigned int sample_count, const struct decision_timing_test_report_input *input) { struct decision_timing_stage_snapshot totals[DECISION_TIMING_STAGE_COUNT]; struct decision_timing_stage_order order; struct decision_timing_report_ctx report; unsigned int i; memset(totals, 0, sizeof(totals)); for (i = 0; i < sample_count; i++) { decision_timing_stage_t stage = samples[i].stage; unsigned int bucket = samples[i].bucket; if (stage >= DECISION_TIMING_STAGE_COUNT) continue; if (bucket >= DECISION_TIMING_BUCKETS) bucket = DECISION_TIMING_BUCKETS - 1; totals[stage].count += samples[i].count; totals[stage].total_ns += samples[i].total_ns; if (samples[i].max_ns > totals[stage].max_ns) totals[stage].max_ns = samples[i].max_ns; totals[stage].buckets[bucket] += samples[i].count; } sort_stages_by_total(totals, &order); report.f = f; report.totals = totals; report.workers = NULL; report.order = ℴ report.decisions = totals[DECISION_TIMING_STAGE_TOTAL].count; report.duration_ns = input ? input->duration_ns : 0; report.worker_count = 0; report.max_queue_depth = input ? input->max_queue_depth : 0; report.q_size = input ? input->q_size : 0; write_report_sections(&report); } #endif /* * write_timing_report - snapshot aggregates and write the timing report. * @config: active daemon configuration. * Returns nothing. */ static void write_timing_report(const conf_t *config) { struct decision_timing_stage_snapshot totals[DECISION_TIMING_STAGE_COUNT]; struct decision_timing_worker_snapshot worker_snapshots[DECISION_TIMING_MAX_WORKERS]; struct decision_timing_stage_order order; struct decision_timing_report_ctx report; FILE *f; unsigned int worker, stage, worker_count; char decisions[32], duration[32], start_time[64], stop_time[64]; const char *start_text, *stop_text; const char *mode = decision_timing_mode_name( config_timing_mode(config)); unsigned long long duration_ns = 0; unsigned long long decision_count; unsigned long long start_mono, stop_mono; unsigned int max_queue_depth; long stopped = atomic_load_explicit(&last_stop_time, memory_order_relaxed); long started = atomic_load_explicit(&run_start_time, memory_order_relaxed); int tfd; memset(totals, 0, sizeof(totals)); memset(worker_snapshots, 0, sizeof(worker_snapshots)); worker_count = atomic_load_explicit(&active_workers, memory_order_relaxed); if (worker_count > DECISION_TIMING_MAX_WORKERS) worker_count = DECISION_TIMING_MAX_WORKERS; for (worker = 0; worker < worker_count; worker++) { worker_snapshots[worker].worker_id = worker; for (stage = 0; stage < DECISION_TIMING_STAGE_COUNT; stage++) { snapshot_stage(&worker_snapshots[worker].stages[stage], &workers[worker].stages[stage]); stage_snapshot_add(&totals[stage], &worker_snapshots[worker].stages[stage]); } } sort_stages_by_total(totals, &order); tfd = open_timing_report(); if (tfd < 0) { msg(LOG_WARNING, "cannot open %s: %s", TIMING_REPORT, strerror(errno)); return; } f = fdopen(tfd, "w"); if (!f) { msg(LOG_WARNING, "cannot fdopen %s: %s", TIMING_REPORT, strerror(errno)); close(tfd); return; } start_mono = atomic_load_explicit(&run_start_mono_ns, memory_order_relaxed); stop_mono = atomic_load_explicit(&run_stop_mono_ns, memory_order_relaxed); if (start_mono && stop_mono >= start_mono) duration_ns = stop_mono - start_mono; else if (stopped >= started) duration_ns = (unsigned long long)(stopped - started) * NSEC_PER_SEC; decision_count = totals[DECISION_TIMING_STAGE_TOTAL].count; max_queue_depth = atomic_load_explicit(&run_max_queue_depth, memory_order_relaxed); report.f = f; report.totals = totals; report.workers = worker_snapshots; report.order = ℴ report.decisions = decision_count; report.duration_ns = duration_ns; report.worker_count = worker_count; report.max_queue_depth = max_queue_depth; report.q_size = config->q_size; format_count(decision_count, decisions, sizeof(decisions)); format_hms_duration(duration_ns, duration, sizeof(duration)); start_text = format_report_time(started, start_time, sizeof(start_time)); if (start_text == NULL) start_text = "unavailable"; stop_text = format_report_time(stopped, stop_time, sizeof(stop_time)); if (stop_text == NULL) stop_text = "unavailable"; fprintf(f, "Mode: %s\n", mode); fprintf(f, "Timing run: %s to %s\n", start_text, stop_text); fprintf(f, "Duration: %s\n", duration); fprintf(f, "Workers: %u\n", worker_count); fprintf(f, "Max queue depth: %u\n", max_queue_depth); fprintf(f, "Decisions: %s\n", decisions); if (duration_ns) fprintf(f, "Throughput: %.1f decisions/sec (wall clock)\n", (double)decision_count / ((double)duration_ns / (double)NSEC_PER_SEC)); else fprintf(f, "Throughput: n/a\n"); if (totals[DECISION_TIMING_STAGE_TOTAL].total_ns) fprintf(f, "Active decision rate: %.1f decisions/sec\n", (double)decision_count / ((double)totals[DECISION_TIMING_STAGE_TOTAL].total_ns / (double)NSEC_PER_SEC)); else fprintf(f, "Active decision rate: n/a\n"); if (atomic_load_explicit(&stop_reason, memory_order_relaxed) == DECISION_TIMING_STOP_OVERFLOW) { int overflow_stage = atomic_load_explicit(&stop_reason_stage, memory_order_relaxed); if (overflow_stage >= 0 && overflow_stage < DECISION_TIMING_STAGE_COUNT) fprintf(f, "Stop reason: counter overflow at %s\n", stage_names[overflow_stage]); else fprintf(f, "Stop reason: counter overflow\n"); } write_report_sections(&report); fclose(f); msg(LOG_INFO, "Wrote decision timing report to %s", TIMING_REPORT); } /* * write_unarmed_report - write a report for an unarmed stop request. * @config: active daemon configuration. * Returns nothing. */ static void write_unarmed_report(const conf_t *config) { const char *mode = decision_timing_mode_name( config_timing_mode(config)); FILE *f; int tfd; tfd = open_timing_report(); if (tfd < 0) { msg(LOG_WARNING, "cannot open %s: %s", TIMING_REPORT, strerror(errno)); return; } f = fdopen(tfd, "w"); if (!f) { msg(LOG_WARNING, "cannot fdopen %s: %s", TIMING_REPORT, strerror(errno)); close(tfd); return; } fprintf(f, "Mode: %s\n", mode); fprintf(f, "Status: timing_collection is not armed\n"); // Keep repeated report output separated when generated in a loop. fputs("\n", f); fclose(f); msg(LOG_INFO, "Wrote decision timing report to %s", TIMING_REPORT); } /* * decision_timing_apply_config - apply a timing mode change. * @mode: configured timing mode. * Returns nothing. */ void decision_timing_apply_config(timing_collection_t mode) { if (mode == TIMING_COLLECTION_OFF && atomic_exchange_explicit(&timing_armed, false, memory_order_acq_rel)) { atomic_store_explicit(&queue_depth_restore_requests, true, memory_order_relaxed); msg(LOG_INFO, "Decision timing disarmed because mode is off"); } } /* * decision_timing_set_active_workers - publish active decision worker count. * @worker_count: number of workers that can update timing blocks. * * Returns nothing. */ void decision_timing_set_active_workers(unsigned int worker_count) { if (worker_count > DECISION_TIMING_MAX_WORKERS) worker_count = DECISION_TIMING_MAX_WORKERS; atomic_store_explicit(&active_workers, worker_count, memory_order_relaxed); } /* * decision_timing_set_queue_depth_hooks - install queue depth callbacks. * @reset: callback that resets max queue depth and returns the saved value. * @restore: callback that returns run max depth and restores saved if larger. * @ctx: callback context. * Returns nothing. */ void decision_timing_set_queue_depth_hooks( decision_timing_queue_depth_reset_fn reset, decision_timing_queue_depth_restore_fn restore, void *ctx) { queue_depth_reset = reset; queue_depth_restore = restore; queue_depth_ctx = ctx; } /* * decision_timing_signal_request - record SIGUSR1 timing intent. * @intent: SIGUSR1 intent value. * @pid: sender pid, or -1 when unavailable. * @uid: sender uid, or -1 when unavailable. * Returns nothing. */ void decision_timing_signal_request(report_intent_t intent, pid_t pid, uid_t uid) { if (intent == REPORT_INTENT_TIMING_ARM) { atomic_store_explicit(&arm_request_pid, (int)pid, memory_order_relaxed); atomic_store_explicit(&arm_request_uid, (int)uid, memory_order_relaxed); atomic_fetch_add_explicit(&arm_requests, 1, memory_order_relaxed); } else if (intent == REPORT_INTENT_TIMING_STOP) { atomic_store_explicit(&stop_request_pid, (int)pid, memory_order_relaxed); atomic_store_explicit(&stop_request_uid, (int)uid, memory_order_relaxed); atomic_fetch_add_explicit(&stop_requests, 1, memory_order_relaxed); } } /* * decision_timing_queue_depth_start - save and reset run queue depth. * Returns nothing. */ static void decision_timing_queue_depth_start(void) { unsigned int saved = 0; if (queue_depth_reset) saved = queue_depth_reset(queue_depth_ctx); if (atomic_load_explicit(&queue_depth_active, memory_order_relaxed)) { unsigned int previous = atomic_load_explicit( &saved_max_queue_depth, memory_order_relaxed); if (previous > saved) saved = previous; } atomic_store_explicit(&saved_max_queue_depth, saved, memory_order_relaxed); atomic_store_explicit(&run_max_queue_depth, 0, memory_order_relaxed); atomic_store_explicit(&queue_depth_active, true, memory_order_relaxed); } /* * decision_timing_queue_depth_stop - snapshot run queue depth and restore. * Returns nothing. */ static void decision_timing_queue_depth_stop(void) { unsigned int current = 0; unsigned int saved; if (!atomic_exchange_explicit(&queue_depth_active, false, memory_order_relaxed)) return; saved = atomic_load_explicit(&saved_max_queue_depth, memory_order_relaxed); if (queue_depth_restore) current = queue_depth_restore(queue_depth_ctx, saved); atomic_store_explicit(&run_max_queue_depth, current, memory_order_relaxed); } /* * decision_timing_arm - start a manual timing run. * @pid: requester pid. * @uid: requester uid. * Returns nothing. */ static void decision_timing_arm(int pid, int uid) { if (atomic_load_explicit(&timing_armed, memory_order_acquire)) { msg(LOG_INFO, "Decision timing start requested by pid=%d uid=%d " "but timing is already armed", pid, uid); return; } reset_worker_blocks(); decision_timing_queue_depth_start(); atomic_store_explicit(&last_arm_time, (long)time(NULL), memory_order_relaxed); atomic_store_explicit(&run_start_time, atomic_load_explicit(&last_arm_time, memory_order_relaxed), memory_order_relaxed); atomic_store_explicit(&run_start_mono_ns, ns_now(), memory_order_relaxed); atomic_store_explicit(&run_stop_mono_ns, 0, memory_order_relaxed); atomic_store_explicit(&stop_reason, DECISION_TIMING_STOP_MANUAL, memory_order_relaxed); atomic_store_explicit(&stop_reason_stage, -1, memory_order_relaxed); atomic_store_explicit(&overflow_stop_requests, 0, memory_order_relaxed); atomic_store_explicit(&timing_armed, true, memory_order_release); msg(LOG_INFO, "Decision timing started by pid=%d uid=%d", pid, uid); } /* * decision_timing_stop - stop a manual timing run and write a report. * @config: active daemon configuration. * @pid: requester pid. * @uid: requester uid. * Returns nothing. */ static void decision_timing_stop(const conf_t *config, int pid, int uid) { bool was_armed; was_armed = atomic_exchange_explicit(&timing_armed, false, memory_order_acq_rel); if (!was_armed) { msg(LOG_INFO, "Decision timing stop requested by pid=%d uid=%d but timing is not armed", pid, uid); write_unarmed_report(config); return; } atomic_store_explicit(&last_stop_time, (long)time(NULL), memory_order_relaxed); atomic_store_explicit(&run_stop_mono_ns, ns_now(), memory_order_relaxed); atomic_store_explicit(&stop_reason, DECISION_TIMING_STOP_MANUAL, memory_order_relaxed); atomic_store_explicit(&stop_reason_stage, -1, memory_order_relaxed); decision_timing_queue_depth_stop(); msg(LOG_INFO, "Decision timing stopped by pid=%d uid=%d", pid, uid); write_timing_report(config); } /* * decision_timing_process_requests - apply pending timing control requests. * @config: active daemon configuration. * * Signal handlers and overflow detection do not mutate timing state directly. * They update the static atomic request flags above: arm_requests, * stop_requests, overflow_stop_requests, and queue_depth_restore_requests. * The decision thread calls this function from normal process context to drain * those flags, start or stop manual timing, restore queue-depth accounting, and * write reports when a timing run ends. * * Returns nothing. */ void decision_timing_process_requests(const conf_t *config) { unsigned int arms, stops; int pid, uid; if (atomic_exchange_explicit(&queue_depth_restore_requests, false, memory_order_relaxed)) decision_timing_queue_depth_stop(); if (atomic_exchange_explicit(&overflow_stop_requests, 0, memory_order_relaxed)) { decision_timing_queue_depth_stop(); write_timing_report(config); } arms = atomic_exchange_explicit(&arm_requests, 0, memory_order_relaxed); if (arms) { pid = atomic_load_explicit(&arm_request_pid, memory_order_relaxed); uid = atomic_load_explicit(&arm_request_uid, memory_order_relaxed); if (config_timing_mode(config) != TIMING_COLLECTION_MANUAL) { msg(LOG_INFO, "Decision timing start ignored because timing_collection is not manual"); } else if (uid != 0) { msg(LOG_INFO, "Decision timing start ignored because uid=%d is not privileged", uid); } else decision_timing_arm(pid, uid); } stops = atomic_exchange_explicit(&stop_requests, 0, memory_order_relaxed); if (stops) { pid = atomic_load_explicit(&stop_request_pid, memory_order_relaxed); uid = atomic_load_explicit(&stop_request_uid, memory_order_relaxed); if (config_timing_mode(config) != TIMING_COLLECTION_MANUAL) { msg(LOG_INFO, "Decision timing stop ignored because timing_collection is not manual"); } else if (uid != 0) { msg(LOG_INFO, "Decision timing stop ignored because uid=%d is not privileged", uid); } else decision_timing_stop(config, pid, uid); } } /* * decision_timing_control_report - write timing control state to state report. * @f: output stream. * @config: active daemon configuration. * Returns nothing. */ void decision_timing_control_report(FILE *f, const conf_t *config) { const char *mode; if (f == NULL || config == NULL) return; mode = decision_timing_mode_name(config_timing_mode(config)); fprintf(f, "Timing collection mode: %s\n", mode); fprintf(f, "Timing collection armed: %s\n", atomic_load_explicit(&timing_armed, memory_order_relaxed) ? "true" : "false"); } /* * decision_timing_history_report - write timing history to state report. * @f: output stream. * Returns nothing. */ void decision_timing_history_report(FILE *f) { const char *arm_text, *stop_text; char arm_time[64], stop_time[64]; if (f == NULL) return; arm_text = format_report_time(atomic_load_explicit(&last_arm_time, memory_order_relaxed), arm_time, sizeof(arm_time)); if (arm_text == NULL) arm_text = "unavailable"; stop_text = format_report_time(atomic_load_explicit(&last_stop_time, memory_order_relaxed), stop_time, sizeof(stop_time)); if (stop_text == NULL) stop_text = "unavailable"; fprintf(f, "Timing collection last start time: %s\n", arm_text); fprintf(f, "Timing collection last stop time: %s\n", stop_text); } /* * decision_timing_decision_begin - begin timing one dequeued event. * @worker_id: decision worker that owns the event. * Returns nothing. */ void decision_timing_decision_begin(unsigned int worker_id) { bool armed; decision_timing_tls.armed = false; decision_timing_tls.worker_id = worker_id; decision_timing_tls.driver = DECISION_TIMING_DRIVER_COUNT; decision_timing_tls.total_start_ns = 0; if (worker_id >= DECISION_TIMING_MAX_WORKERS) return; armed = atomic_load_explicit(&timing_armed, memory_order_acquire); if (DECISION_TIMING_UNLIKELY(armed)) { decision_timing_tls.total_start_ns = ns_now(); if (decision_timing_tls.total_start_ns == 0) return; decision_timing_tls.armed = true; } } /* * decision_timing_decision_end - finish timing one dequeued event. * Returns nothing. */ void decision_timing_decision_end(void) { uint64_t end; if (DECISION_TIMING_UNLIKELY(decision_timing_tls.armed)) { end = ns_now(); if (end >= decision_timing_tls.total_start_ns) record_stage(DECISION_TIMING_STAGE_TOTAL, end - decision_timing_tls.total_start_ns); decision_timing_tls.armed = false; } } /* * decision_timing_queue_enqueue_time - capture an enqueue timestamp if armed. * Returns monotonic nanoseconds for queue timing, or zero when unarmed. * * This is called by the event producer, before the decision worker has copied * the armed state into thread-local storage. Keep it limited to one armed-flag * load and only call clock_gettime() while a manual timing run is active. */ uint64_t decision_timing_queue_enqueue_time(void) { if (DECISION_TIMING_UNLIKELY(atomic_load_explicit(&timing_armed, memory_order_acquire))) return ns_now(); return 0; } /* * decision_timing_queue_dequeued - record time spent in the userspace queue. * @enqueue_ns: timestamp captured when the event was queued. * Returns nothing. */ void decision_timing_queue_dequeued(uint64_t enqueue_ns) { uint64_t dequeue_ns; if (DECISION_TIMING_UNLIKELY(decision_timing_tls.armed && enqueue_ns != 0)) { dequeue_ns = decision_timing_tls.total_start_ns; if (dequeue_ns >= enqueue_ns) record_stage(DECISION_TIMING_STAGE_QUEUE_WAIT, dequeue_ns - enqueue_ns); } } /* * report_missing_helper_driver - report helper timing outside a driver once. * @helper: helper type being timed. * Returns nothing. */ static void report_missing_helper_driver(const char *helper) { bool expected = false; if (!atomic_compare_exchange_strong_explicit( &missing_helper_driver_logged, &expected, true, memory_order_relaxed, memory_order_relaxed)) return; msg(LOG_WARNING, "Decision timing %s helper called outside evaluation/response", helper); } /* * mime_stage_for_driver - map MIME helper substage to active driver stage. * @stage: MIME helper substage. * Returns a concrete timing stage, or DECISION_TIMING_STAGE_COUNT on error. */ static decision_timing_stage_t mime_stage_for_driver( decision_timing_mime_stage_t stage) { static const decision_timing_stage_t map[][2] = { { DECISION_TIMING_STAGE_EVAL_MIME_DETECTION, DECISION_TIMING_STAGE_RESPONSE_MIME_DETECTION }, { DECISION_TIMING_STAGE_EVAL_MIME_FAST_CLASSIFICATION, DECISION_TIMING_STAGE_RESPONSE_MIME_FAST_CLASSIFICATION }, { DECISION_TIMING_STAGE_EVAL_MIME_GATHER_ELF, DECISION_TIMING_STAGE_RESPONSE_MIME_GATHER_ELF }, { DECISION_TIMING_STAGE_EVAL_MIME_LIBMAGIC_FALLBACK, DECISION_TIMING_STAGE_RESPONSE_MIME_LIBMAGIC_FALLBACK } }; decision_timing_driver_t driver = decision_timing_tls.driver; if (stage > DECISION_TIMING_MIME_LIBMAGIC_FALLBACK) stage = DECISION_TIMING_MIME_TOTAL; if (driver >= DECISION_TIMING_DRIVER_COUNT) { report_missing_helper_driver("MIME"); return DECISION_TIMING_STAGE_COUNT; } return map[stage][driver]; } /* * trust_db_stage_for_driver - map trust DB substage to active driver stage. * @stage: trust DB helper substage. * Returns a concrete timing stage, or DECISION_TIMING_STAGE_COUNT on error. */ static decision_timing_stage_t trust_db_stage_for_driver( decision_timing_trust_db_stage_t stage) { static const decision_timing_stage_t map[][2] = { { DECISION_TIMING_STAGE_EVAL_TRUST_DB_LOOKUP, DECISION_TIMING_STAGE_RESPONSE_TRUST_DB_LOOKUP }, { DECISION_TIMING_STAGE_EVAL_TRUST_DB_LOCK_WAIT, DECISION_TIMING_STAGE_RESPONSE_TRUST_DB_LOCK_WAIT }, { DECISION_TIMING_STAGE_EVAL_TRUST_DB_READ, DECISION_TIMING_STAGE_RESPONSE_TRUST_DB_READ } }; decision_timing_driver_t driver = decision_timing_tls.driver; if (stage > DECISION_TIMING_TRUST_DB_READ) stage = DECISION_TIMING_TRUST_DB_TOTAL; if (driver >= DECISION_TIMING_DRIVER_COUNT) { report_missing_helper_driver("trust DB"); return DECISION_TIMING_STAGE_COUNT; } return map[stage][driver]; } /* * decision_timing_mime_stage_begin_slow - time a driver-specific MIME stage. * @stage: MIME helper substage. * @span: caller-owned span storage. * Returns nothing. */ void decision_timing_mime_stage_begin_slow(decision_timing_mime_stage_t stage, struct decision_timing_span *span) { decision_timing_stage_begin_slow(mime_stage_for_driver(stage), span); } /* * decision_timing_trust_db_stage_begin_slow - time driver-specific trust DB. * @stage: trust DB helper substage. * @span: caller-owned span storage. * Returns nothing. */ void decision_timing_trust_db_stage_begin_slow( decision_timing_trust_db_stage_t stage, struct decision_timing_span *span) { decision_timing_stage_begin_slow(trust_db_stage_for_driver(stage), span); } /* * decision_timing_stage_begin_slow - record a timed stage start. * @stage: stage being measured. * @span: caller-owned span storage. * Returns nothing. */ void decision_timing_stage_begin_slow(decision_timing_stage_t stage, struct decision_timing_span *span) { if (stage >= DECISION_TIMING_STAGE_COUNT) return; span->stage = stage; span->start_ns = ns_now(); if (span->start_ns == 0) return; span->active = true; } /* * decision_timing_stage_end_slow - record a timed stage result. * @span: span previously started. * Returns nothing. */ void decision_timing_stage_end_slow(struct decision_timing_span *span) { uint64_t end; end = ns_now(); if (end >= span->start_ns) record_stage(span->stage, end - span->start_ns); span->active = false; } fapolicyd-2.0/src/library/decision-timing.h000066400000000000000000000167561524071644000210350ustar00rootroot00000000000000/* * decision-timing.h - bounded decision timing diagnostics * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef DECISION_TIMING_HEADER #define DECISION_TIMING_HEADER #include #include #include #include #include "conf.h" #if defined(__GNUC__) || defined(__clang__) #define DECISION_TIMING_UNLIKELY(x) __builtin_expect(!!(x), 0) #else #define DECISION_TIMING_UNLIKELY(x) (x) #endif /* * Timing stage names are emitted as phase:operation[:child]. Lazy helpers * that can be driven by multiple code paths get separate phase-specific rows * so the report can distinguish rule evaluation from response logging/audit. */ typedef enum { DECISION_TIMING_STAGE_TOTAL, DECISION_TIMING_STAGE_QUEUE_WAIT, DECISION_TIMING_STAGE_EVENT_BUILD, DECISION_TIMING_STAGE_CACHE_FLUSH, DECISION_TIMING_STAGE_PROC_FINGERPRINT, DECISION_TIMING_STAGE_PROC_STATUS_EXE_LOOKUP, DECISION_TIMING_STAGE_FD_STAT, DECISION_TIMING_STAGE_FD_PATH_RESOLUTION, DECISION_TIMING_STAGE_EVAL_MIME_DETECTION, DECISION_TIMING_STAGE_EVAL_MIME_FAST_CLASSIFICATION, DECISION_TIMING_STAGE_EVAL_MIME_GATHER_ELF, DECISION_TIMING_STAGE_EVAL_MIME_LIBMAGIC_FALLBACK, DECISION_TIMING_STAGE_RESPONSE_MIME_DETECTION, DECISION_TIMING_STAGE_RESPONSE_MIME_FAST_CLASSIFICATION, DECISION_TIMING_STAGE_RESPONSE_MIME_GATHER_ELF, DECISION_TIMING_STAGE_RESPONSE_MIME_LIBMAGIC_FALLBACK, DECISION_TIMING_STAGE_HASH_IMA, DECISION_TIMING_STAGE_HASH_SHA, DECISION_TIMING_STAGE_EVAL_TRUST_DB_LOOKUP, DECISION_TIMING_STAGE_EVAL_TRUST_DB_LOCK_WAIT, DECISION_TIMING_STAGE_EVAL_TRUST_DB_READ, DECISION_TIMING_STAGE_RESPONSE_TRUST_DB_LOOKUP, DECISION_TIMING_STAGE_RESPONSE_TRUST_DB_LOCK_WAIT, DECISION_TIMING_STAGE_RESPONSE_TRUST_DB_READ, DECISION_TIMING_STAGE_RULE_LOCK_WAIT, DECISION_TIMING_STAGE_RULE_EVALUATION, DECISION_TIMING_STAGE_RESPONSE_TOTAL, DECISION_TIMING_STAGE_SYSLOG_DEBUG_FORMAT, DECISION_TIMING_STAGE_AUDIT_RESPONSE_PREP, DECISION_TIMING_STAGE_FANOTIFY_RESPONSE_WRITE, DECISION_TIMING_STAGE_COUNT } decision_timing_stage_t; typedef enum { DECISION_TIMING_DRIVER_EVALUATION, DECISION_TIMING_DRIVER_RESPONSE, DECISION_TIMING_DRIVER_COUNT } decision_timing_driver_t; typedef enum { DECISION_TIMING_MIME_TOTAL, DECISION_TIMING_MIME_FAST_CLASSIFICATION, DECISION_TIMING_MIME_GATHER_ELF, DECISION_TIMING_MIME_LIBMAGIC_FALLBACK } decision_timing_mime_stage_t; typedef enum { DECISION_TIMING_TRUST_DB_TOTAL, DECISION_TIMING_TRUST_DB_LOCK_WAIT, DECISION_TIMING_TRUST_DB_READ } decision_timing_trust_db_stage_t; struct decision_timing_context { bool armed; unsigned int worker_id; decision_timing_driver_t driver; uint64_t total_start_ns; }; struct decision_timing_span { bool active; decision_timing_stage_t stage; uint64_t start_ns; }; typedef unsigned int (*decision_timing_queue_depth_reset_fn)(void *ctx); typedef unsigned int (*decision_timing_queue_depth_restore_fn)(void *ctx, unsigned int saved); extern __thread struct decision_timing_context decision_timing_tls; void decision_timing_apply_config(timing_collection_t mode); void decision_timing_set_active_workers(unsigned int worker_count); void decision_timing_set_queue_depth_hooks( decision_timing_queue_depth_reset_fn reset, decision_timing_queue_depth_restore_fn restore, void *ctx); void decision_timing_signal_request(report_intent_t intent, pid_t pid, uid_t uid); void decision_timing_process_requests(const conf_t *config); void decision_timing_control_report(FILE *f, const conf_t *config); void decision_timing_history_report(FILE *f); void decision_timing_decision_begin(unsigned int worker_id); void decision_timing_decision_end(void); uint64_t decision_timing_queue_enqueue_time(void); void decision_timing_queue_dequeued(uint64_t enqueue_ns); void decision_timing_stage_begin_slow(decision_timing_stage_t stage, struct decision_timing_span *span); void decision_timing_stage_end_slow(struct decision_timing_span *span); void decision_timing_mime_stage_begin_slow(decision_timing_mime_stage_t stage, struct decision_timing_span *span); void decision_timing_trust_db_stage_begin_slow( decision_timing_trust_db_stage_t stage, struct decision_timing_span *span); #ifdef TEST_DECISION_TIMING_REPORT struct decision_timing_test_stage_sample { decision_timing_stage_t stage; unsigned long long count; unsigned long long total_ns; unsigned long long max_ns; unsigned int bucket; }; struct decision_timing_test_report_input { unsigned long long duration_ns; unsigned int max_queue_depth; unsigned int q_size; }; void decision_timing_test_write_report(FILE *f, const struct decision_timing_test_stage_sample *samples, unsigned int sample_count, const struct decision_timing_test_report_input *input); #endif /* * decision_timing_stage_begin - start timing a stage for this event. * @stage: stage being measured. * @span: caller-owned span storage. * Returns nothing. * * The armed flag is copied to thread-local state once per dequeued event. * When the event is unarmed this inline fast path does not call clock_gettime, * update histograms, or touch timing counters. */ static inline void decision_timing_stage_begin(decision_timing_stage_t stage, struct decision_timing_span *span) { span->active = false; if (DECISION_TIMING_UNLIKELY(decision_timing_tls.armed)) decision_timing_stage_begin_slow(stage, span); } /* * decision_timing_mime_stage_begin - start timing a MIME helper stage. * @stage: MIME helper substage. * @span: caller-owned span storage. * Returns nothing. */ static inline void decision_timing_mime_stage_begin( decision_timing_mime_stage_t stage, struct decision_timing_span *span) { span->active = false; if (DECISION_TIMING_UNLIKELY(decision_timing_tls.armed)) decision_timing_mime_stage_begin_slow(stage, span); } /* * decision_timing_trust_db_stage_begin - start timing a trust DB helper stage. * @stage: trust DB helper substage. * @span: caller-owned span storage. * Returns nothing. */ static inline void decision_timing_trust_db_stage_begin( decision_timing_trust_db_stage_t stage, struct decision_timing_span *span) { span->active = false; if (DECISION_TIMING_UNLIKELY(decision_timing_tls.armed)) decision_timing_trust_db_stage_begin_slow(stage, span); } /* * decision_timing_stage_end - finish timing a stage for this event. * @span: span previously passed to decision_timing_stage_begin(). * Returns nothing. */ static inline void decision_timing_stage_end(struct decision_timing_span *span) { if (DECISION_TIMING_UNLIKELY(span->active)) decision_timing_stage_end_slow(span); } /* * decision_timing_driver_push - set the current logical timing driver. * @driver: driver to use for nested lazy helper measurements. * Returns the previous driver. */ static inline decision_timing_driver_t decision_timing_driver_push( decision_timing_driver_t driver) { decision_timing_driver_t previous = decision_timing_tls.driver; if (DECISION_TIMING_UNLIKELY(decision_timing_tls.armed)) decision_timing_tls.driver = driver; return previous; } /* * decision_timing_driver_pop - restore the previous timing driver. * @driver: driver returned by decision_timing_driver_push(). * Returns nothing. */ static inline void decision_timing_driver_pop( decision_timing_driver_t driver) { if (DECISION_TIMING_UNLIKELY(decision_timing_tls.armed)) decision_timing_tls.driver = driver; } #endif fapolicyd-2.0/src/library/escape.c000066400000000000000000000154721524071644000172000ustar00rootroot00000000000000/* * escape.c - Source file for escaping capability * Copyright (c) 2021,23 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka */ #include "config.h" #include "escape.h" #include #include #include #include #include #include "message.h" static const char sh_set[] = "\"'`$\\!()| "; /* * escape_shell_width - return the encoded width of one input byte. * @c: byte to classify. * * Returns four for control bytes, two for shell metacharacters, and one for * bytes copied unchanged. */ static size_t escape_shell_width(unsigned char c) { if (c < 32) return 4; if (strchr(sh_set, c)) return 2; return 1; } /* * escape_shell_size - calculate the encoded string length. * @input: NUL-terminated string to inspect. * @needs_escape: receives whether any byte needs shell escaping. * * Returns the encoded length, or SIZE_MAX if its calculation overflows. */ static size_t escape_shell_size(const char *input, int *needs_escape) __nonnull ((1, 2)) __attr_access ((__read_only__, 1)) __attr_access ((__write_only__, 2)); static size_t escape_shell_size(const char *input, int *needs_escape) { size_t input_pos = 0; size_t output_size = 0; *needs_escape = 0; while (input[input_pos]) { size_t width = escape_shell_width( (unsigned char)input[input_pos]); if (width > 1) *needs_escape = 1; if (output_size > SIZE_MAX - width) return SIZE_MAX; output_size += width; input_pos++; } return output_size; } /* * this function checks whether escaping is needed and if yes * it returns positive value and this value represents the size * of the string after escaping */ size_t check_escape_shell(const char *input) { int needs_escape; size_t output_size; if (!input) return 0; output_size = escape_shell_size(input, &needs_escape); if (!needs_escape) return 0; return output_size; } #define MAX_SIZE 8192 /* * escape_shell_into - shell-escape a bounded string into a bounded buffer. * @input: bytes to encode. * @input_size: number of readable bytes in @input. * @output: destination for the encoded NUL-terminated string. * @output_capacity: writable size of @output, including its terminator. * * Returns the encoded length, or SIZE_MAX if the destination is too small. */ static size_t escape_shell_into(const char *input, size_t input_size, char *output, size_t output_capacity) __nonnull ((1, 3)) __attr_access ((__read_only__, 1, 2)) __attr_access ((__write_only__, 3, 4)); static size_t escape_shell_into(const char *input, size_t input_size, char *output, size_t output_capacity) { size_t input_pos, output_pos = 0; if (output_capacity == 0) return SIZE_MAX; for (input_pos = 0; input_pos < input_size; input_pos++) { unsigned char c = (unsigned char)input[input_pos]; size_t width = escape_shell_width(c); /* Keep one byte available for the terminating NUL. */ if (width >= output_capacity - output_pos) return SIZE_MAX; if (width == 4) { output[output_pos++] = '\\'; output[output_pos++] = '0' + ((c & 0300) >> 6); output[output_pos++] = '0' + ((c & 0070) >> 3); output[output_pos++] = '0' + (c & 0007); } else if (width == 2) { output[output_pos++] = '\\'; output[output_pos++] = c; } else { output[output_pos++] = c; } } output[output_pos] = '\0'; return output_pos; } char *escape_shell(const char *input, size_t expected_size) { char *escape_buffer; size_t input_size, output_size; if (!input) return NULL; if (expected_size >= MAX_SIZE) return NULL; escape_buffer = malloc(expected_size + 1); if (escape_buffer == NULL) return NULL; input_size = strlen(input); output_size = escape_shell_into(input, input_size, escape_buffer, expected_size + 1); if (output_size != expected_size) { free(escape_buffer); return NULL; } return escape_buffer; } #define isoctal(a) (((a) & ~7) == '0') /* * unescape_shell - decode shell and octal escapes in place. * @s: writable NUL-terminated string. * @capacity: writable size of @s, including the terminating NUL. * * Returns 0 on success and -1 if @capacity is zero or no NUL is found within * the buffer. Incomplete octal escapes are copied unchanged. */ int unescape_shell(char *s, size_t capacity) { const char *end; size_t len; size_t sz = 0; char *buf = s; if (capacity == 0) return -1; end = memchr(s, '\0', capacity); if (end == NULL) return -1; len = (size_t)(end - s); while (sz < len) { size_t remaining = len - sz; if (*s == '\\' && remaining >= 4 && isoctal(s[1]) && isoctal(s[2]) && isoctal(s[3])) { *buf++ = 64*(s[1] & 7) + 8*(s[2] & 7) + (s[3] & 7); s += 4; sz += 4; } else if (*s == '\\' && remaining >= 2 && !isoctal(s[1])) { *buf++ = s[1]; s += 2; sz += 2; } else { *buf++ = *s++; sz++; } } *buf = '\0'; return 0; } #define IS_HEX(X) (isxdigit((unsigned char)(X)) > 0 && \ !(islower((unsigned char)(X)) > 0)) static char asciiHex2Bits(char X) { char base = 0; if (X >= '0' && X <= '9') { base = '0'; } else if (X >= 'A' && X <= 'F') { base = 'A' - 10; } return (X - base) & 0X00FF; } // unescape old format of a trust file // it makes code backwards compatible char *unescape(const char *input) { size_t input_len = strlen(input); size_t out_len = 0; for (size_t i = 0; i < input_len; i++) { if (input[i] == '%' && i + 2 < input_len && IS_HEX(input[i + 1]) && IS_HEX(input[i + 2])) { out_len++; i += 2; } else { out_len++; } } if (out_len > 4096) return NULL; //for backward compatibility char *buffer = malloc(out_len + 1); if (!buffer) return NULL; size_t pos = 0; for (size_t i = 0; i < input_len; i++) { if (input[i] == '%' && i + 2 < input_len && IS_HEX(input[i + 1]) && IS_HEX(input[i + 2])) { char c = asciiHex2Bits(input[i + 1]); char d = asciiHex2Bits(input[i + 2]); buffer[pos++] = (c << 4) + d; i += 2; } else { if (input[i] == '%') msg(LOG_WARNING, "Input %s does not have a valid escape sequence, " "unable to unescape, copying char by char", input); buffer[pos++] = input[i]; } } buffer[pos] = '\0'; return buffer; } fapolicyd-2.0/src/library/escape.h000066400000000000000000000031461524071644000172000ustar00rootroot00000000000000/* * escape.h - Header file for escaping capability * Copyright (c) 2021,23 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka */ #ifndef ESCAPE_H #define ESCAPE_H #include "gcc-attributes.h" /* @expected_size must be the exact encoded length, excluding its NUL. */ char *escape_shell(const char *input, size_t expected_size) __attribute_malloc__ __attr_dealloc_free __wur __attr_access ((__read_only__, 1)); /* Return the encoded length when escaping is needed, otherwise zero. */ size_t check_escape_shell(const char *input) __wur __attr_access ((__read_only__, 1)); /* @capacity includes the required NUL terminator. */ int unescape_shell(char *s, size_t capacity) __nonnull ((1)) __wur __attr_access ((__read_write__, 1, 2)); char *unescape(const char *input) __attribute_malloc__ __attr_dealloc_free __nonnull ((1)) __wur __attr_access ((__read_only__, 1)); #endif fapolicyd-2.0/src/library/event.c000066400000000000000000001531411524071644000170550ustar00rootroot00000000000000/* * event.c - Functions to access event attributes * Copyright (c) 2016,2018-20,2023 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * Radovan Sroka */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include "attr-lookup-metrics.h" #include "daemon-config.h" #include "decision-context.h" #include "event.h" #include "database.h" #include "decision-timing.h" #include "file.h" #include "lru.h" #include "message.h" #include "policy.h" #include "rules.h" #include "process.h" #define ALL_EVENTS (FAN_ALL_EVENTS|FAN_OPEN_PERM|FAN_ACCESS_PERM| \ FAN_OPEN_EXEC_PERM) /* * A normal exec/open pattern should complete quickly. Keep this below the * daemon deadman window so one stuck BUILDING slot cannot park permission * events indefinitely. The stale timeout is 10 seconds, expressed in * milliseconds so the intended window is easy to read. */ #define SUBJECT_BUILDING_STALE_MS 10000ULL #define NSEC_PER_MSEC 1000000ULL #define SUBJECT_BUILDING_STALE_NS \ (SUBJECT_BUILDING_STALE_MS * NSEC_PER_MSEC) #define SUBJECT_BUILDING_LOG_INTERVAL 60 static struct decision_context default_decision_context = { .building_tracer_rate_limit = MESSAGE_RATE_LIMIT_INIT(SUBJECT_BUILDING_LOG_INTERVAL), .building_stale_rate_limit = MESSAGE_RATE_LIMIT_INIT(SUBJECT_BUILDING_LOG_INTERVAL), }; static __thread struct decision_context *current_decision_context; static struct decision_context *report_contexts; static inline struct decision_context *active_decision_context(void) { if (current_decision_context == NULL) return &default_decision_context; return current_decision_context; } #define subj_cache (active_decision_context()->subject_cache) #define obj_cache (active_decision_context()->object_cache) #define obj_cache_warned (active_decision_context()->object_cache_warned) #define early_subj_cache_evictions \ (active_decision_context()->early_subject_cache_evictions) #define building_tracer_evictions \ (active_decision_context()->building_tracer_evict_count) #define building_stale_evictions \ (active_decision_context()->building_stale_evict_count) #define building_tracer_log \ (active_decision_context()->building_tracer_rate_limit) #define building_stale_log \ (active_decision_context()->building_stale_rate_limit) static atomic_ulong object_cache_flush_generation; enum building_evict_reason { BUILDING_EVICT_TRACER, BUILDING_EVICT_STALE, }; struct building_evict_context { pid_t pid; unsigned int slot; state_t state; const char *path1; uint64_t age_ns; unsigned int event_count; }; /* * decision_context_current - return active mutable decision state. * * A default empty context is available before the daemon initializes the real * caches so policy-only tests can still use the logging buffer. */ struct decision_context *decision_context_current(void) { return active_decision_context(); } /* * decision_context_set_current - publish the active decision context. * @ctx: context to use, or NULL to restore the default empty context. * Returns nothing. */ void decision_context_set_current(struct decision_context *ctx) { current_decision_context = ctx; } /* * decision_context_close_file_helpers - release context-owned file state. * @ctx: context whose libmagic, udev, and device cache state should be freed. * Returns nothing. */ void decision_context_close_file_helpers(struct decision_context *ctx) { if (ctx == NULL) return; if (ctx->file_udev) { udev_unref(ctx->file_udev); ctx->file_udev = NULL; } if (ctx->magic_fast) { magic_close(ctx->magic_fast); ctx->magic_fast = NULL; } if (ctx->magic_full) { magic_close(ctx->magic_full); ctx->magic_full = NULL; } free(ctx->device_cache.devname); ctx->device_cache.devname = NULL; ctx->device_cache.device = 0; } /* * decision_context_set_worker_id - label a context for per-worker reports. * @ctx: worker-owned decision context to label. * @worker_id: decision worker index that owns @ctx. * Returns nothing. */ void decision_context_set_worker_id(struct decision_context *ctx, unsigned int worker_id) { if (ctx) ctx->worker_id = worker_id; } /* * decision_context_register - expose a worker context to report aggregation. * @ctx: context whose private counters should be included in reports. * Returns nothing. */ static void decision_context_register(struct decision_context *ctx) { if (ctx == NULL || ctx == &default_decision_context || ctx->report_registered) return; ctx->report_next = report_contexts; ctx->report_registered = true; report_contexts = ctx; } /* * decision_context_unregister - remove a context from report aggregation. * @ctx: context being destroyed. * Returns nothing. */ static void decision_context_unregister(struct decision_context *ctx) { struct decision_context **cur = &report_contexts; if (ctx == NULL || !ctx->report_registered) return; while (*cur) { if (*cur == ctx) { *cur = ctx->report_next; ctx->report_next = NULL; ctx->report_registered = false; return; } cur = &(*cur)->report_next; } ctx->report_next = NULL; ctx->report_registered = false; } /* * decision_context_for_each - visit contexts that own decision state. * @iter: callback invoked for each registered context. * @data: caller-private callback data. * * Policy-only tests can run without init_event_system(). In that case the * default thread context is visited so metrics recorded through * decision_context_current() remain visible to reports. */ void decision_context_for_each(decision_context_iter_fn iter, void *data) { struct decision_context *ctx; if (iter == NULL) return; if (report_contexts == NULL) { iter(active_decision_context(), data); return; } for (ctx = report_contexts; ctx; ctx = ctx->report_next) iter(ctx, data); } /* * decision_context_init_rate_limits - initialize per-context log throttles. * @ctx: context to initialize. * Returns nothing. */ static void decision_context_init_rate_limits(struct decision_context *ctx) { atomic_init(&ctx->building_tracer_rate_limit.last_log, 0); ctx->building_tracer_rate_limit.interval = SUBJECT_BUILDING_LOG_INTERVAL; atomic_init(&ctx->building_stale_rate_limit.last_log, 0); ctx->building_stale_rate_limit.interval = SUBJECT_BUILDING_LOG_INTERVAL; } /* * decision_context_init_policy_counters - initialize policy metric counters. * @counters: policy counter block to initialize. * Returns nothing. */ static void decision_context_init_policy_counters( struct decision_policy_counters *counters) { atomic_init(&counters->allowed, 0); atomic_init(&counters->denied, 0); atomic_init(&counters->allowed_by_rule, 0); atomic_init(&counters->allowed_by_fallthrough, 0); atomic_init(&counters->fallthrough_open, 0); atomic_init(&counters->fallthrough_execute, 0); atomic_init(&counters->fallthrough_trusted, 0); atomic_init(&counters->fallthrough_untrusted, 0); atomic_init(&counters->fallthrough_trust_unknown, 0); atomic_init(&counters->fallthrough_executable, 0); atomic_init(&counters->fallthrough_programmatic, 0); atomic_init(&counters->fallthrough_sharedlib, 0); atomic_init(&counters->fallthrough_unknown_ftype, 0); atomic_init(&counters->fallthrough_other_ftype, 0); } /* * object_cache_flush_generation_snapshot - read the global object-cache * invalidation generation. * * Returns the latest generation requested by trust database publishers. */ unsigned long object_cache_flush_generation_snapshot(void) { return atomic_load_explicit(&object_cache_flush_generation, memory_order_acquire); } /* * request_object_cache_flush - publish an object-cache invalidation. * * Each decision worker owns a private object cache, so trust database updates * must be broadcast as a generation. Workers compare this global value with * the generation last applied to their local context and flush independently * before processing the next event. * * Returns the newly published generation. */ unsigned long request_object_cache_flush(void) { return atomic_fetch_add_explicit(&object_cache_flush_generation, 1, memory_order_acq_rel) + 1; } /* * event_now_ns - read monotonic time for BUILDING age checks. * * Returns monotonic nanoseconds, or zero if the clock cannot be read. */ static uint64_t event_now_ns(void) { struct timespec ts; if (clock_gettime(CLOCK_MONOTONIC, &ts)) return 0; return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; } /* * subject_building_age_ns - calculate the age of a BUILDING subject. * @info: cached process state to inspect. * * Returns the elapsed BUILDING age in nanoseconds, or zero when no start * time is available. */ static uint64_t subject_building_age_ns(const struct proc_info *info) { uint64_t now; if (info == NULL || info->building_started_ns == 0) return 0; now = event_now_ns(); if (now <= info->building_started_ns) return 0; return now - info->building_started_ns; } /* * subject_mark_building_event - record one event seen while BUILDING. * @info: cached process state to update. * * Returns nothing. */ static void subject_mark_building_event(struct proc_info *info) { if (info == NULL || info->state >= STATE_FULL) return; if (info->building_started_ns == 0) info->building_started_ns = event_now_ns(); if (info->building_event_count != UINT_MAX) info->building_event_count++; } /* * building_evict_reason_name - return display text for an eviction reason. * @reason: reason being reported. * * Returns a stable string for logs and reports. */ static const char *building_evict_reason_name( enum building_evict_reason reason) { if (reason == BUILDING_EVICT_TRACER) return "tracer"; return "stale"; } /* * record_building_evict - count and log a traced or stale BUILDING eviction. * @reason: reason the cache occupant is being evicted. * @ctx: immutable details for the diagnostic message. * * Returns nothing. */ static void record_building_evict(enum building_evict_reason reason, const struct building_evict_context *ctx) { struct message_rate_limit *log_limit; if (reason == BUILDING_EVICT_TRACER) { atomic_fetch_add_explicit(&building_tracer_evictions, 1, memory_order_relaxed); log_limit = &building_tracer_log; } else { atomic_fetch_add_explicit(&building_stale_evictions, 1, memory_order_relaxed); log_limit = &building_stale_log; } if (ctx == NULL || !message_rate_limit_allow(log_limit, time(NULL))) return; msg(LOG_WARNING, "BUILDING subject cache eviction: reason=%s pid=%d slot=%u " "state=%d path1=%s age_ns=%llu events=%u", building_evict_reason_name(reason), ctx->pid, ctx->slot, ctx->state, ctx->path1 ? ctx->path1 : "?", (unsigned long long)ctx->age_ns, ctx->event_count); } /* * subject_evict_warn - warn when a subject is evicted before fully built * @s: subject array to be evicted * * Rapid PID reuse can force a partially collected subject out of the * cache if the cache is too small. When this happens the dynamic linker * (ld.so) rule may deny access because when the evicted process re-appears * in the future, the loader (ld.so) appears as a standalone execution and * matches the ld_so pattern. Warn the administrator so they can consider * raising subj_cache_size to reduce the chances of this happening. */ static void subject_evict_warn(s_array *s) { int warn = 0; if (s && s->info && s->info->state < STATE_FULL) { /* * Normal interpreter re-exec replaces the process image * before all paths are gathered. If the re-exec ends in * a script (with or without #!) we know it is benign. * Suppress the suggestion to grow the cache. */ if (!((s->info->state == STATE_REOPEN) && (s->info->elf_info & (HAS_SHEBANG|TEXT_SCRIPT))) ) { warn = 1; atomic_fetch_add_explicit(&early_subj_cache_evictions, 1, memory_order_relaxed); } } if (atomic_load_explicit(&early_subj_cache_evictions, memory_order_relaxed) > 5) return; if (warn) { msg(LOG_WARNING, "pid %d in state %d (%s) is being evicted from the " "subject cache before pattern detection completes: " "increase subj_cache_size", s->info->pid, s->info->state, s->info->path1); } } /* * obj_evict_warn - check object cache eviction ratios * * Opportunistically check eviction ratios during evictions and warn the * administrator when thresholds indicate the object cache is too small. * Checks occur no more than once every 16 evictions and only one runtime * warning is emitted. * * It uses 2 ratios to decide if we need to issue a warning: * * E_over_M = evictions / misses * - Measures how often a miss requires throwing out an existing object. * - High values mean the cache is not just missing, but actively churning, * which points to either capacity pressure or poor distribution. * * E_over_Q = evictions / total lookups * - Measures the overall fraction of requests that cause an eviction. * - This gives a user-facing view of churn: how much of the workload is * paying the eviction penalty out of all operations. * * Together, these ratios let us distinguish "expected misses" from * "pathological evictions" and trigger a resize warning only when the cache * is turning over too aggressively for its occupancy level. */ __attribute__((cold)) static void obj_evict_warn(void *unused) { unsigned long evicts, miss, hit, lookups, e_over_m, e_over_q; unsigned long raw_evictions; unsigned int count; unsigned int occ, thr_m = 0, thr_q = 0; if (obj_cache_warned) return; raw_evictions = atomic_load_explicit(&obj_cache->evictions, memory_order_relaxed); if (raw_evictions & 0xF) return; evicts = raw_evictions + 1; miss = atomic_load_explicit(&obj_cache->misses, memory_order_relaxed) + 1; hit = atomic_load_explicit(&obj_cache->hits, memory_order_relaxed); lookups = hit + miss; count = atomic_load_explicit(&obj_cache->count, memory_order_relaxed); occ = (count * 100) / obj_cache->total; e_over_m = (evicts * 100) / miss; e_over_q = (evicts * 100) / (lookups ? lookups : 1); if (occ >= 85) { // Near-full tables churn; above these levels growth is // usually cheaper than misses. thr_m = 80; thr_q = 35; } else if (occ >= 75) { // Some churn is expected; beyond this you’re throwing away // too much reuse. thr_m = 55; thr_q = 20; } else if (occ >= 60) { // At this level evictions should be infrequent; higher means // collisions/skew or underprovisioning. thr_m = 35; thr_q = 12; } else return; if (e_over_m > thr_m || e_over_q > thr_q) { msg(LOG_WARNING, "object cache eviction ratios high (occupancy: %u%%, " "evict/miss=%lu%%, evict/lookups=%lu%%): " "increase obj_cache_size", occ, e_over_m, e_over_q); obj_cache_warned = true; } } /* * decision_context_destroy - release one worker's mutable decision state. * @ctx: context returned by decision_context_create(). * * The caller must stop the owning worker before destroying its context. The * cleanup disables cache eviction warnings because every live entry is being * intentionally discarded. */ void decision_context_destroy(struct decision_context *ctx) { if (ctx == NULL || ctx == &default_decision_context) return; /* We're intentionally clearing the caches; disable warnings */ if (ctx->subject_cache) ctx->subject_cache->evict_cb = NULL; if (atomic_load_explicit(&ctx->early_subject_cache_evictions, memory_order_relaxed)) msg(LOG_WARNING, "Processes are being evicted from the subject cache before " "pattern detection completes: increase subj_cache_size " "(total early evictions: %u)", atomic_load_explicit(&ctx->early_subject_cache_evictions, memory_order_relaxed)); if (ctx->object_cache) ctx->object_cache->evict_cb = NULL; if (ctx->object_cache_warned) msg(LOG_WARNING, "object cache eviction ratios high: increase obj_cache_size"); decision_context_unregister(ctx); decision_context_close_file_helpers(ctx); destroy_lru(ctx->subject_cache); destroy_lru(ctx->object_cache); decision_defer_destroy(&ctx->defer_queue); free(ctx->working_buffer); if (current_decision_context == ctx) decision_context_set_current(NULL); free(ctx); } /* * decision_context_create - allocate private mutable state for one worker. * @config: daemon cache and defer sizing configuration. * * The returned context owns its subject cache, object cache, defer array, * policy counters, logging buffer storage, and file helper state. File helper * handles are opened later by file_init() after the daemon has dropped to its * runtime credentials. * * Returns a new registered context, or NULL on allocation failure. */ struct decision_context *decision_context_create(const struct conf *config) { struct decision_context *ctx; if (config == NULL) return NULL; ctx = calloc(1, sizeof(*ctx)); if (ctx == NULL) return NULL; decision_context_init_rate_limits(ctx); atomic_init(&ctx->early_subject_cache_evictions, 0); atomic_init(&ctx->building_tracer_evict_count, 0); atomic_init(&ctx->building_stale_evict_count, 0); decision_context_init_policy_counters(&ctx->policy_counters); /* * Attach subject_evict_warn so we can see when fast PID turnover * drops a subject before classification completes. Without all the * paths collected ld.so can report spurious access denials. A larger * subj_cache_size lengthens the window and avoids this condition. */ ctx->subject_cache = init_lru(config->subj_cache_size, (void (*)(void *))subject_clear, "Subject", (void (*)(void *))subject_evict_warn); if (!ctx->subject_cache) { decision_context_destroy(ctx); return NULL; } ctx->object_cache = init_lru(config->obj_cache_size, (void (*)(void *))object_clear, "Object", obj_evict_warn); if (!ctx->object_cache) { decision_context_destroy(ctx); return NULL; } ctx->object_cache_flush_generation = object_cache_flush_generation_snapshot(); if (decision_defer_init(&ctx->defer_queue, config->subj_cache_size)) { decision_context_destroy(ctx); return NULL; } decision_defer_metrics_snapshot_reset(&ctx->defer_queue, &ctx->last_defer_metrics, 0); decision_context_register(ctx); return ctx; } // Return 0 on success and 1 on error int init_event_system(const conf_t *config) { struct decision_context *ctx = decision_context_create(config); if (ctx == NULL) return 1; decision_context_set_current(ctx); return 0; } static int flush_cache(void) { Queue *old_cache = obj_cache; Queue *new_cache; unsigned int size; if (old_cache == NULL) return 1; if (atomic_load_explicit(&old_cache->count, memory_order_relaxed) == 0) return 0; size = old_cache->total; msg(LOG_DEBUG, "Flushing object cache"); new_cache = init_lru(size, (void (*)(void *))object_clear, "Object", obj_evict_warn); if (!new_cache) return 1; old_cache->evict_cb = NULL; destroy_lru(old_cache); obj_cache = new_cache; msg(LOG_DEBUG, "Flushed"); return 0; } /* * flush_cache_if_needed - apply pending object-cache invalidations locally. * * Trust database updates advance a global generation. Since every decision * context owns its own object cache, each worker must compare and update its * local generation instead of consuming a single global flag. * * Returns 0 on success and 1 if the cache could not be rebuilt. */ static int flush_cache_if_needed(void) { struct decision_context *ctx = active_decision_context(); unsigned long generation; struct decision_timing_span timing; int rc; generation = object_cache_flush_generation_snapshot(); if (ctx->object_cache_flush_generation == generation) return 0; decision_timing_stage_begin(DECISION_TIMING_STAGE_CACHE_FLUSH, &timing); rc = flush_cache(); decision_timing_stage_end(&timing); if (rc == 0) ctx->object_cache_flush_generation = generation; return rc; } void destroy_event_system(void) { struct decision_context *ctx; while (report_contexts) { ctx = report_contexts; decision_context_destroy(ctx); } decision_context_set_current(NULL); } static inline void reset_subject_attributes(s_array *s) { subject_reset(s, EXE); subject_reset(s, COMM); subject_reset(s, EXE_TYPE); subject_reset(s, SUBJ_TRUST); } /* * event_subject_slot - compute the subject cache slot for a pid. * @pid: process id from fanotify metadata. * * Returns the hash slot used by new_event() for the subject cache. Deferral * must use this helper so it observes the same collision domain as the cache. */ unsigned int event_subject_slot(pid_t pid) { return compute_subject_key(subj_cache, pid); } /* * subject_slot_info - return cached process state for one subject slot. * @slot: subject cache slot to inspect. * * Returns the cached process metadata, or NULL when the slot is empty. */ static struct proc_info *subject_slot_info(unsigned int slot) { QNode *q_node = lru_peek_slot(subj_cache, slot); s_array *s; if (q_node == NULL || q_node->item == NULL) return NULL; s = (s_array *)q_node->item; return s->info; } /* * subject_slot_state - return the cached subject state for one slot. * @slot: subject cache slot to inspect. * @pid: optional destination for the cached pid. * * Returns STATE_FULL when the slot is empty or has no process metadata. That * lets callers treat incomplete cache entries only as blocking when the cache * has a real subject whose startup pattern state is still before STATE_FULL. */ static state_t subject_slot_state(unsigned int slot, pid_t *pid) { struct proc_info *info = subject_slot_info(slot); if (info == NULL) return STATE_FULL; if (pid) *pid = info->pid; return info->state; } /* * subject_building_is_traced - test if a BUILDING subject has a tracer. * @info: cached process state for the subject cache occupant. * * Returns 1 when /proc reports a nonzero TracerPid, 0 otherwise. */ static int subject_building_is_traced(const struct proc_info *info) { struct proc_status_info status = { .ppid = -1, .tracer_state = PROC_TRACER_UNKNOWN, .uid = NULL, .groups = NULL, .comm = NULL }; if (info == NULL) return 0; if (read_proc_status(info->pid, PROC_STAT_TRACER, &status) != 0) return 0; return status.tracer_state == PROC_TRACER_TRACED; } /* * subject_building_is_stale - test if BUILDING metadata is stuck or obsolete. * @info: cached process state for the subject cache occupant. * * Returns 1 when the process disappeared, the PID was reused, or the subject * has stayed BUILDING longer than the bounded stale window. */ static int subject_building_is_stale(const struct proc_info *info) { struct proc_info *current; int stale; if (info == NULL) return 0; current = stat_proc_entry(info->pid); if (current == NULL) return 1; stale = compare_proc_infos(current, info); clear_proc_info(current); free(current); if (stale) return 1; return subject_building_age_ns(info) >= SUBJECT_BUILDING_STALE_NS; } /* * evict_building_subject - remove a traced or stale BUILDING occupant. * @slot: subject cache slot to evict. * @info: cached process state expected to occupy @slot. * @reason: eviction reason to count and log. * * Returns nothing. */ static void evict_building_subject(unsigned int slot, struct proc_info *info, enum building_evict_reason reason) { struct building_evict_context ctx; QNode *q_node; s_array *s; if (info == NULL) return; q_node = check_lru_cache(subj_cache, slot); if (q_node == NULL || q_node->item == NULL) return; s = (s_array *)q_node->item; if (s->info != info) return; ctx.pid = info->pid; ctx.slot = slot; ctx.state = info->state; ctx.path1 = info->path1; ctx.age_ns = subject_building_age_ns(info); ctx.event_count = info->building_event_count; record_building_evict(reason, &ctx); lru_record_collision(subj_cache); lru_evict(subj_cache, slot); } /* * event_subject_slot_is_blocked - test whether an incoming event must wait. * @slot: subject cache slot for the incoming event. * @pid: incoming event pid. * * A different pid in a pre-STATE_FULL slot is still building pattern state and * would be prematurely evicted by new_event(). Same-pid events are allowed to * proceed because they may be the event that advances the subject state. * Deferring same-pid events would park the work needed to move the subject out * of its building state and could deadlock the slot. * * Returns 1 when the incoming event should be deferred, 0 otherwise. */ int event_subject_slot_is_blocked(unsigned int slot, pid_t pid) { struct proc_info *info = subject_slot_info(slot); if (info == NULL) return 0; if (info->pid == pid || info->state >= STATE_FULL) return 0; if (subject_building_is_traced(info)) { evict_building_subject(slot, info, BUILDING_EVICT_TRACER); return 0; } if (subject_building_is_stale(info)) { evict_building_subject(slot, info, BUILDING_EVICT_STALE); return 0; } return 1; } /* * event_subject_slot_is_unblocked - test whether a slot can release defers. * @slot: subject cache slot to inspect. * * Returns 1 when the slot is empty or its current subject has reached * STATE_FULL or a later terminal pattern state. */ int event_subject_slot_is_unblocked(unsigned int slot) { return subject_slot_state(slot, NULL) >= STATE_FULL; } // Return 0 on success and 1 on error int new_event(const struct fanotify_event_metadata *m, event_t *e) { subject_attr_t subj; QNode *q_node; unsigned int key, subject_key, rc, evict = 1, skip_path = 0; s_array *s; o_array *o; struct proc_info *pinfo; struct file_info *finfo; struct decision_timing_span timing; if (flush_cache_if_needed()) return 1; // Transfer things from fanotify structs to ours e->pid = m->pid; e->fd = m->fd; e->type = m->mask & ALL_EVENTS; e->num = 0; key = compute_subject_key(subj_cache, m->pid); subject_key = key; q_node = check_lru_cache(subj_cache, key); s = (s_array *)q_node->item; // get proc fingerprint decision_timing_stage_begin( DECISION_TIMING_STAGE_PROC_FINGERPRINT, &timing); pinfo = stat_proc_entry(m->pid); decision_timing_stage_end(&timing); if (pinfo == NULL) return 1; // Check the subject to see if its what its supposed to be if (s) { rc = compare_proc_infos(pinfo, s->info); // EXEC_PERM causes 2 events for every execute. First is an // execute request. This is followed by an open request of // the same file. So, if we are collecting and perm is open, // that means this is the second step, open. We also need // be sure we are the same process. We skip collecting path // because it was collected on perm = execute. if ((s->info->state == STATE_COLLECTING) && (e->type & FAN_OPEN_PERM) && !rc) { // special branch after ld_so exec // next opens will go fall trough if (s->info->path1 && (strcmp(s->info->path1, SYSTEM_LD_SO) == 0)) s->info->state = STATE_DEFAULT_REOPEN; else { skip_path = 1; s->info->state = STATE_REOPEN; } } // If not same proc or we detect execution, evict if (rc) lru_record_collision(subj_cache); evict = rc || e->type & FAN_OPEN_EXEC_PERM; // We need to reset everything now that execve has finished if (s->info->state == STATE_STATIC_PARTIAL && !rc) { // If the static app itself launches an app right // away, go back to collecting. if (e->type & FAN_OPEN_EXEC_PERM) s->info->state = STATE_COLLECTING; else { s->info->state = STATE_STATIC; skip_path = 1; } evict = 0; reset_subject_attributes(s); } // Static has to sequence through a state machine to get to // the point where we can do a full subject reset. Still // in execve at this point. if ((s->info->state == STATE_STATIC_REOPEN) && (e->type & FAN_OPEN_PERM) && !rc) { s->info->state = STATE_STATIC_PARTIAL; evict = 0; skip_path = 1; } // If we've seen the reopen and its an execute and process // has an interpreter and we're the same process, don't evict // and don't collect the path since reopen interp will. The // !skip_path is to prevent the STATE_REOPEN change above from // falling into this. if ((s->info->state == STATE_REOPEN) && !skip_path && (e->type & FAN_OPEN_EXEC_PERM) && (s->info->elf_info & HAS_INTERP) && !rc) { s->info->state = STATE_DEFAULT_REOPEN; evict = 0; skip_path = 1; } // this is how STATE_REOPEN and // STATE_DEFAULT_REOPEN differs // in STATE_REOPEN path is always skipped if ((s->info->state == STATE_REOPEN) && !skip_path && (e->type & FAN_OPEN_PERM) && !rc) { skip_path = 1; } if (evict) { lru_evict(subj_cache, key); q_node = check_lru_cache(subj_cache, key); s = (s_array *)q_node->item; } else if (s->cnt == 0) msg(LOG_DEBUG, "cached subject has cnt of 0"); } if (evict) { // If empty, setup the subject with what we currently have e->s = malloc(sizeof(s_array)); if (e->s == NULL || subject_create(e->s)) { free(e->s); e->s = NULL; clear_proc_info(pinfo); free(pinfo); lru_evict(subj_cache, key); return 1; } subj.type = PID; subj.pid = e->pid; if (subject_add(e->s, &subj)) { subject_clear(e->s); free(e->s); e->s = NULL; clear_proc_info(pinfo); free(pinfo); lru_evict(subj_cache, key); return 1; } // give custody of the list to the cache q_node->item = e->s; ((s_array *)q_node->item)->info = pinfo; // If this is the first time we've seen this process // and its doing a file open, its likely to be a running // process. That means we should not do pattern detection. if (!s && (e->type & FAN_OPEN_PERM)) pinfo->state = STATE_NORMAL; } else { // Use the one from the cache e->s = s; clear_proc_info(pinfo); free(pinfo); } // Init the object // get file fingerprint rc = 1; decision_timing_stage_begin(DECISION_TIMING_STAGE_FD_STAT, &timing); finfo = stat_file_entry(m->fd); decision_timing_stage_end(&timing); if (finfo == NULL) { /* On stat_file_entry failure, evict the subject to avoid * leaving an incomplete subject cached, which could * confuse later lookups and pattern matching. */ if (evict) { lru_evict(subj_cache, key); e->s = NULL; } return 1; } // Just using inodes don't give a good key. It needs // conditioning to use more slots in the cache. unsigned long magic = finfo->inode + finfo->time.tv_nsec + finfo->size; key = compute_object_key(obj_cache, magic); q_node = check_lru_cache(obj_cache, key); o = (o_array *)q_node->item; if (o) { rc = compare_file_infos(finfo, o->info); if (rc) { lru_record_collision(obj_cache); lru_evict(obj_cache, key); q_node = check_lru_cache(obj_cache, key); o = (o_array *)q_node->item; } } if (rc) { // If empty, setup the object with what we currently have e->o = malloc(sizeof(o_array)); if (e->o == NULL || object_create(e->o)) { free(e->o); e->o = NULL; free(finfo); lru_evict(obj_cache, key); if (evict) { lru_evict(subj_cache, subject_key); e->s = NULL; } return 1; } // give custody of the list to the cache q_node->item = e->o; ((o_array *)q_node->item)->info = finfo; } else { // Use the one from the cache e->o = o; free(finfo); } // Setup pattern info pinfo = e->s->info; if (pinfo && !skip_path && pinfo->state < STATE_FULL) { object_attr_t *on = get_obj_attr(e, PATH); if (on) { const char *file = on->o; if (pinfo->path1 == NULL) { // In this step, we gather info on what is // being asked permission to execute. pinfo->path1 = strdup(file); pinfo->elf_info = gather_elf(e->fd, e->o->info->size); // pinfo->state = STATE_COLLECTING;Just for clarity } else if (pinfo->path2 == NULL) { pinfo->path2 = strdup(file); pinfo->state = STATE_PARTIAL; } else { // This third look is needed because the first // two are still the old process as far as // procfs is concerned. Reset things that could // change based on the new process name. pinfo->state = STATE_FULL; reset_subject_attributes(s); } } } subject_mark_building_event(pinfo); return 0; } /* * fetch_proc_status - populate subject cache entries using /proc status * @e: event whose subject cache should be filled * @t: subject attribute type requested by the caller * * The function gathers all configured fields from /proc//status for the * process associated with @e. Each successfully read attribute is added to * the subject cache so subsequent lookups do not need to touch procfs * again. * * Return: pointer to the requested attribute on success, NULL otherwise. */ static subject_attr_t *fetch_proc_status(event_t *e, subject_type_t t) { unsigned int mask = policy_get_proc_status_mask(); struct proc_status_info info = { .ppid = -1, .uid = NULL, .groups = NULL, .comm = NULL }; struct decision_timing_span timing; decision_timing_stage_begin( DECISION_TIMING_STAGE_PROC_STATUS_EXE_LOOKUP, &timing); if (read_proc_status(e->pid, mask, &info) != 0) { decision_timing_stage_end(&timing); return NULL; } decision_timing_stage_end(&timing); // Cache everything - sets and comm are malloc'ed. Transfer ownership. // Not checking return of subject_add. Caller needs to check for NULL. if (mask & PROC_STAT_PPID) { subject_attr_t sub; sub.type = PPID; sub.pid = info.ppid; subject_add(e->s, &sub); } if (mask & PROC_STAT_UID) { subject_attr_t sub; sub.type = UID; sub.set = info.uid; subject_add(e->s, &sub); } if (mask & PROC_STAT_GID) { subject_attr_t sub; sub.type = GID; sub.set = info.groups; subject_add(e->s, &sub); } if (mask & PROC_STAT_COMM) { subject_attr_t sub; sub.type = COMM; sub.str = info.comm; subject_add(e->s, &sub); } //return the subject entry return subject_access(e->s, t); } /* * get_subj_attr - return a subject attribute, creating it on demand * @e: event describing the subject whose attribute is needed * @t: subject attribute identifier * * The function first looks for @t in the subject cache. When missing, it * performs the necessary lookup and stores the result for reuse. Some * attributes are retrieved directly, while UID/GID and credential data * are collected in bulk via fetch_proc_status(). * * Return: pointer to the requested attribute, or NULL if acquisition fails. */ __attribute__((hot)) subject_attr_t *get_subj_attr(event_t *e, subject_type_t t) { subject_attr_t subj; subject_attr_t *sn; s_array *s = e->s; struct decision_timing_span timing; attr_lookup_metrics_count_subject_request(t); sn = subject_access(s, t); if (sn) return sn; // The desired attribute is not on the list, look it up and cache it subj.type = t; subj.str = NULL; switch (t) { case AUID: attr_lookup_metrics_count_subject_lookup(t); decision_timing_stage_begin( DECISION_TIMING_STAGE_PROC_STATUS_EXE_LOOKUP, &timing); subj.uval = get_program_auid_from_pid(e->pid); decision_timing_stage_end(&timing); break; case PPID: case UID: case GID: case COMM: attr_lookup_metrics_count_subject_lookup(t); /* * UID/GID credentials may differ between the real, * effective, saved, and filesystem slots. Cache all * but saved so the rule engine can evaluate all * possible identities during matching. */ return fetch_proc_status(e, t); break; case SESSIONID: attr_lookup_metrics_count_subject_lookup(t); decision_timing_stage_begin( DECISION_TIMING_STAGE_PROC_STATUS_EXE_LOOKUP, &timing); subj.uval = (unsigned int) get_program_sessionid_from_pid(e->pid); decision_timing_stage_end(&timing); break; case PID: attr_lookup_metrics_count_subject_lookup(t); subj.pid = e->pid; break; // If these 2 ever get separated, update subject_add // and subject_access in subject.c case EXE: case EXE_DIR: { char buf[PATH_MAX+1], *ptr; attr_lookup_metrics_count_subject_lookup(t); errno = 0; decision_timing_stage_begin( DECISION_TIMING_STAGE_PROC_STATUS_EXE_LOOKUP, &timing); ptr = get_program_from_pid(e->pid, sizeof(buf), buf); decision_timing_stage_end(&timing); if (errno == ENOENT) { /* kworkers have no exe entry * readlink("/proc/4/exe", 0x55624a28d410, 64) = -1 ENOENT (No such file or directory) * use comm */ sn = subject_access(s, COMM); if (!sn) sn = fetch_proc_status(e, COMM); if (sn) subj.str = strdup(sn->str); else subj.str = strdup("?"); } else if (ptr) subj.str = strdup(buf); else subj.str = strdup("?"); } break; case EXE_TYPE: { char buf[128], *ptr; attr_lookup_metrics_count_subject_lookup(t); decision_timing_stage_begin( DECISION_TIMING_STAGE_PROC_STATUS_EXE_LOOKUP, &timing); ptr = get_type_from_pid(e->pid, sizeof(buf), buf); decision_timing_stage_end(&timing); if (ptr) subj.str = strdup(buf); else subj.str = strdup("?"); } break; case SUBJ_TRUST: { subject_attr_t *exe; attr_lookup_metrics_count_subject_lookup(t); exe = get_subj_attr(e, EXE); subj.uval = 0; if (exe) { if (exe->str) { int res = check_trust_database(exe->str, NULL, 0); // ignore -1 if (res == 1) subj.uval = 1; else subj.uval = 0; } } } break; default: return NULL; } if (subject_add(e->s, &subj) == 0) { sn = subject_access(e->s, t); return sn; } // free .str only when it was really used // otherwise invalid free is possible if (t >= COMM) free(subj.str); return NULL; } /* * This function will search the list for a nv pair of the right type. * If not found, it will create the type and return it. */ __attribute__((hot)) object_attr_t *get_obj_attr(event_t *e, object_type_t t) { char buf[PATH_MAX+1], *ptr; object_attr_t obj; object_attr_t *on; o_array *o = e->o; struct decision_timing_span timing; attr_lookup_metrics_count_object_request(t); on = object_access(o, t); if (on) return on; // One not on the list, look it up and make one obj.type = t; obj.o = NULL; obj.val = 0; switch (t) { case PATH: case ODIR: attr_lookup_metrics_count_object_lookup(t); // Try to avoid looking up the path if we have it on = object_find_file(o); if (on) obj.o = strdup(on->o); else { decision_timing_stage_begin( DECISION_TIMING_STAGE_FD_PATH_RESOLUTION, &timing); ptr = get_file_from_fd(e->fd, e->pid, sizeof(buf), buf); decision_timing_stage_end(&timing); if (ptr) obj.o = strdup(buf); else obj.o = strdup("?"); } break; case DEVICE: attr_lookup_metrics_count_object_lookup(t); ptr = get_device_from_stat(o->info->device, sizeof(buf), buf); if (ptr) obj.o = strdup(buf); else obj.o = strdup("?"); break; case FTYPE: { object_attr_t *path; attr_lookup_metrics_count_object_lookup(t); path = get_obj_attr(e, PATH); // Attribute MIME lookup to the active logical driver // so rule and response/debug costs stay separate in // timing reports. decision_timing_mime_stage_begin( DECISION_TIMING_MIME_TOTAL, &timing); ptr = get_file_type_from_fd(e->fd, o->info, path ? path->o : "?", sizeof(buf), buf); decision_timing_stage_end(&timing); if (ptr) obj.o = strdup(buf); else obj.o = strdup("?"); } break; case FILE_HASH: { file_hash_alg_t alg = FILE_HASH_ALG_SHA256; attr_lookup_metrics_count_object_lookup(t); if (o->info) { if (o->info->digest_alg != FILE_HASH_ALG_NONE) alg = o->info->digest_alg; if (o->info->digest[0]) { obj.o = strdup(o->info->digest); break; } obj.o = get_hash_from_fd2(e->fd, o->info->size, alg); if (obj.o) { file_info_cache_digest(o->info, alg); strncpy(o->info->digest, obj.o, FILE_DIGEST_STRING_MAX-1); o->info->digest[FILE_DIGEST_STRING_MAX-1] = 0; } else file_info_reset_digest(o->info); } } break; case OBJ_TRUST: { object_attr_t *path; attr_lookup_metrics_count_object_lookup(t); path = get_obj_attr(e, PATH); if (path && path->o) { int res = check_trust_database(path->o, o->info, e->fd); // ignore -1 if (res == 1) obj.val = 1; else obj.val = 0; } } break; default: obj.o = NULL; return NULL; } if (object_add(e->o, &obj) == 0) { on = object_access(e->o, t); return on; } free(obj.o); return NULL; } static void print_queue_stats(FILE *f, const struct lru_metrics *metrics) { fprintf(f, "%s cache size: %u\n", metrics->name, metrics->total); fprintf(f, "%s slots in use: %u (%u%%)\n", metrics->name, metrics->count, metrics->total ? (100*metrics->count)/metrics->total : 0); fprintf(f, "%s hits: %lu\n", metrics->name, metrics->hits); fprintf(f, "%s misses: %lu\n", metrics->name, metrics->misses); fprintf(f, "%s collisions: %lu\n", metrics->name, metrics->collisions); fprintf(f, "%s evictions: %lu (%lu%%)\n", metrics->name, metrics->evictions, metrics->hits ? (100*metrics->evictions)/metrics->hits : 0); } /* * print_cache_config - write cache capacity from a metrics snapshot. * @f: report stream. * @metrics: cache metrics snapshot. * Returns nothing. */ static void print_cache_config(FILE *f, const struct lru_metrics *metrics) { fprintf(f, "Per worker %s cache size: %u\n", metrics->name, metrics->total); } /* * print_cache_utilization - write current cache occupancy. * @f: report stream. * @metrics: cache metrics snapshot. * Returns nothing. */ static void print_cache_utilization(FILE *f, const struct lru_metrics *metrics) { fprintf(f, "%s slots in use: %u (%u%%)\n", metrics->name, metrics->count, metrics->total ? (100*metrics->count)/metrics->total : 0); } /* * print_cache_metrics - write resettable cache effectiveness counters. * @f: report stream. * @metrics: cache metrics snapshot. * Returns nothing. */ static void print_cache_metrics(FILE *f, const struct lru_metrics *metrics) { fprintf(f, "%s hits: %lu\n", metrics->name, metrics->hits); fprintf(f, "%s misses: %lu\n", metrics->name, metrics->misses); fprintf(f, "%s collisions: %lu\n", metrics->name, metrics->collisions); fprintf(f, "%s evictions: %lu (%lu%%)\n", metrics->name, metrics->evictions, metrics->hits ? (100*metrics->evictions)/metrics->hits : 0); } struct cache_worker_snapshot { unsigned int worker_id; struct lru_metrics subject; struct lru_metrics object; unsigned int early_evictions; unsigned int tracer_evictions; unsigned int stale_evictions; }; struct cache_report_snapshot { struct lru_metrics subject; struct lru_metrics object; unsigned int early_evictions; unsigned int tracer_evictions; unsigned int stale_evictions; unsigned int worker_count; struct cache_worker_snapshot workers[DAEMON_CONFIG_DECISION_THREADS_MAX]; int reset; }; /* * lru_metrics_merge - fold one worker cache snapshot into an aggregate. * @aggregate: total cache metrics reported to operators. * @metrics: metrics copied from one worker-owned cache. * Returns nothing. */ static void lru_metrics_merge(struct lru_metrics *aggregate, const struct lru_metrics *metrics) { aggregate->count += metrics->count; aggregate->total += metrics->total; aggregate->hits += metrics->hits; aggregate->misses += metrics->misses; aggregate->collisions += metrics->collisions; aggregate->evictions += metrics->evictions; } /* * cache_report_snapshot_context - snapshot one worker context for reports. * @ctx: worker context being sampled. * @data: struct cache_report_snapshot aggregate. * Returns nothing. */ static void cache_report_snapshot_context(struct decision_context *ctx, void *data) { struct cache_report_snapshot *snapshot = data; struct cache_worker_snapshot *worker = NULL; unsigned int index; if (ctx == NULL || snapshot == NULL) return; index = snapshot->worker_count; if (index < DAEMON_CONFIG_DECISION_THREADS_MAX) { worker = &snapshot->workers[index]; worker->worker_id = ctx->worker_id; lru_metrics_snapshot(ctx->subject_cache, &worker->subject, snapshot->reset); lru_metrics_snapshot(ctx->object_cache, &worker->object, snapshot->reset); lru_metrics_merge(&snapshot->subject, &worker->subject); lru_metrics_merge(&snapshot->object, &worker->object); } else { struct lru_metrics metrics; lru_metrics_snapshot(ctx->subject_cache, &metrics, snapshot->reset); lru_metrics_merge(&snapshot->subject, &metrics); lru_metrics_snapshot(ctx->object_cache, &metrics, snapshot->reset); lru_metrics_merge(&snapshot->object, &metrics); } if (snapshot->reset) { unsigned int early = atomic_exchange_explicit( &ctx->early_subject_cache_evictions, 0, memory_order_relaxed); unsigned int tracer = atomic_exchange_explicit( &ctx->building_tracer_evict_count, 0, memory_order_relaxed); unsigned int stale = atomic_exchange_explicit( &ctx->building_stale_evict_count, 0, memory_order_relaxed); snapshot->early_evictions += early; snapshot->tracer_evictions += tracer; snapshot->stale_evictions += stale; if (worker) { worker->early_evictions = early; worker->tracer_evictions = tracer; worker->stale_evictions = stale; } } else { unsigned int early = atomic_load_explicit( &ctx->early_subject_cache_evictions, memory_order_relaxed); unsigned int tracer = atomic_load_explicit( &ctx->building_tracer_evict_count, memory_order_relaxed); unsigned int stale = atomic_load_explicit( &ctx->building_stale_evict_count, memory_order_relaxed); snapshot->early_evictions += early; snapshot->tracer_evictions += tracer; snapshot->stale_evictions += stale; if (worker) { worker->early_evictions = early; worker->tracer_evictions = tracer; worker->stale_evictions = stale; } } if (snapshot->worker_count < DAEMON_CONFIG_DECISION_THREADS_MAX) snapshot->worker_count++; } /* * cache_report_snapshot_reset - aggregate cache counters across workers. * @snapshot: destination for aggregate cache and BUILDING eviction metrics. * @reset: non-zero resets interval counters after copying them. * Returns nothing. */ static void cache_report_snapshot_reset(struct cache_report_snapshot *snapshot, int reset) { memset(snapshot, 0, sizeof(*snapshot)); snapshot->subject.name = "Subject"; snapshot->object.name = "Object"; snapshot->reset = reset; decision_context_for_each(cache_report_snapshot_context, snapshot); } /* * cache_worker_snapshot_sort - order worker snapshots by worker id. * @workers: worker snapshot array. * @count: number of populated entries in @workers. * Returns nothing. */ static void cache_worker_snapshot_sort(struct cache_worker_snapshot *workers, unsigned int count) { unsigned int i, j; for (i = 0; i < count; i++) { for (j = i + 1; j < count; j++) { struct cache_worker_snapshot tmp; if (workers[i].worker_id <= workers[j].worker_id) continue; tmp = workers[i]; workers[i] = workers[j]; workers[j] = tmp; } } } /* * print_cache_metrics_worker - write one worker's cache counters. * @f: report stream. * @worker_id: decision worker that owns these cache counters. * @metrics: cache metrics snapshot. * Returns nothing. */ static void print_cache_metrics_worker(FILE *f, unsigned int worker_id, const struct lru_metrics *metrics) { fprintf(f, " Decision worker %u %s hits: %lu\n", worker_id, metrics->name, metrics->hits); fprintf(f, " Decision worker %u %s misses: %lu\n", worker_id, metrics->name, metrics->misses); fprintf(f, " Decision worker %u %s collisions: %lu\n", worker_id, metrics->name, metrics->collisions); fprintf(f, " Decision worker %u %s evictions: %lu (%lu%%)\n", worker_id, metrics->name, metrics->evictions, metrics->hits ? (100*metrics->evictions)/metrics->hits : 0); } void run_usage_report(const conf_t *config, FILE *f) { time_t t; QNode *q_node; struct lru_metrics metrics; if (f == NULL) return; if (config->detailed_report) { t = time(NULL); fprintf(f, "File access attempts from oldest to newest as of %s\n", ctime(&t)); fprintf(f, "\tFILE\t\t\t\t\t\t ATTEMPTS\n"); fprintf(f, "---------------------------------------------------------------------------\n" ); if (atomic_load_explicit(&obj_cache->count, memory_order_relaxed) == 0) { fprintf(f, "(none)\n"); return; } q_node = obj_cache->end; while (q_node) { unsigned int len; const char *file; o_array *o = (o_array *)q_node->item; object_attr_t *on = object_find_file(o); if (on == NULL) goto next_obj; file = on->o; if (file == NULL) goto next_obj; len = strlen(file); if (len > 62) fprintf(f, "%s\t%lu\n", file, q_node->uses); else fprintf(f, "%-62s\t%lu\n", file, q_node->uses); next_obj: q_node = q_node->prev; } fprintf(f, "\n---\n\n"); } lru_metrics_snapshot(obj_cache, &metrics, 0); print_queue_stats(f, &metrics); fprintf(f, "\n\n"); if (config->detailed_report) { fprintf(f, "Active processes oldest to most recently active as of %s\n", ctime(&t)); fprintf(f, "\tEXE\tCOMM\t\t\t\t\t ATTEMPTS\n"); fprintf(f, "---------------------------------------------------------------------------\n" ); if (atomic_load_explicit(&subj_cache->count, memory_order_relaxed) == 0) { fprintf(f, "(none)\n"); return; } q_node = subj_cache->end; while (q_node) { unsigned int len; char *exe, *comm, *text; subject_attr_t *se, *sc; s_array *s = (s_array *)q_node->item; se = subject_find_exe(s); if (se == NULL) goto next_subj; exe = se->str; if (exe == NULL) goto next_subj; sc = subject_find_comm(s); if (sc == NULL) comm = "?"; else comm = sc->str ? sc->str : "?"; if (asprintf(&text, "%s (%s)", exe, comm) < 0) { fprintf(f, "?\n"); goto next_subj; } len = strlen(text); if (len > 62) fprintf(f, "%s\t%lu\n", text, q_node->uses); else fprintf(f,"%-62s\t%lu\n", text, q_node->uses); free(text); next_subj: q_node = q_node->prev; } fprintf(f, "\n---\n\n"); } lru_metrics_snapshot(subj_cache, &metrics, 0); print_queue_stats(f, &metrics); fprintf(f, "\n"); } void do_cache_reports(FILE *f) { do_cache_reports_reset(f, 0); } /* * do_cache_config_report - write cache capacities sized at startup. * @f: report stream. * Returns nothing. */ void do_cache_config_report(FILE *f) { struct cache_report_snapshot snapshot; struct lru_metrics subject; struct lru_metrics object; if (f == NULL) return; cache_report_snapshot_reset(&snapshot, 0); subject = snapshot.subject; object = snapshot.object; if (snapshot.worker_count) { subject.total /= snapshot.worker_count; object.total /= snapshot.worker_count; } print_cache_config(f, &subject); print_cache_config(f, &object); } /* * do_cache_utilization_report - write current cache slot utilization. * @f: report stream. * Returns nothing. */ void do_cache_utilization_report(FILE *f) { struct cache_report_snapshot snapshot; if (f == NULL) return; cache_report_snapshot_reset(&snapshot, 0); print_cache_utilization(f, &snapshot.subject); print_cache_utilization(f, &snapshot.object); } /* * do_cache_health_report - write cache counters that warrant investigation. * @f: report stream. * Returns nothing. */ void do_cache_health_report(FILE *f) { struct cache_report_snapshot snapshot; if (f == NULL) return; cache_report_snapshot_reset(&snapshot, 0); fprintf(f, "Early subject cache evictions: %u\n", snapshot.early_evictions); fprintf(f, "Subject BUILDING tracer evictions: %u\n", snapshot.tracer_evictions); fprintf(f, "Subject BUILDING stale evictions: %u\n", snapshot.stale_evictions); } /* * do_cache_metrics_report_reset - write cache effectiveness counters. * @f: report stream. * @reset: non-zero resets counters after snapshotting them. * Returns nothing. */ void do_cache_metrics_report_reset(FILE *f, int reset) { struct cache_report_snapshot snapshot; if (f == NULL) return; cache_report_snapshot_reset(&snapshot, reset); fprintf(f, "\nSubject cache effectiveness:\n"); print_cache_metrics(f, &snapshot.subject); fprintf(f, "Early subject cache evictions: %u\n", snapshot.early_evictions); fprintf(f, "Subject BUILDING tracer evictions: %u\n", snapshot.tracer_evictions); fprintf(f, "Subject BUILDING stale evictions: %u\n", snapshot.stale_evictions); fprintf(f, "\nObject cache effectiveness:\n"); print_cache_metrics(f, &snapshot.object); if (snapshot.worker_count) { unsigned int i; cache_worker_snapshot_sort(snapshot.workers, snapshot.worker_count); fprintf(f, "\nPer-worker cache effectiveness:\n"); for (i = 0; i < snapshot.worker_count; i++) { struct cache_worker_snapshot *worker = &snapshot.workers[i]; print_cache_metrics_worker(f, worker->worker_id, &worker->subject); fprintf(f, " Decision worker %u Early subject cache evictions: %u\n", worker->worker_id, worker->early_evictions); fprintf(f, " Decision worker %u Subject BUILDING tracer evictions: %u\n", worker->worker_id, worker->tracer_evictions); fprintf(f, " Decision worker %u Subject BUILDING stale evictions: %u\n", worker->worker_id, worker->stale_evictions); print_cache_metrics_worker(f, worker->worker_id, &worker->object); } } } /* * do_cache_reports_reset - write cache metrics and optionally reset counters. * @f: output stream. * @reset: non-zero resets interval counters after copying them. * * Cache sizes and occupancy are state values and are never reset. */ void do_cache_reports_reset(FILE *f, int reset) { do_cache_config_report(f); do_cache_utilization_report(f); do_cache_metrics_report_reset(f, reset); } fapolicyd-2.0/src/library/event.h000066400000000000000000000041211524071644000170530ustar00rootroot00000000000000/* * event.h - Header file for event.c * Copyright (c) 2016,2018-20,2022-23,2025-26 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * Radovan Sroka */ #ifndef EVENT_HEADER #define EVENT_HEADER #include "config.h" #include #include #include #include "subject.h" #include "object.h" #include "conf.h" typedef struct ev { pid_t pid; int fd; int type; unsigned num; s_array *s; o_array *o; } event_t; int init_event_system(const conf_t *config); void destroy_event_system(void); unsigned long request_object_cache_flush(void); unsigned long object_cache_flush_generation_snapshot(void); int new_event(const struct fanotify_event_metadata *m, event_t *e); unsigned int event_subject_slot(pid_t pid); int event_subject_slot_is_blocked(unsigned int slot, pid_t pid); int event_subject_slot_is_unblocked(unsigned int slot); __attribute__((hot)) subject_attr_t *get_subj_attr(event_t *e, subject_type_t t); __attribute__((hot)) object_attr_t *get_obj_attr(event_t *e, object_type_t t); void run_usage_report(const conf_t *config, FILE *f); void do_cache_config_report(FILE *f); void do_cache_utilization_report(FILE *f); void do_cache_health_report(FILE *f); void do_cache_metrics_report_reset(FILE *f, int reset); void do_cache_reports(FILE *f); void do_cache_reports_reset(FILE *f, int reset); #endif fapolicyd-2.0/src/library/failure-action.c000066400000000000000000000145011524071644000206320ustar00rootroot00000000000000/* * failure-action.c - internal failure action model * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #include "config.h" #include #include #include "failure-action.h" struct failure_definition { const char *name; failure_action_t action; }; /* * failure_definitions - known daemon reliability failures * * Most failures start in observe mode. Worker stall keeps the historical * deadman behavior by default: terminate the daemon so the service manager can * restart it. Later high-security configuration can choose fail-closed or * degraded actions here. */ static const struct failure_definition failure_definitions[] = { [FAILURE_REASON_QUEUE_FULL] = { "queue_full", FAILURE_ACTION_OBSERVE }, [FAILURE_REASON_KERNEL_QUEUE_OVERFLOW] = { "kernel_queue_overflow", FAILURE_ACTION_OBSERVE }, [FAILURE_REASON_WORKER_STALL] = { "worker_stall", FAILURE_ACTION_TERMINATE }, [FAILURE_REASON_RULE_RELOAD_FAILURE] = { "rule_reload_failure", FAILURE_ACTION_OBSERVE }, [FAILURE_REASON_TRUST_RELOAD_FAILURE] = { "trust_reload_failure", FAILURE_ACTION_OBSERVE }, [FAILURE_REASON_RESPONSE_WRITE_FAILURE] = { "response_write_failure", FAILURE_ACTION_OBSERVE }, [FAILURE_REASON_FANOTIFY_FS_ERROR] = { "fanotify_filesystem_error", FAILURE_ACTION_OBSERVE }, }; static atomic_ulong failure_counts[FAILURE_REASON_MAX]; /* * failure_reason_valid - determine whether a reason enum is known. * @reason: failure reason supplied by a caller. * Returns 1 for a valid reason and 0 otherwise. */ static int failure_reason_valid(failure_reason_t reason) { unsigned int index = (unsigned int)reason; return index < FAILURE_REASON_MAX && failure_definitions[index].name != NULL; } /* * failure_reason_name - return the stable metric name for a failure. * @reason: failure reason supplied by a caller. * Returns the stable reason name or "unknown". */ const char *failure_reason_name(failure_reason_t reason) { if (!failure_reason_valid(reason)) return "unknown"; return failure_definitions[reason].name; } /* * failure_reason_action - return the current configured failure action. * @reason: failure reason supplied by a caller. * * Today all reasons observe only. Keeping this query separate from the counter * lets later configuration change behavior without touching every call site. * * Returns the configured action for @reason. */ failure_action_t failure_reason_action(failure_reason_t reason) { if (!failure_reason_valid(reason)) return FAILURE_ACTION_OBSERVE; return failure_definitions[reason].action; } /* * failure_action_name - return the stable name for a failure action. * @action: action enum to describe. * Returns the action name or "unknown". */ const char *failure_action_name(failure_action_t action) { switch (action) { case FAILURE_ACTION_OBSERVE: return "observe"; case FAILURE_ACTION_TERMINATE: return "terminate"; } return "unknown"; } /* * failure_action_execute - apply the configured action for one failure. * @reason: failure reason whose action should run. * * The caller records and logs the failure before calling this helper. The * terminate action intentionally uses SIGKILL to preserve the old deadman * behavior when a decision worker stalls inside uninterruptible work. * * Returns nothing. */ void failure_action_execute(failure_reason_t reason) { switch (failure_reason_action(reason)) { case FAILURE_ACTION_OBSERVE: return; case FAILURE_ACTION_TERMINATE: raise(SIGKILL); return; } } /* * failure_action_record - count one observed daemon failure. * @reason: failure reason to increment. * * Returns the new counter value for valid reasons, 0 for unknown reasons. */ unsigned long failure_action_record(failure_reason_t reason) { if (!failure_reason_valid(reason)) return 0; return atomic_fetch_add_explicit(&failure_counts[reason], 1, memory_order_relaxed) + 1; } /* * failure_action_count - return the count for one failure reason. * @reason: failure reason to read. * Returns the current counter value, or 0 for unknown reasons. */ unsigned long failure_action_count(failure_reason_t reason) { if (!failure_reason_valid(reason)) return 0; return atomic_load_explicit(&failure_counts[reason], memory_order_relaxed); } /* * failure_action_snapshot - copy failure counters, optionally resetting them. * @metrics: destination metrics snapshot. * @reset: non-zero resets counters after copying them. * Returns nothing. */ void failure_action_snapshot(failure_action_metrics_t *metrics, int reset) { failure_reason_t reason; if (metrics == NULL) return; for (reason = 0; reason < FAILURE_REASON_MAX; reason++) { if (reset) metrics->counts[reason] = atomic_exchange_explicit( &failure_counts[reason], 0, memory_order_relaxed); else metrics->counts[reason] = failure_action_count(reason); } } /* * failure_action_metrics_count - read one value from a metrics snapshot. * @metrics: metrics returned by failure_action_snapshot(). * @reason: failure reason to read. * Returns the snapshot value, or 0 for unknown reasons. */ unsigned long failure_action_metrics_count( const failure_action_metrics_t *metrics, failure_reason_t reason) { if (metrics == NULL || !failure_reason_valid(reason)) return 0; return metrics->counts[reason]; } /* * failure_action_metrics_report - print failure action metric snapshot. * @f: report stream. * @metrics: metrics returned by failure_action_snapshot(). * Returns nothing. */ void failure_action_metrics_report(FILE *f, const failure_action_metrics_t *metrics) { failure_reason_t reason; if (f == NULL || metrics == NULL) return; for (reason = 0; reason < FAILURE_REASON_MAX; reason++) fprintf(f, "Failure action %s (%s): %lu\n", failure_reason_name(reason), failure_action_name(failure_reason_action(reason)), failure_action_metrics_count(metrics, reason)); } /* * failure_action_report - print current failure action counters. * @f: report stream. * Returns nothing. */ void failure_action_report(FILE *f) { failure_action_metrics_t metrics; failure_action_snapshot(&metrics, 0); failure_action_metrics_report(f, &metrics); } fapolicyd-2.0/src/library/failure-action.h000066400000000000000000000030631524071644000206400ustar00rootroot00000000000000/* * failure-action.h - internal failure action model * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef FAILURE_ACTION_HEADER #define FAILURE_ACTION_HEADER #include typedef enum { FAILURE_REASON_QUEUE_FULL, FAILURE_REASON_KERNEL_QUEUE_OVERFLOW, FAILURE_REASON_WORKER_STALL, FAILURE_REASON_RULE_RELOAD_FAILURE, FAILURE_REASON_TRUST_RELOAD_FAILURE, FAILURE_REASON_RESPONSE_WRITE_FAILURE, FAILURE_REASON_FANOTIFY_FS_ERROR, FAILURE_REASON_MAX } failure_reason_t; typedef enum { FAILURE_ACTION_OBSERVE, FAILURE_ACTION_TERMINATE } failure_action_t; typedef struct { unsigned long counts[FAILURE_REASON_MAX]; } failure_action_metrics_t; const char *failure_reason_name(failure_reason_t reason); failure_action_t failure_reason_action(failure_reason_t reason); const char *failure_action_name(failure_action_t action); unsigned long failure_action_record(failure_reason_t reason); unsigned long failure_action_count(failure_reason_t reason); void failure_action_execute(failure_reason_t reason); void failure_action_snapshot(failure_action_metrics_t *metrics, int reset); unsigned long failure_action_metrics_count( const failure_action_metrics_t *metrics, failure_reason_t reason); void failure_action_metrics_report(FILE *f, const failure_action_metrics_t *metrics); void failure_action_report(FILE *f); #endif fapolicyd-2.0/src/library/fapolicyd-backend.h000066400000000000000000000105571524071644000213030ustar00rootroot00000000000000/* * fapolicyd-backend.h - Header file for database backend interface * Copyright (c) 2020-23 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka */ #ifndef FAPOLICYD_BACKEND_HEADER #define FAPOLICYD_BACKEND_HEADER #include #include #include #include "conf.h" #include "file.h" // If this gets extended, please put the new items at the end. typedef enum { SRC_UNKNOWN, SRC_RPM, SRC_FILE_DB, SRC_DEB } trust_src_t; /* * Trust DB payload format * * Each payload stores "source size digest" as text. The size is a fixed * unsigned 64-bit byte count, rather than native off_t or size_t. This keeps * the on-disk decimal format independent of the process ABI: a 32-bit build * with large-file support must parse the same record as a 64-bit build. * * Keep native filesystem values as off_t and allocation lengths as size_t. * Convert only at this serialization boundary, and reject a stored value that * cannot be represented by the native off_t needed for a stat comparison. */ typedef uint64_t trustdb_size_t; /* UINT64_MAX has 20 decimal digits. */ #define TRUSTDB_SIZE_DECIMAL_DIGITS 20 // Do not pad the hash value so SHA1 and SHA256 digests parse correctly. // The input form includes a digest width for scanf's destination buffer. #define STR_IMPL(x) #x #define STR(x) STR_IMPL(x) #define DATA_FORMAT "%u %" PRIu64 " %s" #define DATA_FORMAT_IN "%u %" SCNu64 " %" STR(FILE_DIGEST_STRING_WIDTH) "s" /* * trustdb_size_from_signed - convert a nonnegative native file size. * @size: Native signed file size or package size. * @stored_size: Receives the serialized trust DB byte count. * * Returns 0 on success or 1 when @size is negative or too wide for the * wire format. */ static inline int trustdb_size_from_signed(intmax_t size, trustdb_size_t *stored_size) { if (size < 0 || (uintmax_t)size > UINT64_MAX) return 1; *stored_size = (trustdb_size_t)size; return 0; } /* * trustdb_size_from_unsigned - convert an unsigned in-memory size. * @size: Native unsigned file or buffer size. * @stored_size: Receives the serialized trust DB byte count. * * Returns 0 on success or 1 when @size exceeds the wire-format range. */ static inline int trustdb_size_from_unsigned(uintmax_t size, trustdb_size_t *stored_size) { if (size > UINT64_MAX) return 1; *stored_size = (trustdb_size_t)size; return 0; } /* * trustdb_size_to_off_t - convert a stored size for filesystem comparison. * @stored_size: Serialized trust DB byte count. * @size: Receives the native off_t value. * * A trust record can be valid on a wider ABI but not representable by a * narrower off_t. The checked round trip rejects that record instead of * truncating it before comparison. * * Returns 0 on success or 1 when @stored_size does not fit in off_t. */ static inline int trustdb_size_to_off_t(trustdb_size_t stored_size, off_t *size) { off_t native_size; if (stored_size > INTMAX_MAX) return 1; native_size = (off_t)(intmax_t)stored_size; if (native_size < 0 || (trustdb_size_t)(intmax_t)native_size != stored_size) return 1; *size = native_size; return 0; } typedef struct _backend { const char * name; int (*init)(void); int (*load)(const conf_t *); int (*close)(void); int memfd; long entries; } backend; extern backend file_backend; #ifdef USE_RPM extern backend rpm_backend; int rpm_backend_load_from_path_for_tests(const conf_t *conf, const char *loader_path, int timeout_ms) __nonnull ((1, 2)); #endif #ifdef USE_DEB extern backend deb_backend; int deb_backend_load_from_path_for_tests(const conf_t *conf, const char *loader_path, int timeout_ms) __nonnull ((1, 2)); #endif #endif fapolicyd-2.0/src/library/fapolicyd-defs.h000066400000000000000000000020541524071644000206260ustar00rootroot00000000000000/* * fapolicyd-defs.h - Header file for defines & enums that cause loops * Copyright (c) 2019 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef FAPOLICYD_DEFS_HEADER #define FAPOLICYD_DEFS_HEADER typedef enum { OPEN_ACC, EXEC_ACC , ANY_ACC } access_t; typedef enum { RULE_FMT_ORIG, RULE_FMT_COLON } rformat_t; #endif fapolicyd-2.0/src/library/fd-fgets.c000066400000000000000000000206141524071644000174310ustar00rootroot00000000000000/* audit-fgets.c -- a replacement for glibc's fgets * Copyright 2018,2022,2025 Red Hat Inc. * All Rights Reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Authors: * Steve Grubb */ #include "config.h" #include #include #include #include #include #include #include "fd-fgets.h" /* * The theory of operation for this family of functions is that it * operates like the glibc fgets function except with a descriptor. * It reads from the descriptor into a buffer and then looks through * the buffer to find a string terminated with a '\n'. It terminates * the string with a 0 and returns it. It updates current to point * to where it left off. On the next read it starts there and tries to * find a '\n'. If it can't find one, it advances the buffer pointer * and only compacts the unread data when there is no room left for * the next read. If the descriptor becomes invalid or there is an * error reading, it returns -1 without changing state or the caller buffer. * The variable eptr marks the end of the buffer. It never changes. */ #define BUF_SIZE 8192 struct fd_fgets_state { char internal[2*BUF_SIZE+1]; char *buffer; char *current; char *eptr; char *orig; int eof; enum fd_mem mem_type; size_t buff_size; }; static struct fd_fgets_state global_state; static int global_init_done; static void fd_fgets_state_init(struct fd_fgets_state *st) { st->buffer = st->internal; st->internal[0] = '\0'; st->current = st->buffer; st->eptr = st->buffer + (2*BUF_SIZE); st->orig = st->buffer; st->eof = 0; st->mem_type = MEM_SELF_MANAGED; st->buff_size = 2*BUF_SIZE; } struct fd_fgets_state *fd_fgets_init(void) { struct fd_fgets_state *st = malloc(sizeof(*st)); if (st) fd_fgets_state_init(st); return st; } void fd_fgets_destroy(struct fd_fgets_state *st) { if (st->buffer != st->internal || st->orig != st->internal) { switch (st->mem_type) { case MEM_MALLOC: free(st->orig); break; case MEM_MMAP: case MEM_MMAP_FILE: munmap(st->orig, st->buff_size); break; case MEM_SELF_MANAGED: default: break; } } free(st); } int fd_fgets_eof_r(struct fd_fgets_state *st) { return st->eof; } /* This function dumps any accumulated text. This is to remove dangling text * that never got consumed for the intended purpose. */ void fd_fgets_clear_r(struct fd_fgets_state *st) { // For MEM_MMAP_FILE, it effectively rewinds the buffer making the // whole buffer available again. This is different than all others // because we can't just dump a file. if (st->mem_type == MEM_MMAP_FILE) { st->buffer = st->orig; st->current = st->eptr; } else { st->buffer = st->orig; st->buffer[0] = 0; st->current = st->buffer; } st->eof = 0; } /* Function to check if we have more data stored * and ready to process. If we have a newline or enough * bytes we return 1 for success. Otherwise 0 meaning that * there is not enough to process without blocking. */ int fd_fgets_more_r(struct fd_fgets_state *st, size_t blen) { size_t avail; char *nl; assert(blen != 0); avail = st->current - st->buffer; /* only scan the valid region */ nl = memchr(st->buffer, '\n', avail); return (nl || avail >= blen - 1); } /* Function to read the next chunk of data from the given fd. If we have * data to return, we Read up to blen-1 chars (or through the next newline), * copy into buf, NUL-terminate, and return the number of chars. * It also returns 0 for no data. And -1 if there was an error reading * the fd. */ int fd_fgets_r(struct fd_fgets_state *st, char *buf, size_t blen, int fd) { size_t avail = st->current - st->buffer, line_len; char *line_end; ssize_t nread; assert(blen != 0); /* 1) Is there already a '\n' in the buffered data? */ line_end = memchr(st->buffer, '\n', avail); /* 2) If not, and we still can read more, pull in more data */ if (line_end == NULL && !st->eof) { if (st->mem_type != MEM_MMAP_FILE && st->current == st->eptr && st->buffer != st->orig) { size_t used = (size_t)(st->current - st->buffer); memmove(st->orig, st->buffer, used); st->buffer = st->orig; st->current = st->buffer + used; avail = used; *st->current = '\0'; } if (st->current != st->eptr) { do { nread = read(fd, st->current, st->eptr - st->current); } while (nread < 0 && errno == EINTR); if (nread < 0) return -1; if (nread == 0) st->eof = 1; else { size_t got = (size_t)nread; st->current[got] = '\0'; st->current += got; avail += got; } /* see if a newline arrived in that chunk */ line_end = memchr(st->buffer, '\n', avail); } } /* 3) Do we now have enough to return? */ if (line_end == NULL) { /* not a full line—only return early if we still expect more */ if (!st->eof && avail < blen - 1 && st->current != st->eptr) return 0; /* else we’ll return whatever we have (either at EOF, * buffer‑full, or enough for blen) */ } /* 4) Compute how many chars to hand back */ if (line_end) { /* include the '\n', but never exceed blen-1 */ line_len = (line_end - st->buffer) + 1; if (line_len > blen - 1) line_len = blen - 1; } else /* no newline: return up to blen-1 or whatever’s left * at EOF/full */ line_len = (avail < blen - 1) ? avail : (blen - 1); /* 5) Copy out, slide the remainder down, reset pointers */ memcpy(buf, st->buffer, line_len); buf[line_len] = '\0'; size_t remainder = avail - line_len; /* For MEM_MMAP_FILE we advance over the returned data permanently. * For other modes we defer compaction until there is no write room * left for the next read. */ if (st->mem_type == MEM_MMAP_FILE) { st->buffer += line_len; if (st->buffer >= st->eptr) st->eof = 1; } else { st->buffer += line_len; } st->current = st->buffer + remainder; if (st->mem_type != MEM_MMAP_FILE) *st->current = '\0'; return (int)line_len; } static inline void fd_fgets_ensure_global(void) { if (!global_init_done) { fd_fgets_state_init(&global_state); global_init_done = 1; } } int fd_fgets_eof(void) { fd_fgets_ensure_global(); return fd_fgets_eof_r(&global_state); } void fd_fgets_clear(void) { fd_fgets_ensure_global(); fd_fgets_clear_r(&global_state); } int fd_fgets_more(size_t blen) { fd_fgets_ensure_global(); return fd_fgets_more_r(&global_state, blen); } int fd_fgets(char *buf, size_t blen, int fd) { fd_fgets_ensure_global(); return fd_fgets_r(&global_state, buf, blen, fd); } int fd_setvbuf_r(struct fd_fgets_state *st, void *buf, size_t buff_size, enum fd_mem how) { /* MEM_SELF_MANAGED means "use st->internal" and is only configured by * fd_fgets_state_init(). Rejecting it here avoids mismatched ownership * when callers pass external buffers. */ if (st == NULL || buf == NULL || buff_size == 0 || how == MEM_SELF_MANAGED) return 1; st->buffer = buf; st->orig = buf; if (how == MEM_MMAP_FILE) /* Setting st->current to the end of the supplied mmap region * is done so that auplugin_fgets_r sees the buffer as already * filled with buff_size bytes of data and there is no space * left for additional reads. This prevents any read() calls. */ st->current = (char *)buf + buff_size; else st->current = st->buffer; /* Reserve one byte for NUL termination when reads append to writable * caller-supplied buffers. MEM_MMAP_FILE is read-only pre-populated data * and uses the full mapped span as a content boundary. */ if (how == MEM_MALLOC || how == MEM_MMAP) st->eptr = st->buffer + buff_size - 1; else st->eptr = st->buffer + buff_size; st->eof = 0; st->mem_type = how; st->buff_size = buff_size; return 0; } int fd_setvbuf(void *buf, size_t buff_size, enum fd_mem how) { fd_fgets_ensure_global(); return fd_setvbuf_r(&global_state, buf, buff_size, how); } fapolicyd-2.0/src/library/fd-fgets.h000066400000000000000000000042631524071644000174400ustar00rootroot00000000000000/* fd-fgets.h -- a replacement for glibc's fgets * Copyright 2019,2020,2022,2025 Red Hat Inc. * All Rights Reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Authors: * Steve Grubb */ #ifndef FD_FGETS_HEADER #define FD_FGETS_HEADER #include #include "gcc-attributes.h" typedef struct fd_fgets_state fd_fgets_state_t; enum fd_mem { MEM_SELF_MANAGED, /* Internal buffer owned by fd-fgets. */ MEM_MALLOC, /* User-supplied malloc buffer; fd-fgets frees it. */ MEM_MMAP, /* User-supplied mmap buffer; fd-fgets munmaps it. */ MEM_MMAP_FILE /* Read-only mmap file data; fd-fgets munmaps it. */ }; void fd_fgets_clear(void); int fd_fgets_eof(void); int fd_fgets_more(size_t blen); int fd_fgets(char *buf, size_t blen, int fd) __attr_access ((__write_only__, 1, 2)) __attr_fd_arg_read (3) __wur; int fd_setvbuf(void *buf, size_t buff_size, enum fd_mem how) __attr_access ((__read_only__, 1, 2)); void fd_fgets_destroy(fd_fgets_state_t *st); fd_fgets_state_t *fd_fgets_init(void) __attribute_malloc__ __attr_dealloc (fd_fgets_destroy, 1); void fd_fgets_clear_r(fd_fgets_state_t *st); int fd_fgets_eof_r(fd_fgets_state_t *st); int fd_fgets_more_r(fd_fgets_state_t *st, size_t blen); int fd_fgets_r(fd_fgets_state_t *st, char *buf, size_t blen, int fd) __attr_access ((__write_only__, 2, 3)) __attr_fd_arg_read (4) __wur; int fd_setvbuf_r(fd_fgets_state_t *st, void *buf, size_t buff_size, enum fd_mem how) __attr_access ((__read_only__, 2, 3)); #endif fapolicyd-2.0/src/library/file-backend.c000066400000000000000000000041531524071644000202360ustar00rootroot00000000000000/* * file-backend.c - file backend * Copyright (c) 2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka * Steve Grubb * Zoltan Fridrich */ #include "config.h" #include #include #include #include #include #include #include "fapolicyd-backend.h" #include "message.h" #include "trust-file.h" static int file_init_backend(void); static int file_load_list(const conf_t *conf); static int file_destroy_backend(void); backend file_backend = { "file", file_init_backend, file_load_list, file_destroy_backend, -1, -1, }; static int file_load_list(const conf_t *conf) { msg(LOG_DEBUG, "Loading file backend"); int memfd = memfd_create("file_snapshot", MFD_CLOEXEC | MFD_ALLOW_SEALING); file_backend.memfd = memfd; file_backend.entries = -1; if (memfd < 0) { msg(LOG_ERR, "memfd_create failed for file backend (%s)", strerror(errno)); return 1; } trust_file_load_all(NULL, memfd); /* Seal the snapshot so readers see a stable view. */ if (fcntl(memfd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE) == -1) // Not a fatal error msg(LOG_WARNING, "Failed to seal file backend memfd (%s)", strerror(errno)); return 0; } static int file_init_backend(void) { return 0; } static int file_destroy_backend(void) { return 0; } fapolicyd-2.0/src/library/file.c000066400000000000000000001400451524071644000166520ustar00rootroot00000000000000/* * file.c - functions for accessing attributes of files * Copyright (c) 2016,2018-23 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "file.h" #include "database.h" #include "decision-context.h" #include "decision-timing.h" #include "paths.h" #include "message.h" #include "process.h" // For elf info bit mask #include "string-util.h" // Local defines #define IMA_XATTR_DIGEST_NG 0x04 // security/integrity/integrity.h // Local declarations static ssize_t safe_read(int fd, char *buf, size_t size) __attr_access ((__write_only__, 2, 3)); static char *get_program_cwd_from_pid(pid_t pid, size_t blen, char *buf) __attr_access ((__write_only__, 3, 2)); static void resolve_path(const char *pcwd, char *path, size_t len) __attr_access ((__write_only__, 2, 3)); static file_init_status_t file_init_failure; // readelf -l path-to-app | grep 'Requesting' | cut -d':' -f2 | tr -d ' ]'; static const char *interpreters[] = { "/lib64/ld-linux-x86-64.so.2", "/lib/ld-linux.so.2", // i686 "/usr/lib64/ld-linux-x86-64.so.2", "/usr/lib/ld-linux.so.2", // i686 "/lib/ld.so.2", "/lib/ld-linux-armhf.so.3", // fedora armv7hl "/lib/ld-linux-aarch64.so.1", // fedora aarch64 "/lib/ld64.so.1", // rhel8 s390x "/lib64/ld64.so.2", // rhel8 ppc64le }; #define MAX_INTERPS (sizeof(interpreters)/sizeof(interpreters[0])) // Define a convience function to rewind a descriptor to the beginning static inline void rewind_fd(int fd) { lseek(fd, 0, SEEK_SET); } // RHEL 8 does not have this define. Remove when RHEL 8 is EOL. #ifndef MAGIC_NO_CHECK_JSON #define MAGIC_NO_CHECK_JSON 0 #endif // Initialize what we can now so that its not done each call int file_init(void) { struct decision_context *ctx = decision_context_current(); decision_context_close_file_helpers(ctx); // Setup udev ctx->file_udev = udev_new(); // Setup libmagic unsetenv("MAGIC"); if (file_init_failure == FILE_INIT_MAGIC_FAST_OPEN_FAILED) { msg(LOG_ERR, "Unable to init fast libmagic"); decision_context_close_file_helpers(ctx); return FILE_INIT_MAGIC_FAST_OPEN_FAILED; } // Fast magic: minimal rules, all expensive checks disabled ctx->magic_fast = magic_open( MAGIC_MIME | MAGIC_ERROR | MAGIC_NO_CHECK_CDF | MAGIC_NO_CHECK_ELF | MAGIC_NO_CHECK_COMPRESS | /* Don't decompress */ MAGIC_NO_CHECK_TAR | MAGIC_NO_CHECK_APPTYPE | MAGIC_NO_CHECK_TOKENS | /* Skip text tokens */ MAGIC_NO_CHECK_JSON /* Skip JSON validation */ ); if (ctx->magic_fast == NULL) { msg(LOG_ERR, "Unable to init fast libmagic"); decision_context_close_file_helpers(ctx); return FILE_INIT_MAGIC_FAST_OPEN_FAILED; } // Load only essential magic rules if (file_init_failure == FILE_INIT_MAGIC_FAST_LOAD_FAILED) { msg(LOG_ERR, "Unable to load fast magic database"); decision_context_close_file_helpers(ctx); return FILE_INIT_MAGIC_FAST_LOAD_FAILED; } if (magic_load(ctx->magic_fast, MAGIC_PATH) != 0) { msg(LOG_ERR, "Unable to load fast magic database"); decision_context_close_file_helpers(ctx); return FILE_INIT_MAGIC_FAST_LOAD_FAILED; } // Full magic: normal operation if (file_init_failure == FILE_INIT_MAGIC_FULL_OPEN_FAILED) { msg(LOG_ERR, "Unable to init full libmagic"); decision_context_close_file_helpers(ctx); return FILE_INIT_MAGIC_FULL_OPEN_FAILED; } ctx->magic_full = magic_open(MAGIC_MIME|MAGIC_ERROR|MAGIC_NO_CHECK_CDF| MAGIC_NO_CHECK_ELF); if (ctx->magic_full == NULL) { msg(LOG_ERR, "Unable to init full libmagic"); decision_context_close_file_helpers(ctx); return FILE_INIT_MAGIC_FULL_OPEN_FAILED; } // System default if (file_init_failure == FILE_INIT_MAGIC_FULL_LOAD_FAILED) { msg(LOG_ERR, "Unable to load default magic database"); decision_context_close_file_helpers(ctx); return FILE_INIT_MAGIC_FULL_LOAD_FAILED; } if (magic_load(ctx->magic_full, NULL) != 0) { msg(LOG_ERR, "Unable to load default magic database"); decision_context_close_file_helpers(ctx); return FILE_INIT_MAGIC_FULL_LOAD_FAILED; } return FILE_INIT_OK; } // Release memory during shutdown void file_close(void) { decision_context_close_file_helpers(decision_context_current()); } /* * file_init_failure_for_tests - request one file_init() fault injection. * @failure: file_init_status_t value to return, or FILE_INIT_OK to disable. * * Returns nothing. */ void file_init_failure_for_tests(file_init_status_t failure) { file_init_failure = failure; } /* * file_hash_length - return the binary digest size for the algorithm. * @alg: file digest algorithm to query. * Returns the digest length in bytes, or 0 when the algorithm is unknown. */ size_t file_hash_length(file_hash_alg_t alg) { switch (alg) { case FILE_HASH_ALG_MD5: return MD5_LEN; case FILE_HASH_ALG_SHA1: return SHA1_LEN; case FILE_HASH_ALG_SHA256: return SHA256_LEN; case FILE_HASH_ALG_SHA512: return SHA512_LEN; default: break; } return 0; } /* * file_hash_alg - return the algorith for the digest size. * @len: the digest length to query. * Returns the digest algorithm. */ file_hash_alg_t file_hash_alg(unsigned len) { // Ordered most probable to least likely switch (len) { case SHA256_LEN * 2: return FILE_HASH_ALG_SHA256; case SHA512_LEN * 2: return FILE_HASH_ALG_SHA512; case MD5_LEN * 2: return FILE_HASH_ALG_MD5; case SHA1_LEN * 2: return FILE_HASH_ALG_SHA1; } return FILE_HASH_ALG_NONE; } /* * file_hash_alg_fast - return the algorith for the digest size * O(1) – no strlen, no scanning * @digest: the digest to query. * Returns the digest algorithm. */ file_hash_alg_t file_hash_alg_fast(const char *digest) { /* cautious access: check shorter offsets first */ if (!digest) return FILE_HASH_ALG_NONE; /* MD5 is 32 hex chars */ if (!digest[MD5_LEN*2]) return FILE_HASH_ALG_MD5; /* SHA1 is 40 hex chars */ if (!digest[SHA1_LEN*2]) return FILE_HASH_ALG_SHA1; /* SHA-256 is 64 hex chars */ if (!digest[SHA256_LEN*2]) return FILE_HASH_ALG_SHA256; /* SHA-512 is 128 hex chars */ if (!digest[SHA512_LEN*2]) return FILE_HASH_ALG_SHA512; return FILE_HASH_ALG_NONE; } /* * file_info_reset_digest - clear cached digest metadata for a file record. * @info: cached file entry to sanitize. */ void file_info_reset_digest(struct file_info *info) { if (info == NULL) return; info->digest_alg = FILE_HASH_ALG_NONE; info->digest[0] = 0; } /* * file_info_cache_digest - persist digest metadata alongside cached files. * @info: cached file entry to update. * @alg: algorithm used to generate the cached digest. * The binary digest length can be derived from file_hash_length(@alg) on * demand, so it is not cached alongside the algorithm selection. */ void file_info_cache_digest(struct file_info *info, file_hash_alg_t alg) { if (info == NULL) return; info->digest_alg = alg; } static const char *hash_prefixes[] = { NULL, // FILE_HASH_ALG_NONE "md5", "sha1", "sha256", "sha512", }; /* * ima_algo_desc - Associate kernel IMA identifiers with local hashing enums. * @ima_alg: Algorithm identifier stored in the IMA digest-ng header. * @alg: Local file hashing algorithm used when recomputing the digest. * @digest_len: Binary digest length for @alg. */ struct ima_algo_desc { uint8_t ima_alg; file_hash_alg_t alg; size_t digest_len; }; static const struct ima_algo_desc ima_algo_map[] = { { HASH_ALGO_MD5, FILE_HASH_ALG_MD5, MD5_LEN }, { HASH_ALGO_SHA1, FILE_HASH_ALG_SHA1, SHA1_LEN }, { HASH_ALGO_SHA256, FILE_HASH_ALG_SHA256, SHA256_LEN }, { HASH_ALGO_SHA512, FILE_HASH_ALG_SHA512, SHA512_LEN }, }; /* * ima_lookup_algo - Translate an IMA digest-ng algorithm id to local metadata. * @ima_id: Numeric algorithm encoded in the xattr header. * Returns a pointer to the mapped description, or NULL when unsupported. */ static const struct ima_algo_desc *ima_lookup_algo(uint8_t ima_id) { unsigned int i; for (i = 0; i < (sizeof(ima_algo_map)/sizeof(ima_algo_map[0])); i++) { if (ima_algo_map[i].ima_alg == ima_id) return &ima_algo_map[i]; } return NULL; } const char *file_hash_alg_name(file_hash_alg_t alg) { unsigned value = alg; if (alg > FILE_HASH_ALG_SHA512) return NULL; return hash_prefixes[value]; } file_hash_alg_t file_hash_name_alg(const char *name) { if (name == NULL || name[0] == 0) return FILE_HASH_ALG_NONE; if (name[0] == 'm') return FILE_HASH_ALG_MD5; if (name[3] == '1') return FILE_HASH_ALG_SHA1; if (name[3] == '2') return FILE_HASH_ALG_SHA256; if (name[3] == '5') return FILE_HASH_ALG_SHA512; return FILE_HASH_ALG_NONE; } /* * stat_file_entry - populate a cached description of an open descriptor. * @fd: descriptor to stat for cache metadata. * Returns an allocated struct file_info on success, otherwise NULL. */ struct file_info *stat_file_entry(int fd) { struct stat sb; if (fstat(fd, &sb) == 0) { struct file_info *info = malloc(sizeof(struct file_info)); if (info == NULL) return info; info->device = sb.st_dev; info->inode = sb.st_ino; info->mode = sb.st_mode; info->size = sb.st_size; // Try to get the modified time. If its zero, then it // hasn't been modified. Revert to create time if no // modifications have been done. if (sb.st_mtim.tv_sec) info->time.tv_sec = sb.st_mtim.tv_sec; else info->time.tv_sec = sb.st_ctim.tv_sec; if (sb.st_mtim.tv_nsec) info->time.tv_nsec = sb.st_mtim.tv_nsec; else info->time.tv_nsec = sb.st_ctim.tv_nsec; file_info_reset_digest(info); return info; } return NULL; } /* * open_verified_regular_file - open a regular file matching prior stat data. * @path: pathname to open. * @expected: stat data collected before reopening @path. * @opened: receives stat data for the opened descriptor. * * O_NOFOLLOW and O_NONBLOCK prevent a replacement symlink or FIFO from * redirecting or stalling the caller. Comparing the opened inode to the * prior stat result rejects a pathname replacement before inspection. * * Returns an owned descriptor on success, or -1 with errno set on failure. */ int open_verified_regular_file(const char *path, const struct stat *expected, struct stat *opened) { int fd, saved_errno; fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK); if (fd < 0) return -1; if (fstat(fd, opened)) { saved_errno = errno; goto error; } if (S_ISREG(opened->st_mode) == 0 || opened->st_dev != expected->st_dev || opened->st_ino != expected->st_ino) { saved_errno = ESTALE; goto error; } return fd; error: close(fd); errno = saved_errno; return -1; } // Returns 0 if equal and 1 if not equal int compare_file_infos(const struct file_info *p1, const struct file_info *p2) { if (p1 == NULL || p2 == NULL) return 1; /* Digest metadata is advisory and excluded from equality checks. */ // Compare in the order to find likely mismatch first //msg(LOG_DEBUG, "inode %ld %ld", p1->inode, p2->inode); if (p1->inode != p2->inode) { //msg(LOG_DEBUG, "mismatch INODE"); return 1; } if (p1->time.tv_nsec != p2->time.tv_nsec) { //msg(LOG_DEBUG, "mismatch NANO"); return 1; } if (p1->time.tv_sec != p2->time.tv_sec) { //msg(LOG_DEBUG, "mismatch SEC"); return 1; } if (p1->size != p2->size) { //msg(LOG_DEBUG, "mismatch BLOCKS"); return 1; } if (p1->device != p2->device) { //msg(LOG_DEBUG, "mismatch DEV"); return 1; } return 0; } /* * check_ignore_mount_noexec - ensure an ignored mount has the noexec flag. * @mounts_file: path to the mount table used to validate the entry. * @point: mount point path to examine. * Returns 1 when the mount exists and has noexec, 0 when the mount is present * but missing the flag, -1 when the mount point is not found, and -2 if the * mount table cannot be read. */ int check_ignore_mount_noexec(const char *mounts_file, const char *point) { FILE *fp; struct mntent *ent; int found = 0; fp = setmntent(mounts_file, "r"); if (fp == NULL) { msg(LOG_ERR, "Cannot read %s (%s)", mounts_file, strerror(errno)); return -2; } while ((ent = getmntent(fp))) { if (strcmp(ent->mnt_dir, point) == 0) { found = 1; if (hasmntopt(ent, "noexec")) { endmntent(fp); return 1; } break; } } endmntent(fp); if (!found) return -1; return 0; } /* * iterate_ignore_mounts - walk through ignore_mounts entries and invoke a callback. * @ignore_list: comma separated list of mount points to process. * @callback: function invoked for each trimmed entry. * @user_data: opaque pointer passed to the callback on each invocation. * Returns 0 on success, 1 when memory allocation fails, or the first non-zero * value returned by the callback. */ int iterate_ignore_mounts(const char *ignore_list, int (*callback)(const char *mount, void *user_data), void *user_data) { char *ptr, *saved, *tmp; if (ignore_list == NULL || callback == NULL) return 0; tmp = strdup(ignore_list); if (tmp == NULL) return 1; ptr = strtok_r(tmp, ",", &saved); while (ptr) { char *mount = fapolicyd_strtrim(ptr); if (*mount) { int rc = callback(mount, user_data); if (rc) { free(tmp); return rc; } } ptr = strtok_r(NULL, ",", &saved); } free(tmp); return 0; } /* * check_ignore_mount_warning - obtain shared warning text for ignore_mounts. * @mounts_file: path to the mount table used to validate entries. * @point: mount point path to examine. * @warning: updated with standardized warning text or NULL when not needed. * Returns the same codes as check_ignore_mount_noexec. */ int check_ignore_mount_warning(const char *mounts_file, const char *point, const char **warning) { int rc; static const char warn_noexec[] = "ignore_mounts entry %1$s must be mounted noexec - it will be watched"; static const char warn_missing[] = "ignore_mounts entry %1$s is not present in %2$s - it will be watched"; static const char warn_unknown[] = "Cannot determine mount options for %1$s - it will be watched"; rc = check_ignore_mount_noexec(mounts_file, point); if (warning) *warning = NULL; if (warning && rc != 1) { if (rc == 0) *warning = warn_noexec; else if (rc == -1) *warning = warn_missing; else if (rc < -1) *warning = warn_unknown; } return rc; } static char *get_program_cwd_from_pid(pid_t pid, size_t blen, char *buf) { char path[32]; ssize_t path_len; snprintf(path, sizeof(path), "/proc/%d/cwd", pid); path_len = readlink(path, buf, blen - 1); if (path_len < 0) return NULL; if ((size_t)path_len < blen) buf[path_len] = 0; else buf[blen-1] = 0; return buf; } // If we had to build a path because it started out relative, // then put the pieces together and get the conanical name static void resolve_path(const char *pcwd, char *path, size_t len) { char tpath[PATH_MAX+1]; int tlen = strlen(pcwd); // Start with current working directory strncpy(tpath, pcwd, PATH_MAX); if (tlen >= PATH_MAX) { tlen=PATH_MAX-1; tpath[PATH_MAX] = 0; } // Add the relative path strncat(tpath, path, (PATH_MAX-1) - tlen); tpath[PATH_MAX] = 0; // Ask for it to be resolved if (realpath(tpath, path) == NULL) { strncpy(path, tpath, len); path[len - 1] = 0; } } char *get_file_from_fd(int fd, pid_t pid, size_t blen, char *buf) { char procfd_path[32]; ssize_t path_len; if (blen == 0) return NULL; snprintf(procfd_path, sizeof(procfd_path)-1, "/proc/self/fd/%d", fd); path_len = readlink(procfd_path, buf, blen - 1); if (path_len < 0) return NULL; if ((size_t)path_len < blen) buf[path_len] = 0; else buf[blen-1] = 0; // If this does not start with a '/' we have a relative path if (buf[0] != '/') { char pcwd[PATH_MAX+1]; pcwd[0] = 0; get_program_cwd_from_pid(pid, sizeof(pcwd), pcwd); resolve_path(pcwd, buf, blen); } return buf; } char *get_device_from_stat(unsigned int device, size_t blen, char *buf) { struct decision_context *ctx = decision_context_current(); struct file_device_cache *cache = &ctx->device_cache; struct udev_device *dev; const char *node; if (cache->device) { if (cache->device == device) { strncpy(buf, cache->devname, blen-1); buf[blen-1] = 0; return buf; } } if (ctx->file_udev == NULL) return NULL; // Create udev_device from the dev_t obtained from stat dev = udev_device_new_from_devnum(ctx->file_udev, 'b', device); if (dev == NULL) return NULL; node = udev_device_get_devnode(dev); if (node == NULL) { udev_device_unref(dev); return NULL; } strncpy(buf, node, blen-1); buf[blen-1] = 0; udev_device_unref(dev); // Update saved values free(cache->devname); cache->devname = strdup(buf); if (cache->devname) cache->device = device; else cache->device = 0; return buf; } const char *classify_elf_info(uint32_t elf, const char *path) { const char *ptr; if (elf & (HAS_ERROR | HAS_BAD_INTERP)) ptr = "application/x-bad-elf"; else if (elf & HAS_EXEC) ptr = "application/x-executable"; else if (elf & HAS_REL) ptr = "application/x-object"; else if (elf & HAS_CORE) ptr = "application/x-coredump"; else if (elf & HAS_INTERP) { // dynamic app ptr = "application/x-executable"; // libc and pthread actually have an interpreter?!? // Need to carve out an exception to reclassify them. const char *p = path; if (!strncmp(p, "/usr", 4)) p += 4; if (!strncmp(p, "/lib", 4)) { p += 4; if (!strncmp(p, "64", 2)) p += 2; if (!strncmp(p, "/libc-2", 7) || !strncmp(p, "/libc.so", 8) || !strncmp(p, "/libpthread-2", 13)) ptr = "application/x-sharedlib"; } } else { if (elf & HAS_DYNAMIC) { // shared obj if (elf & HAS_DEBUG) ptr = "application/x-executable"; else ptr = "application/x-sharedlib"; } else return NULL; } // TODO: add HAS_EXE_STACK and HAS_RWE_LOAD to // classify BAD_ELF based on system policy return ptr; } /* * This function classifies the descriptor if it's not a regular file. * This is needed because libmagic tries to read it and comes up with * application/x-empty instead. This function will return NULL if the * file is not a device. Otherwise a pointer to its mime type. */ const char *classify_device(mode_t mode) { const char *ptr = NULL; switch (mode & S_IFMT) { case S_IFCHR: ptr = "inode/chardevice"; break; case S_IFBLK: ptr = "inode/blockdevice"; break; case S_IFIFO: ptr = "inode/fifo"; break; case S_IFSOCK: ptr = "inode/socket"; break; } return ptr; } /* * Mime Type Detection Overview * ---------------------------- * * Determining a file's mime type is expensive when relying solely on libmagic. * Profiling showed libmagic spending ~43% of its time on text encoding * analysis even for files whose type could be determined from their first * few bytes. * * This code implements a tiered detection strategy that tries fast O(1) checks * before falling back to libmagic. A single pread() loads the file header * once; this buffer is reused across all detection stages: * * 1. Empty files - size == 0 returns application/x-empty immediately. * 2. ELF detection - gather_elf() classifies executables and libraries. * 3. Shebang scripts - extract_shebang_interpreter() + mime_from_shebang() * identify shell, python, perl, etc. by interpreter. * 4. Magic numbers - detect_by_magic_number() matches PNG, JPEG, gzip. * 5. Text formats - detect_text_format() catches HTML, XML, JSON. * 6. Two-tier libmagic - magic_fast (minimal rules) then magic_full if needed * * Shebang detection extracts the interpreter basename regardless of path * (/bin/sh, /usr/bin/env bash, /nix/store/.../python3 all work). Interpreter * matching uses suffix patterns (*sh catches bash/dash/zsh/fish/ksh) rather * than exact names to handle variants across distributions. * * Based on a Fedora system scan, this approach resolves ~98% of files without * a full libmagic lookup: ELF ~75%, shebang scripts ~16%, magic/text ~7%. */ // Hot function could benefit from aggressive optimization #pragma GCC push_options #pragma GCC optimize ("O3") /* * is_shebang_delim - check if character delimits shebang tokens * @ch: character to test * * Returns 1 if @ch is whitespace/newline used as token delimiter. */ static int is_shebang_delim(char ch) { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; } /* * is_env_assignment_token - check whether token is NAME=VALUE * @start: first character of token * @end: one past token end * * Returns 1 if token has '=' after at least one character, else 0. */ static int is_env_assignment_token(const char *start, const char *end) { const char *p; for (p = start; p < end; p++) if (*p == '=') return p > start; return 0; } /* * env_opt_needs_argument - whether env option consumes next token * @start: first character of token * @end: one past token end * * Returns 1 if the option consumes the next argument token. */ static int env_opt_needs_argument(const char *start, const char *end) { size_t len = end - start; if (len == 2 && start[0] == '-' && (start[1] == 'u' || start[1] == 'C')) return 1; if (len == 7 && memcmp(start, "--unset", 7) == 0) return 1; if (len == 7 && memcmp(start, "--chdir", 7) == 0) return 1; return 0; } /* * extract_shebang_interpreter - parse a shebang line to find interpreter * @data: pointer to file header data * @len: number of bytes available in @data * @buf: storage for the interpreter basename * @buflen: size of @buf * * Handles variations like: * #!/bin/sh * #!/usr/bin/bash * #!/usr/local/bin/python3 * #!/nix/store/abc123-python-3.11/bin/python3 * #!/usr/bin/env python3 * #!/usr/bin/env -S FOO=1 bash -eux * #!/bin/env -S python3 -u * Returns pointer to @buf with the interpreter basename (e.g., "bash", * "python3"), or NULL when no interpreter can be parsed. */ const char *extract_shebang_interpreter(const char *data, size_t len, char *buf, size_t buflen) { char line[256]; size_t n; char *p, *end, *slash; size_t basename_len; if (len == 0) return NULL; if (len > sizeof(line) - 1) len = sizeof(line) - 1; n = len; memcpy(line, data, n); if (n < 4 || line[0] != '#' || line[1] != '!') return NULL; line[n] = '\0'; /* Skip #! and whitespace */ p = line + 2; while (*p == ' ' || *p == '\t') p++; /* Find end of first token (the path) */ end = p; while (*end && *end != ' ' && *end != '\t' && *end != '\n' && *end != '\r') end++; /* Get basename - works for any path format */ slash = end - 1; while (slash > p && *slash != '/') slash--; if (*slash == '/') slash++; basename_len = end - slash; /* Check if this is 'env' (handles /any/path/env) */ if (basename_len == 3 && strncmp(slash, "env", 3) == 0) { int end_of_options = 0; /* * env [OPTION]... [NAME=VALUE]... COMMAND [ARG]... * Skip options and assignments until COMMAND. */ p = end; while (*p == ' ' || *p == '\t') p++; while (*p) { char *tok = p; int needs_arg = 0; end = p; while (*end && !is_shebang_delim(*end)) end++; if (!end_of_options && end - tok == 2 && tok[0] == '-' && tok[1] == '-') { end_of_options = 1; } else if (!end_of_options && tok[0] == '-' && !is_env_assignment_token(tok, end)) { needs_arg = env_opt_needs_argument(tok, end); } else if (is_env_assignment_token(tok, end)) { /* NAME=VALUE tokens are not interpreters. */ } else { break; } p = end; while (*p == ' ' || *p == '\t') p++; if (needs_arg) { while (*p && !is_shebang_delim(*p)) p++; while (*p == ' ' || *p == '\t') p++; } } if (!*p) return NULL; /* Now p points to the interpreter */ end = p; while (*end && !is_shebang_delim(*end)) end++; /* Get basename again (env arg might have a path too) */ slash = end - 1; while (slash > p && *slash != '/') slash--; if (*slash == '/') slash++; basename_len = end - slash; } if (basename_len == 0 || basename_len >= buflen) return NULL; /* Copy basename, keeping version number but stripping sub-versions * python3.11.2 -> python3, perl5.32 -> perl5 */ size_t i; for (i = 0; i < basename_len && i < buflen - 1; i++) { char ch = slash[i]; /* Stop at '.' or second consecutive digit */ if (ch == '.') break; if (ch >= '0' && ch <= '9' && i > 0 && slash[i-1] >= '0' && slash[i-1] <= '9') break; buf[i] = ch; } if (i == 0) return NULL; buf[i] = '\0'; return buf; } #pragma GCC pop_options /* * mime_from_shebang - map a shebang interpreter to a mime type * @interp: interpreter basename extracted from the shebang line * * Uses suffix and prefix matching to classify interpreters without * relying on their absolute path. Returns the mime type string for * recognized interpreters or NULL to let libmagic handle unknown ones. */ const char *mime_from_shebang(const char *interp) { const char *p; size_t len; if (!interp || !*interp) return NULL; /* Find end of string - we need the pointer for suffix check */ for (p = interp; *p; p++) ; len = p - interp; /* * Shell detection - match *sh suffix * Covers: sh, ash, bash, dash, fish, ksh, mksh, pdksh, zsh, csh, tcsh * Mirrors magic rule: (a|ba|da|fi|k|mk|pdk|z|c|tc)?sh * Avoid: wish,tclsh,jimsh - which are tcl */ if (len >= 2 && p[-2] == 's' && p[-1] == 'h') { if (len >= 4 && p[-4] == 'w') return "text/x-tcl"; if (len >= 5 && ((p[-5] == 't' && p[-4] == 'c' && p[-3] == 'l') || (p[-5] == 'j' && p[-4] == 'i' && p[-3] == 'm')) ) return "text/x-tcl"; return "text/x-shellscript"; } /* Python - python, python2, python3 * Note: file-5.47 changes this to 'text/x-script.python'. For * now, let's keep the old one so we don't break installations. */ if (len >= 6 && memcmp(interp, "python", 6) == 0) return "text/x-python"; /* Perl - perl, perl5 */ if (len >= 4 && memcmp(interp, "perl", 4) == 0) return "text/x-perl"; /* Lua */ if (len >= 3 && memcmp(interp, "lua", 3) == 0) return "text/x-lua"; /* Node.js */ if (len >= 4 && memcmp(interp, "node", 4) == 0) return "application/javascript"; /* SystemTap */ if (len >= 4 && memcmp(interp, "stap", 4) == 0) return "text/x-systemtap"; /* PHP */ if (len >= 3 && memcmp(interp, "php", 3) == 0) return "text/x-php"; /* R / Rscript */ if ((len >= 7 && memcmp(interp, "Rscript", 7) == 0) || //R-core (len == 1 && (interp[0] == 'r' || interp[0] == 'R'))) // R-littler return "text/x-R"; if (len >= 8 && memcmp(interp, "ocamlrun", 8) == 0) return "application/x-bytecode.ocaml"; /* * Unknown interpreter - return NULL to fall through to libmagic. * Being conservative here avoids misclassifying exotic interpreters. */ return NULL; } /* * detect_by_magic_number - detect common binaries from their magic number * @hdr: file header bytes * @len: number of bytes available in @hdr * * Performs O(1) checks for well-known magic numbers so libmagic can be * avoided when the type is obvious. Returns a mime type string or NULL * when no match is found. */ const char *detect_by_magic_number(const unsigned char *hdr, size_t len) { // We only access hdr[3] at the most so require at least 4 bytes if (len < 4) return NULL; /* PNG */ if (hdr[0] == 0x89 && hdr[1] == 'P' && hdr[2] == 'N' && hdr[3] == 'G') return "image/png"; /* JPEG */ if (hdr[0] == 0xFF && hdr[1] == 0xD8 && hdr[2] == 0xFF) return "image/jpeg"; /* GIF */ if (hdr[0] == 'G' && hdr[1] == 'I' && hdr[2] == 'F' && hdr[3] == '8') return "image/gif"; /* gzip */ if (hdr[0] == 0x1F && hdr[1] == 0x8B) return "application/gzip"; return NULL; } /* * detect_text_format - determine text subtype from initial bytes * @hdr: file header bytes * @len: number of bytes available in @hdr * * Looks for BOM, leading whitespace, and markup indicators to quickly * classify common text formats. Returns a mime type string or NULL when * further analysis is needed. */ const char *detect_text_format(const char *hdr, size_t len) { if (len < 5) return NULL; /* Skip UTF-8 BOM if present */ const char *p = hdr; const char *end = hdr + len; if ((unsigned char)hdr[0] == 0xEF && (unsigned char)hdr[1] == 0xBB && (unsigned char)hdr[2] == 0xBF) p += 3; /* Skip leading whitespace */ while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++; /* Check remaining length before string comparisons */ size_t remaining = end - p; if (remaining < 5) return NULL; /* HTML */ if (remaining >= 14 && strncasecmp(p, "mode & S_IFREG) { // If its a regular file (block devices have 0 length, too) // check to see if it's empty to skip doing all of the // expensive checks. Empty files are unexpectedly common. if (i->size == 0) { strncpy(buf, "application/x-empty", blen-1); buf[blen-1] = 0; decision_timing_stage_end(&fast_timing); return buf; } decision_timing_mime_stage_begin( DECISION_TIMING_MIME_GATHER_ELF, &gather_timing); elf = gather_elf(fd, i->size); decision_timing_stage_end(&gather_timing); if (elf & IS_ELF) { ptr = classify_elf_info(elf, path); if (ptr == NULL) { decision_timing_stage_end(&fast_timing); return (char *)ptr; } strncpy(buf, ptr, blen-1); buf[blen-1] = 0; decision_timing_stage_end(&fast_timing); return buf; } header_read = pread(fd, header, sizeof(header) - 1, 0); if (header_read > 0) { header_len = header_read; header[header_len] = '\0'; rewind_fd(fd); } else header[0] = '\0'; if (elf & HAS_SHEBANG) { // See if we can identify the mime-type char interp[64]; if (extract_shebang_interpreter(header, header_len, interp, sizeof(interp))) { ptr = mime_from_shebang(interp); if (ptr) { strncpy(buf, ptr, blen-1); buf[blen-1] = 0; decision_timing_stage_end(&fast_timing); return buf; } } } // Quick magic number check for common binary formats ptr = detect_by_magic_number((const unsigned char *)header, header_len); if (ptr) { strncpy(buf, ptr, blen-1); buf[blen-1] = 0; decision_timing_stage_end(&fast_timing); return buf; } // Quick text format detection if (elf & TEXT_SCRIPT) { ptr = detect_text_format(header, header_len); if (ptr) { strncpy(buf, ptr, blen-1); buf[blen-1] = 0; decision_timing_stage_end(&fast_timing); return buf; } } } // Take a look to see if its a device ptr = classify_device(i->mode); if (ptr) { strncpy(buf, ptr, blen-1); buf[blen-1] = 0; decision_timing_stage_end(&fast_timing); return buf; } decision_timing_stage_end(&fast_timing); // Use libmagic when in-house classification did not identify the object. if (ctx->magic_fast == NULL || ctx->magic_full == NULL) return NULL; decision_timing_mime_stage_begin( DECISION_TIMING_MIME_LIBMAGIC_FALLBACK, &timing); ptr = magic_descriptor(ctx->magic_fast, fd); if (ptr == NULL || memcmp(ptr, "text/plain", 10) == 0 || memcmp(ptr, "application/octet-stream", 24) == 0) { // Fall back to the whole database lookup rewind_fd(fd); ptr = magic_descriptor(ctx->magic_full, fd); if (ptr == NULL) { decision_timing_stage_end(&timing); return NULL; } } decision_timing_stage_end(&timing); char *str; strncpy(buf, ptr, blen-1); buf[blen-1] = 0; str = strchr(buf, ';'); if (str) *str = 0; return buf; } // This function converts byte array into asciie hex char *bytes2hex(char *final, const unsigned char *buf, unsigned int size) { unsigned int i; char *ptr = final; const char *hex = "0123456789abcdef"; if (final == NULL) return final; for (i=0; i>4]; /* Upper nibble */ *ptr++ = hex[buf[i] & 0x0F]; /* Lower nibble */ } *ptr = 0; return final; } // This function wraps read(2) so its signal-safe static ssize_t safe_read(int fd, char *buf, size_t size) { ssize_t len; do { len = read(fd, buf, size); } while (len < 0 && errno == EINTR); return len; } /* * get_hash_from_fd2 - calculate the requested file digest. * @fd: open descriptor whose contents should be measured. * @size: number of bytes to include in the digest calculation. * @alg: digest algorithm to use for the measurement. * Returns a heap-allocated hex string on success or NULL when hashing fails. */ static const char *degenerate_hash_sha1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; static const char *degenerate_hash_sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; static const char *degenerate_hash_sha512 = "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce" "47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"; static const char *degenerate_hash_md5 = "d41d8cd98f00b204e9800998ecf8427e"; char *get_hash_from_fd2(int fd, size_t size, file_hash_alg_t alg) { unsigned char *mapped; char *digest = NULL; size_t digest_length; struct decision_timing_span timing; decision_timing_stage_begin(DECISION_TIMING_STAGE_HASH_SHA, &timing); if (size == 0) { char *degenerate; switch (alg) { case FILE_HASH_ALG_SHA1: degenerate = strdup(degenerate_hash_sha1); break; case FILE_HASH_ALG_SHA256: degenerate = strdup(degenerate_hash_sha256); break; case FILE_HASH_ALG_SHA512: degenerate = strdup(degenerate_hash_sha512); break; case FILE_HASH_ALG_MD5: degenerate = strdup(degenerate_hash_md5); break; default: degenerate = NULL; break; } decision_timing_stage_end(&timing); return degenerate; } digest_length = file_hash_length(alg); if (digest_length == 0) { decision_timing_stage_end(&timing); return NULL; } mapped = mmap(0, size, PROT_READ, MAP_SHARED|MAP_POPULATE, fd, 0); if (mapped != MAP_FAILED) { unsigned char hptr[SHA512_DIGEST_LENGTH]; int computed = 0; switch (alg) { case FILE_HASH_ALG_SHA1: SHA1(mapped, size, hptr); computed = 1; break; case FILE_HASH_ALG_SHA256: SHA256(mapped, size, hptr); computed = 1; break; case FILE_HASH_ALG_SHA512: SHA512(mapped, size, hptr); computed = 1; break; case FILE_HASH_ALG_MD5: #ifdef USE_DEB MD5(mapped, size, hptr); computed = 1; #endif break; default: break; } munmap(mapped, size); if (computed) { digest = malloc((digest_length * 2) + 1); if (digest) bytes2hex(digest, hptr, digest_length); } } decision_timing_stage_end(&timing); return digest; } // This function returns 0 on error and 1 if successful /* * get_ima_hash - Decode the IMA digest-ng xattr and expose the measurement. * @fd: open file descriptor backed by an IMA measurement. * @alg: output parameter updated with the parsed algorithm, may be NULL. * @sha: caller supplied buffer large enough for FILE_DIGEST_STRING_MAX. * Returns 1 when a supported digest is parsed successfully, or 0 on failure. */ int get_ima_hash(int fd, file_hash_alg_t *alg, char *sha) { const struct ima_algo_desc *desc; unsigned char tmp[2 + SHA512_LEN]; ssize_t len; struct decision_timing_span timing; if (alg) *alg = FILE_HASH_ALG_NONE; decision_timing_stage_begin(DECISION_TIMING_STAGE_HASH_IMA, &timing); /* * digest-ng places the format type in byte 0 and the hash algorithm in * byte 1. The remaining bytes hold the binary digest whose length depends * on the algorithm chosen by the policy, so we size the buffer for the * largest algorithm we support. */ len = fgetxattr(fd, "security.ima", tmp, sizeof(tmp)); if (len < 2) { msg(LOG_DEBUG, "Can't read ima xattr"); decision_timing_stage_end(&timing); return 0; } if (tmp[0] != IMA_XATTR_DIGEST_NG) { msg(LOG_DEBUG, "Wrong ima xattr type"); decision_timing_stage_end(&timing); return 0; } desc = ima_lookup_algo(tmp[1]); if (desc == NULL) { msg(LOG_DEBUG, "Unsupported ima hash algorithm %u", tmp[1]); decision_timing_stage_end(&timing); return 0; } if (len < (ssize_t)(2 + desc->digest_len)) { msg(LOG_DEBUG, "ima xattr too small for alg %u", tmp[1]); decision_timing_stage_end(&timing); return 0; } bytes2hex(sha, &tmp[2], desc->digest_len); if (alg) *alg = desc->alg; decision_timing_stage_end(&timing); return 1; } static inline ssize_t read_preliminary_header(int fd, unsigned char *e_ident) { return safe_read(fd, (char *)e_ident, EI_NIDENT); } static Elf32_Ehdr *read_header32(int fd, Elf32_Ehdr *ptr, const unsigned char *e_ident) { memcpy(ptr->e_ident, e_ident, EI_NIDENT); ssize_t rc = safe_read(fd, (char *)ptr + EI_NIDENT, sizeof(Elf32_Ehdr) - EI_NIDENT); if (rc == (sizeof(Elf32_Ehdr) - EI_NIDENT)) return ptr; return NULL; } static Elf64_Ehdr *read_header64(int fd, Elf64_Ehdr *ptr, const unsigned char *e_ident) { memcpy(ptr->e_ident, e_ident, EI_NIDENT); ssize_t rc = safe_read(fd, (char *)ptr + EI_NIDENT, sizeof(Elf64_Ehdr) - EI_NIDENT); if (rc == (sizeof(Elf64_Ehdr) - EI_NIDENT)) return ptr; return NULL; } /* * interpreter_is_trusted - verify interpreter exists, is executable, trusted. * @interp: absolute interpreter path from PT_INTERP. * Returns 1 if interpreter exists, is executable, and trusted; 0 otherwise. */ static int interpreter_is_trusted(const char *interp) { struct file_info *info; int fd; int trusted = 0; if (interp == NULL || interp[0] == 0) return 0; // Open non-blocking in case it's a fifo fd = open(interp, O_RDONLY|O_CLOEXEC|O_NONBLOCK); if (fd < 0) return 0; info = stat_file_entry(fd); if (info && S_ISREG(info->mode) && (info->mode & (S_IXUSR|S_IXGRP|S_IXOTH))) { if (check_trust_database(interp, info, fd) == 1) trusted = 1; } free(info); close(fd); return trusted; } /** * Check interpreter provided as an argument obtained from the ELF against * known fixed locations in the file hierarchy. */ static int check_interpreter(const char *interp) { unsigned i; for (i = 0; i < MAX_INTERPS; i++) { if (strcmp(interp, interpreters[i]) == 0) return 0; } // We fell through the list that we know about. If it is trusted, // allow it. This is an attempt to accomodate other distributions // that may not have the same RHEL/Fedora interpreters. The best // solution is for them to add to the list. if (interpreter_is_trusted(interp)) return 0; return 1; } static int looks_like_text_script(int fd) { unsigned char hdr[512]; ssize_t n = pread(fd, hdr, sizeof(hdr), 0); if (n < 4) return 0; /* too small */ /* if it contains a NUL or control characters, call it binary */ for (ssize_t i = 0; i < n; ++i) if (hdr[i] < 0x09) return 0; return 1; /* looks like plain text */ } // size is the file size from fstat done when event was received uint32_t gather_elf(int fd, off_t size) { unsigned char e_ident[EI_NIDENT]; uint32_t info = 0; ssize_t rc; rc = read_preliminary_header(fd, e_ident); if (rc < 2) goto rewind_out; /* Detect scripts via shebang before ELF check */ if (e_ident[0] == '#' && e_ident[1] == '!') { info |= HAS_SHEBANG; goto rewind_out; } /* Make sure we have the full preliminary header */ if (rc < EI_NIDENT) goto rewind_out; /* Check ELF magic */ if (strncmp((char *)e_ident, ELFMAG, 4)) { // Not ELF - see if it might be text script if (looks_like_text_script(fd)) info |= TEXT_SCRIPT; goto rewind_out; } info |= IS_ELF; if (e_ident[EI_CLASS] == ELFCLASS32) { unsigned i, type; Elf32_Phdr *ph_tbl = NULL; Elf32_Ehdr hdr_buf; Elf32_Ehdr *hdr = read_header32(fd, &hdr_buf, e_ident); if (hdr == NULL) { info |= HAS_ERROR; goto rewind_out; } type = hdr->e_type & 0xFFFF; if (type == ET_EXEC) info |= HAS_EXEC; else if (type == ET_REL) info |= HAS_REL; else if (type == ET_CORE) info |= HAS_CORE; // Look for program header information // We want to do a basic size check to make sure /* * Promote before multiplying so malformed headers cannot * wrap in unsigned int before the bounds check. */ unsigned long sz = (unsigned long)hdr->e_phentsize * (unsigned long)hdr->e_phnum; // Program headers are meaning for executable & shared obj only if (sz == 0 && type == ET_REL) goto done32_obj; /* Verify the entry size is right */ if ((unsigned)hdr->e_phentsize != sizeof(Elf32_Phdr) || (unsigned)hdr->e_phnum == 0) { info |= HAS_ERROR; goto rewind_out; } if (sz > ((unsigned long)size - sizeof(Elf32_Ehdr))) { info |= HAS_ERROR; goto rewind_out; } ph_tbl = malloc(sz); if (ph_tbl == NULL) goto err_out32; if ((unsigned int)lseek(fd, (off_t)hdr->e_phoff, SEEK_SET) != hdr->e_phoff) goto err_out32; // Read in complete table if ((unsigned int)safe_read(fd, (char *)ph_tbl, sz) != sz) goto err_out32; // Check for rpath record for (i = 0; i < hdr->e_phnum; i++) { if (ph_tbl[i].p_type == PT_LOAD) { info |= HAS_LOAD; // If we have RWE flags, something is wrong if (ph_tbl[i].p_flags == (PF_X|PF_W|PF_R)) info |= HAS_RWE_LOAD; } if (ph_tbl[i].p_type == PT_PHDR) info |= HAS_PHDR; // Obtain program interpreter from ELF object file if (ph_tbl[i].p_type == PT_INTERP) { uint32_t len; char interp[385]; uint32_t filesz = ph_tbl[i].p_filesz; uint32_t offset = ph_tbl[i].p_offset; info |= HAS_INTERP; if ((unsigned int) lseek(fd, offset, SEEK_SET) != offset) goto err_out32; len = (filesz < 385 ? filesz : 385); if ((unsigned int) safe_read(fd, (char *) interp, len) != len) goto err_out32; // Explictly terminate the string if (len == 0) interp[0] = 0; else interp[len - 1] = '\0'; // Perform ELF interpreter validation if (check_interpreter(interp)) info |= HAS_BAD_INTERP; } if (ph_tbl[i].p_type == PT_GNU_STACK) { // If we have Execute flags, something is wrong if (ph_tbl[i].p_flags & PF_X) info |= HAS_EXE_STACK; } if (ph_tbl[i].p_type == PT_DYNAMIC) { unsigned int j = 0; unsigned int num; info |= HAS_DYNAMIC; if (ph_tbl[i].p_filesz > size) goto err_out32; num = ph_tbl[i].p_filesz / sizeof(Elf32_Dyn); if (num > 1000) goto err_out32; if (num == 0) continue; Elf32_Dyn *dyn_tbl = malloc(ph_tbl[i].p_filesz); if (dyn_tbl == NULL) goto err_out32; if((unsigned int)lseek(fd, ph_tbl[i].p_offset, SEEK_SET) != ph_tbl[i].p_offset) { free(dyn_tbl); goto err_out32; } if ((unsigned int)safe_read(fd, (char *)dyn_tbl, ph_tbl[i].p_filesz) != ph_tbl[i].p_filesz) { free(dyn_tbl); goto err_out32; } while (j < num) { if (dyn_tbl[j].d_tag == DT_NEEDED) { // intentional } /* else if (dyn_tbl[j].d_tag == DT_RUNPATH) info |= HAS_RPATH; else if (dyn_tbl[j].d_tag == DT_RPATH) info |= HAS_RPATH; */ else if (dyn_tbl[j].d_tag == DT_DEBUG) { info |= HAS_DEBUG; break; } j++; } free(dyn_tbl); } // if (info & HAS_RPATH) // break; } goto done32; err_out32: info |= HAS_ERROR; done32: free(ph_tbl); done32_obj: ; // fix an 'error label at end of compound statement' } else if (e_ident[EI_CLASS] == ELFCLASS64) { unsigned i, type; Elf64_Phdr *ph_tbl; Elf64_Ehdr hdr_buf; Elf64_Ehdr *hdr = read_header64(fd, &hdr_buf, e_ident); if (hdr == NULL) { info |= HAS_ERROR; goto rewind_out; } type = hdr->e_type & 0xFFFF; if (type == ET_EXEC) info |= HAS_EXEC; else if (type == ET_REL) info |= HAS_REL; else if (type == ET_CORE) info |= HAS_CORE; // Look for program header information // We want to do a basic size check to make sure /* * Promote before multiplying so malformed headers cannot * wrap in unsigned int before the bounds check. */ unsigned long sz = (unsigned long)hdr->e_phentsize * (unsigned long)hdr->e_phnum; // Program headers are meaning for executable & shared obj only if (sz == 0 && type == ET_REL) goto done64_obj; /* Verify the entry size is right */ if ((unsigned)hdr->e_phentsize != sizeof(Elf64_Phdr) || (unsigned)hdr->e_phnum == 0) { info |= HAS_ERROR; goto rewind_out; } if (sz > ((unsigned long)size - sizeof(Elf64_Ehdr))) { info |= HAS_ERROR; goto rewind_out; } ph_tbl = malloc(sz); if (ph_tbl == NULL) goto err_out64; if ((unsigned int)lseek(fd, (off_t)hdr->e_phoff, SEEK_SET) != hdr->e_phoff) goto err_out64; // Read in complete table if ((unsigned int)safe_read(fd, (char *)ph_tbl, sz) != sz) goto err_out64; // Check for rpath record for (i = 0; i < hdr->e_phnum; i++) { if (ph_tbl[i].p_type == PT_LOAD) { info |= HAS_LOAD; // If we have RWE flags, something is wrong if (ph_tbl[i].p_flags == (PF_X|PF_W|PF_R)) info |= HAS_RWE_LOAD; } if (ph_tbl[i].p_type == PT_PHDR) info |= HAS_PHDR; // Obtain program interpreter from ELF object file if (ph_tbl[i].p_type == PT_INTERP) { uint64_t len; char interp[385]; uint64_t filesz = ph_tbl[i].p_filesz; uint64_t offset = ph_tbl[i].p_offset; info |= HAS_INTERP; if ((unsigned int) lseek(fd, offset, SEEK_SET) != offset) goto err_out64; len = (filesz < 385 ? filesz : 385); if ((unsigned int) safe_read(fd, (char *) interp, len) != len) goto err_out64; /* Explicitly terminate the string */ if (len == 0) interp[0] = 0; else interp[len - 1] = '\0'; // Perform ELF interpreter validation if (check_interpreter(interp)) info |= HAS_BAD_INTERP; } if (ph_tbl[i].p_type == PT_GNU_STACK) { // If we have Execute flags, something is wrong if (ph_tbl[i].p_flags & PF_X) info |= HAS_EXE_STACK; } if (ph_tbl[i].p_type == PT_DYNAMIC) { unsigned int j = 0; unsigned int num; info |= HAS_DYNAMIC; if (ph_tbl[i].p_filesz>(long unsigned int)size) goto err_out64; num = ph_tbl[i].p_filesz / sizeof(Elf64_Dyn); if (num > 1000) goto err_out64; if (num == 0) continue; Elf64_Dyn *dyn_tbl = malloc(ph_tbl[i].p_filesz); if (dyn_tbl == NULL) goto err_out64; if ((unsigned int)lseek(fd, ph_tbl[i].p_offset, SEEK_SET) != ph_tbl[i].p_offset) { free(dyn_tbl); goto err_out64; } if ((unsigned int)safe_read(fd, (char *)dyn_tbl, ph_tbl[i].p_filesz) != ph_tbl[i].p_filesz) { free(dyn_tbl); goto err_out64; } while (j < num) { if (dyn_tbl[j].d_tag == DT_NEEDED) { // intentional } /* else if (dyn_tbl[j].d_tag == DT_RUNPATH) info |= HAS_RPATH; else if (dyn_tbl[j].d_tag == DT_RPATH) info |= HAS_RPATH; */ else if (dyn_tbl[j].d_tag == DT_DEBUG) { info |= HAS_DEBUG; break; } j++; } free(dyn_tbl); } // if (info & HAS_RPATH) // break; } goto done64; err_out64: info |= HAS_ERROR; done64: free(ph_tbl); done64_obj: ; // fix an 'error label at end of compound statement' } else // Invalid ELF class info |= HAS_ERROR; rewind_out: rewind_fd(fd); return info; } fapolicyd-2.0/src/library/file.h000066400000000000000000000104261524071644000166560ustar00rootroot00000000000000/* * file.h - Header file for file.c * Copyright (c) 2016,2018-20,2022 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef FILE_HEADER #define FILE_HEADER #include #include #include #include "gcc-attributes.h" // Supported digest algorithms for file content measurement typedef enum { FILE_HASH_ALG_NONE = 0, FILE_HASH_ALG_MD5, // Legacy support for MD5-based trust sources FILE_HASH_ALG_SHA1, FILE_HASH_ALG_SHA256, FILE_HASH_ALG_SHA512, } file_hash_alg_t; typedef enum { FILE_INIT_OK = 0, FILE_INIT_MAGIC_FAST_OPEN_FAILED = 1, FILE_INIT_MAGIC_FAST_LOAD_FAILED = 2, FILE_INIT_MAGIC_FULL_OPEN_FAILED = 3, FILE_INIT_MAGIC_FULL_LOAD_FAILED = 4, } file_init_status_t; #define MD5_LEN 16 #define SHA1_LEN 20 #define SHA256_LEN 32 #define SHA512_LEN 64 // Longest printable digest string expected - includes algorithm prefix and NUL // (SHA512_LEN * 2) + 8 = 136 bytes including the terminating NUL #define FILE_DIGEST_STRING_MAX 136 #define FILE_DIGEST_STRING_WIDTH 135 #define TRUSTDB_DATA_BUFSZ (FILE_DIGEST_STRING_MAX + 64) // Information we will cache to identify the same executable struct file_info { dev_t device; ino_t inode; mode_t mode; off_t size; struct timespec time; file_hash_alg_t digest_alg; char digest[FILE_DIGEST_STRING_MAX]; }; int file_init(void); void file_init_failure_for_tests(file_init_status_t failure); void file_close(void); struct file_info *stat_file_entry(int fd) __attr_dealloc_free __attr_fd_arg (1); int open_verified_regular_file(const char *path, const struct stat *expected, struct stat *opened) __nonnull ((1, 2, 3)); void file_info_reset_digest(struct file_info *info); file_hash_alg_t file_hash_alg(unsigned len); file_hash_alg_t file_hash_alg_fast(const char *digest); void file_info_cache_digest(struct file_info *info, file_hash_alg_t alg); size_t file_hash_length(file_hash_alg_t alg); const char *file_hash_alg_name(file_hash_alg_t alg); file_hash_alg_t file_hash_name_alg(const char *name); int compare_file_infos(const struct file_info *p1, const struct file_info *p2); int check_ignore_mount_noexec(const char *mounts_file, const char *point); int iterate_ignore_mounts(const char *ignore_list, int (*callback)(const char *mount, void *user_data), void *user_data); int check_ignore_mount_warning(const char *mounts_file, const char *point, const char **warning); char *get_file_from_fd(int fd, pid_t pid, size_t blen, char *buf) __attr_access ((__write_only__, 4, 3)) __attr_fd_arg (1); char *get_device_from_stat(unsigned int device, size_t blen, char *buf) __attr_access ((__write_only__, 3, 2)); const char *classify_device(mode_t mode); const char *classify_elf_info(uint32_t elf, const char *path); const char *extract_shebang_interpreter(const char *data, size_t len, char *buf, size_t buflen) __attr_access ((__write_only__, 3, 4)); const char *mime_from_shebang(const char *interp); const char *detect_by_magic_number(const unsigned char *hdr, size_t len); const char *detect_text_format(const char *hdr, size_t len); char *get_file_type_from_fd(int fd, const struct file_info *i, const char *path, size_t blen, char *buf) __attr_access ((__write_only__, 5, 4)) __attr_fd_arg_read (1); char *bytes2hex(char *final, const unsigned char *buf, unsigned int size) __attr_access ((__read_only__, 2, 3)); char *get_hash_from_fd2(int fd, size_t size, file_hash_alg_t alg) __attr_dealloc_free __attr_fd_arg_read (1); int get_ima_hash(int fd, file_hash_alg_t *alg, char *sha) __attr_fd_arg (1); uint32_t gather_elf(int fd, off_t size) __attr_fd_arg_read (1); #endif fapolicyd-2.0/src/library/filter.c000066400000000000000000000537151524071644000172270ustar00rootroot00000000000000/* * filter.c - filter for a trust source * Copyright (c) 2023 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka */ /* * Overview * ------- * * Filters are stored in a tree. Each node describes a path fragment and * whether it should be kept (ADD) or dropped (SUB). The tree is walked using * an explicit stack rather than recursion. Stack items track the current * filter node, the depth level and an offset into the path being evaluated. * * Three major users of the stack exist: * * - filter_check() walks the tree comparing a path against the filters. * - filter_load_file() builds the tree from an indented configuration file. * - filter_destroy_obj() iteratively frees the tree. * * Using a stack keeps memory usage predictable and avoids deep recursion when * filters contain many nested paths. * * filter_check() is intentionally a read-only walk over the compiled filter * tree. Trust database imports are serialized today, but the tree is shared * library state and future import backends may want to reuse one filter * generation. Per-check traversal flags therefore live in stack_item_t, not in * filter_t nodes. * * Assumption: real-world filter nesting is shallow (Fedora default max = 4). * MAX_FILTER_DEPTH is set to 64 for safety; raise it if installers add deeper * trees. */ #include "config.h" #include #include #include #include #include #include "filter.h" #include "stack.h" #include "message.h" #include "string-util.h" #include "paths.h" #pragma GCC optimize("O3") filter_t *global_filter = NULL; static FILE *trace = NULL; #define FILTER_TRACE(fmt, ...) \ do { \ if (trace) \ fprintf(trace, fmt, ##__VA_ARGS__); \ } while (0) void filter_set_trace(FILE *stream) { trace = stream; } static filter_t *filter_create_obj(void); static void filter_destroy_obj(filter_t *_filter); static size_t filter_count_nodes(filter_t *root); static int stack_push_vars_cap(stack_t *_stack, stack_item_t *buf, int *sp, size_t cap, int _level, int _offset, filter_t *_filter); static int stack_push_vars(stack_t *_stack, stack_item_t *buf, int *sp, int _level, int _offset, filter_t *_filter); /* * filter_init - initialize module and global filter tree * Returns 0 on success and 1 on failure. */ int filter_init(void) { global_filter = filter_create_obj(); if (global_filter == NULL) return 1; return 0; } /* * filter_destroy - free global filter tree */ void filter_destroy(void) { filter_destroy_obj(global_filter); global_filter = NULL; } /* * filter_create_obj - allocate filter object and fill with defaults * Returns pointer to new object or NULL on failure. */ static filter_t *filter_create_obj(void) { filter_t *filter = malloc(sizeof(filter_t)); if (filter) { filter->type = NONE; filter->path = NULL; filter->len = 0; filter->matched = 0; filter->processed = 0; list_init(&filter->list); } return filter; } /* * filter_destroy_obj - free filter tree rooted at _filter * Uses an explicit stack to avoid deep recursion. */ static void filter_destroy_obj(filter_t *_filter) { if (_filter == NULL) return; filter_t *filter = _filter; stack_t stack; stack_init(&stack); stack_push(&stack, filter); while (!stack_is_empty(&stack)) { filter = (filter_t*)stack_top(&stack); if (filter->processed) { (void)free(filter->path); // assume that item->data is NULL (list nodes were // cleared earlier) list_empty(&filter->list); (void)free(filter); stack_pop(&stack); continue; } list_item_t *item = list_get_first(&filter->list); for (; item != NULL ; item = item->next) { filter_t *next_filter = (filter_t*)item->data; // we can use list_empty() later // we dont want to free filter right now // it will freed after popping item->data = NULL; stack_push(&stack, next_filter); } /* mark node as processed so it will be freed on next pass */ filter->processed = 1; } stack_destroy(&stack); } /* * filter_count_nodes - count filter nodes in a tree * @root: root node of filter tree * Returns number of nodes reachable from @root. */ static size_t filter_count_nodes(filter_t *root) { size_t count = 0; stack_t stack; if (root == NULL) return 0; stack_init(&stack); stack_push(&stack, root); while (!stack_is_empty(&stack)) { filter_t *filter = (filter_t *)stack_top(&stack); list_item_t *item; stack_pop(&stack); count++; for (item = list_get_first(&filter->list); item != NULL; item = item->next) stack_push(&stack, item->data); } stack_destroy(&stack); return count; } /* * stack_push_vars_cap - push traversal context with explicit capacity * @_stack: traversal stack * @buf: stack item buffer * @sp: current stack pointer in @buf * @cap: number of entries available in @buf * @_level: current filter nesting level * @_offset: current offset in matched path * @_filter: filter node to push * Returns 0 on success and -1 if @cap would be exceeded. */ static int stack_push_vars_cap(stack_t *_stack, stack_item_t *buf, int *sp, size_t cap, int _level, int _offset, filter_t *_filter) { if (_stack == NULL || buf == NULL || sp == NULL) return -1; if (*sp < 0 || (size_t)*sp >= cap) return -1; stack_item_t *item = &buf[(*sp)++]; item->level = _level; item->offset = _offset; item->processed = 0; item->matched = 0; item->filter = _filter; stack_push(_stack, item); return 0; } /* * stack_push_vars - create context item & push it to the top of traversal stack * Returns 0 on success and -1 if MAX_FILTER_DEPTH would be exceeded. */ static int stack_push_vars(stack_t *_stack, stack_item_t *buf, int *sp, int _level, int _offset, filter_t *_filter) { return stack_push_vars_cap(_stack, buf, sp, MAX_FILTER_DEPTH, _level, _offset, _filter); } /* * stack_pop_vars - pop context item from traversal stack */ static void stack_pop_vars(stack_t *_stack, int *sp) { if (_stack == NULL || sp == NULL || *sp <= 0) return; stack_pop(_stack); (*sp)--; } /* * stack_pop_all_vars - pop all context items */ static void stack_pop_all_vars(stack_t *_stack, int *sp) { if (_stack == NULL || sp == NULL) return; while (!stack_is_empty(_stack)) stack_pop_vars(_stack, sp); } /* * filter_check - compare path against loaded filters * @_path: full path of file to test * Returns FILTER_ALLOW if file should be kept, FILTER_DENY if it should be * dropped, or FILTER_ERR_DEPTH if traversal state cannot be allocated * (treated the same as a deny by callers to keep processing other paths). */ __attribute__((hot)) filter_rc_t filter_check(const char *_path) { if (_path == NULL) { msg(LOG_ERR, "filter_check: path is NULL, something is wrong!"); return 0; } filter_t *filter = global_filter; size_t path_len = strnlen(_path, PATH_MAX); char *path = alloca(path_len + 1); strncpy(path, _path, path_len); path[path_len] = 0; /* Reject paths with parent directory references */ if ((path[0] == '.' && path[1] == '.' && (path[2] == '/' || path[2] == '\0')) || strstr(path, "/../") != NULL || (path_len >= 3 && strcmp(path + path_len - 3, "/..") == 0)) return FILTER_DENY; /* offset tracks how much of the path has already matched */ size_t offset = 0; /* Create a stack to store the filters that need to be checked */ stack_t stack; stack_init(&stack); size_t stack_cap = filter_count_nodes(global_filter); stack_item_t *stack_buf; int sp = 0; if (stack_cap == 0) { stack_destroy(&stack); return FILTER_DENY; } stack_buf = calloc(stack_cap, sizeof(*stack_buf)); if (stack_buf == NULL) { msg(LOG_ERR, "fapolicyd: cannot allocate filter traversal stack"); stack_destroy(&stack); return FILTER_ERR_DEPTH; } filter_rc_t res = FILTER_DENY; int level = 0; stack_item_t *stack_item; if (stack_push_vars_cap(&stack, stack_buf, &sp, stack_cap, level, offset, filter)) { msg(LOG_WARNING, "fapolicyd: filter traversal stack exhausted\n"); free(stack_buf); stack_destroy(&stack); return FILTER_ERR_DEPTH; } while(!stack_is_empty(&stack)) { int matched = 0; stack_item = (stack_item_t *)stack_top(&stack); stack_item->processed = 1; filter = stack_item->filter; // this is starting branch of the algo // assuming that in root filter filter->path is NULL if (filter->path == NULL) { list_item_t *item = list_get_first(&filter->list); // push all the descendants to the stack for (; item != NULL ; item = item->next) { filter_t *next_filter = (filter_t*)item->data; if (stack_push_vars_cap(&stack, stack_buf, &sp, stack_cap, level+1, offset, next_filter)) { msg(LOG_WARNING, "fapolicyd: filter traversal stack exhausted\n"); res = FILTER_ERR_DEPTH; goto end; } } // usual branch, start with processing } else { // wildcard contition char *is_wildcard = strpbrk(filter->path, "?*["); if (is_wildcard) { int count = 0; char *filter_lim, *filter_old_lim; filter_lim = filter_old_lim = filter->path; char *path_lim, *path_old_lim; path_lim = path_old_lim = path+offset; // there can be wildcard in the dir name as well // we need to count how many chars can be eaten // by wildcard while(1) { filter_lim = strchr(filter_lim, '/'); path_lim = strchr(path_lim, '/'); if (filter_lim) { count++; filter_old_lim = filter_lim; filter_lim++; } else break; if (path_lim) { path_old_lim = path_lim; path_lim++; } else break; } // put 0 after the last / char tmp = '\0'; if (count && *(filter_old_lim+1) == '\0') { tmp = *(path_old_lim+1); *(path_old_lim+1) = '\0'; } // check fnmatch against remaining path matched = !fnmatch(filter->path, path+offset,0); // restore original path string if (count && *(filter_old_lim+1) == '\0') *(path_old_lim+1) = tmp; if (matched) offset = (path_old_lim - path) + offset; } else { // match normal path or just specific part of it matched = !strncmp(path+offset, filter->path, filter->len); if (matched) offset += filter->len; } if (matched) { level++; stack_item->matched = 1; // if matched we need ot push descendants // to the stack list_item_t *item=list_get_first(&filter->list); // if there are no descendants and it is // a wildcard then it's a match if (item == NULL && is_wildcard) { const char *rule = (filter->path && *filter->path) ? filter->path : "/"; FILTER_TRACE("%s %s %s\n", filter->type == ADD ? "allow" : "deny", rule, "match"); // if '+' ret 1 and if '-' ret 0 res = filter->type == ADD ? FILTER_ALLOW : FILTER_DENY; goto end; } // no descendants, and already compared // whole path string so its a match if (item == NULL && path_len == offset) { const char *rule = (filter->path && *filter->path) ? filter->path : "/"; FILTER_TRACE("%s %s %s\n", filter->type == ADD ? "allow" : "deny", rule, "match"); // if '+' ret 1 and if '-' ret 0 res = filter->type == ADD ? FILTER_ALLOW : FILTER_DENY; goto end; } // push descendants to the stack for (; item != NULL ; item = item->next) { filter_t *next_filter = (filter_t*)item->data; if (stack_push_vars_cap(&stack, stack_buf, &sp, stack_cap, level, offset, next_filter)) { msg(LOG_WARNING, "fapolicyd: filter traversal stack exhausted\n"); res = FILTER_ERR_DEPTH; goto end; } } } } if (filter->type != NONE) { const char *rule = (filter->path && *filter->path) ? filter->path : "/"; FILTER_TRACE("%s %s %s\n", filter->type == ADD ? "allow" : "deny", rule, matched ? "match" : "no match"); } stack_item = NULL; // pop already processed filters from the top of the stack do { if (stack_item) { filter = stack_item->filter; // assuimg that nothing has matched on the // upper level so it's a directory match if (stack_item->matched && filter->path[filter->len-1] == '/') { res = filter->type == ADD ? FILTER_ALLOW : FILTER_DENY; goto end; } stack_pop_vars(&stack, &sp); } stack_item = (stack_item_t*)stack_top(&stack); } while(stack_item && stack_item->processed); if (!stack_item) break; offset = stack_item->offset; level = stack_item->level; } end: FILTER_TRACE("decision %s\n", res == FILTER_ALLOW ? "include" : "exclude"); // Clean up the stack stack_pop_all_vars(&stack, &sp); stack_destroy(&stack); free(stack_buf); return res; } /* * filter_prune_list - Remove list entries that do not pass the filter. * @list: List of paths to be checked. * @path: Optional configuration file path, defaults to FILTER_FILE. * * Initializes the filter module, loads the configuration, and walks the * supplied list. Any entry that is not allowed by the filter is removed. * Returns 0 on success and 1 if initialization, loading, or evaluation fails. */ int filter_prune_list(list_t *list, const char *path) { if (list == NULL) return 1; if (filter_init()) return 1; if (filter_load_file(path)) { filter_destroy(); return 1; } list_item_t *lptr = list->first, *prev = NULL; while (lptr) { list_item_t *next = lptr->next; filter_rc_t res = filter_check(lptr->index); if (res == FILTER_ALLOW) { prev = lptr; lptr = next; continue; } if (res == FILTER_ERR_DEPTH) msg(LOG_WARNING, "filter nesting exceeds MAX_FILTER_DEPTH for %s; excluding", (char *)lptr->index); if (prev) prev->next = lptr->next; else list->first = lptr->next; if (!lptr->next) list->last = prev; list_destroy_item(&lptr); --list->count; lptr = next; } filter_destroy(); return 0; } /* * filter_load_file - load filter configuration and build tree * @path: optional configuration file path, defaults to FILTER_FILE * Returns 0 on success and 1 on error. */ int filter_load_file(const char *path) { int res = 0; FILE *stream; msg(LOG_DEBUG, "Loading filter"); if (path == NULL) { stream = fopen(OLD_FILTER_FILE, "r"); if (stream == NULL) { stream = fopen(FILTER_FILE, "r"); if (stream == NULL) { msg(LOG_ERR, "Cannot open filter file %s", FILTER_FILE); return 1; } } else { msg(LOG_INFO, "Using old filter file: %s, use the new one: %s", OLD_FILTER_FILE, FILTER_FILE); msg(LOG_INFO, "Consider 'mv %s %s'", OLD_FILTER_FILE, FILTER_FILE); } } else { stream = fopen(path, "r"); if (stream == NULL) { msg(LOG_ERR, "Cannot open filter file %s", path); return 1; } } size_t len = 0; char * line = NULL; long line_number = 0; int last_level = 0; stack_t stack; stack_init(&stack); stack_item_t stack_buf[MAX_FILTER_DEPTH]; int sp = 0; /* root of the tree is already allocated */ if (stack_push_vars(&stack, stack_buf, &sp, last_level, 0, global_filter)) { msg(LOG_WARNING, "fapolicyd: rule nesting exceeds MAX_FILTER_DEPTH (%d)\n", MAX_FILTER_DEPTH); fclose(stream); return 1; /* depth too deep */ } while (getline(&line, &len, stream) != -1) { line_number++; if (line[0] == '\0' || line[0] == '\n') { free(line); line = NULL; continue; } // get rid of the new line char char * new_line = strchr(line, '\n'); if (new_line) { *new_line = '\0'; len--; } int level = 1; char * rest = line; filter_type_t type = NONE; for (size_t i = 0 ; i < len ; i++) { switch (line[i]) { case ' ': level++; continue; case '+': type = ADD; break; case '-': type = SUB; break; case '#': type = COMMENT; break; default: type = BAD; break; } // continue with next char // skip + and space rest = fapolicyd_strtrim(&(line[i+2])); break; } // ignore comment if (type == COMMENT) { free(line); line = NULL; continue; } // if something bad return error if (type == BAD) { msg(LOG_ERR, "filter_load_file: cannot parse line number %ld, \"%s\"", line_number, line); free(line); line = NULL; goto bad; } filter_t * filter = filter_create_obj(); if (!filter) { free(line); line = NULL; goto bad; } filter->path = strdup(rest); if (filter->path == NULL) { filter_destroy_obj(filter); free(line); line = NULL; goto bad; } filter->len = strlen(filter->path); filter->type = type; // compare indetention between the last and current line last_level = ((stack_item_t*)stack_top(&stack))->level; if (level == last_level) { // since we are at the same level as filter before // we need to pop the previous filter from the top stack_pop_vars(&stack, &sp); // pushing filter to the list of top's children list list_prepend( &((stack_item_t*)stack_top(&stack))->filter->list, NULL, (void*)filter); // pushing filter to the top of the stack if (stack_push_vars(&stack, stack_buf, &sp, level, 0, filter)) { msg(LOG_WARNING, "fapolicyd: rule nesting exceeds MAX_FILTER_DEPTH (%d)\n", MAX_FILTER_DEPTH); filter_destroy_obj(filter); free(line); line = NULL; goto bad; } } else if (level == last_level + 1) { // this filter has higher level tha privious one // we wont do pop just push // pushing filter to the list of top's children list list_prepend( &((stack_item_t*)stack_top(&stack))->filter->list, NULL, (void*)filter); // pushing filter to the top of the stack if (stack_push_vars(&stack, stack_buf, &sp, level, 0, filter)) { msg(LOG_WARNING, "fapolicyd: rule nesting exceeds MAX_FILTER_DEPTH (%d)\n", MAX_FILTER_DEPTH); filter_destroy_obj(filter); free(line); line = NULL; goto bad; } } else if (level < last_level){ // level of indentation dropped, we need to pop // +1 is meant for getting rid of the current // level so we can push again for (int i = 0 ; i < last_level - level + 1; i++) { stack_pop_vars(&stack, &sp); } // pushing filter to the list of top's children list list_prepend( &((stack_item_t*)stack_top(&stack))->filter->list, NULL, (void*)filter); // pushing filter to the top of the stack if (stack_push_vars(&stack, stack_buf, &sp, level, 0, filter)) { msg(LOG_WARNING, "fapolicyd: rule nesting exceeds MAX_FILTER_DEPTH (%d)\n", MAX_FILTER_DEPTH); filter_destroy_obj(filter); free(line); line = NULL; goto bad; } } else { msg(LOG_ERR, "filter_load_file: paring error line: %ld, \"%s\"", line_number, line); filter_destroy_obj(filter); free(line); line = NULL; goto bad; } } if (line) { free(line); line = NULL; } goto good; bad: res = 1; good: fclose(stream); stack_pop_all_vars(&stack, &sp); stack_destroy(&stack); if (global_filter->list.count == 0) { const char *conf_file = path ? path : FILTER_FILE; msg(LOG_ERR, "filter_load_file: no valid filter provided in %s", conf_file); } return res; } /* * These are some ideas to improve performance if the number of rules grows * or we find this is holding up trustdb restablishment in the future: * * Speed-up steps from simplest to most involved * * 1. Compute and cache wildcard metadata at load time * Add two fields to filter_t: bool has_wildcard and char last_char. * Set them once in filter_load_file(). * During matching skip strpbrk() and the separator-count loop unless * has_wildcard is true; for plain prefixes just use memcmp(). * * 2. Stop copying the path * Instead of alloca+strcpy, keep a const char *p = _path; pointer and move * it with offsets. * If mutability is required only for the “temporarily NUL-terminate” * trick, maintain a small struct { size_t pos; char saved; } stack * and restore the byte after fnmatch. * * 3. Reset node flags with a generation counter * Give filter_t a 32-bit vis_tag and increment a global visit_id each * time filter_check() starts. * A node is “visited” when vis_tag == visit_id; no memory writes are * needed to “unvisit” between calls, eliminating persistent * matched/processed state and making the code thread-friendly. * * 4. Group children into two vectors * On load, partition each node’s children into * • “literal” (no wildcard) * • “pattern” (has wildcard) * Store literals in a sorted array and binary-search them; patterns stay * in a small list evaluated with fnmatch(). * ROI: most look-ups stop after a logarithmic search without polling * wildcard siblings. * * 5. Build a prefix-trie * Instead of a general linked list, compile the filter into a radix tree * keyed by path components. * Each node then needs at most one comparison per component; backtracking * is unnecessary. Memory usage stays modest because rules share prefixes. * * 6. Pre-compile glob patterns into DFA * Libraries like libglob/libtre can compile POSIX globs into a mini-automaton. * The matcher then advances the DFA over the path once, rather than * calling fnmatch() repeatedly. * * 7. Batch evaluation / directory memoisation * When scanning entire RPM databases the same directory prefix recurs * thousands of times (/usr/lib/ vs. every .so). * Cache the verdict for each directory path; skip evaluation for children * once an ancestor’s decision is known. * */ fapolicyd-2.0/src/library/filter.h000066400000000000000000000037511524071644000172270ustar00rootroot00000000000000/* * filter.h - Header for a filter implementation * Copyright (c) 2023 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka */ #ifndef FILTER_H_ #define FILTER_H_ #include #include #include #include "llist.h" #include "gcc-attributes.h" typedef enum filter_type { NONE, ADD, SUB, COMMENT, BAD, } filter_type_t; typedef struct _filter { filter_type_t type; char * path; size_t len; /* * filter_check() treats loaded filters as immutable. These flags are * reserved for destructive/internal tree walks that own the tree. */ int processed; int matched; list_t list; } filter_t; extern filter_t *global_filter; typedef struct _stack_item { int level; int offset; int processed; int matched; filter_t *filter; } stack_item_t; #ifndef MAX_FILTER_DEPTH #define MAX_FILTER_DEPTH 64 #endif /* filter_check return codes (depth errors exclude the path) */ typedef enum { FILTER_DENY = 0, FILTER_ALLOW = 1, FILTER_ERR_DEPTH = -2, } filter_rc_t; int filter_init(void); void filter_destroy(void); __attribute__((hot)) filter_rc_t filter_check(const char *path) __wur; int filter_load_file(const char *path) __wur; void filter_set_trace(FILE *stream); int filter_prune_list(list_t *list, const char *path) __wur; #endif // FILTER_H_ fapolicyd-2.0/src/library/gcc-attributes.h000066400000000000000000000051241524071644000206560ustar00rootroot00000000000000/* * gcc-attributes.h - fallback compiler attribute definitions * Copyright (c) 2020-22,2024-26 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. */ #ifndef GCC_ATTRIBUTES_H #define GCC_ATTRIBUTES_H // These macros originate in sys/cdefs.h. These are stubs in case undefined. #include // any major header brings cdefs.h #ifndef __fapolicyd_has_attribute /* Match glibc's guard so older Clang does not parse __has_attribute calls. */ # if (defined __has_attribute \ && (!defined __clang_minor__ \ || 3 < __clang_major__ + (5 <= __clang_minor__))) # define __fapolicyd_has_attribute(attr) __has_attribute (attr) # else # define __fapolicyd_has_attribute(attr) 0 # endif #endif #ifndef __returns_nonnull # define __returns_nonnull #endif #ifndef __attr_access # define __attr_access(x) #endif #ifndef __attr_dealloc # define __attr_dealloc(dealloc, argno) # define __attr_dealloc_free #endif #ifndef __attribute_malloc__ # define __attribute_malloc__ #endif #ifndef __attribute_const__ # define __attribute_const__ #endif #ifndef __attribute_pure__ # define __attribute_pure__ #endif #ifndef __noreturn # define __noreturn __attribute__ ((__noreturn__)) #endif #ifndef __nonnull # define __nonnull(params) #endif #ifndef __attr_fd_arg # if __fapolicyd_has_attribute (__fd_arg__) # define __attr_fd_arg(argno) __attribute__ ((__fd_arg__ (argno))) # else # define __attr_fd_arg(argno) # endif #endif #ifndef __attr_fd_arg_read # if __fapolicyd_has_attribute (__fd_arg_read__) # define __attr_fd_arg_read(argno) \ __attribute__ ((__fd_arg_read__ (argno))) # else # define __attr_fd_arg_read(argno) # endif #endif #ifndef __attr_fd_arg_write # if __fapolicyd_has_attribute (__fd_arg_write__) # define __attr_fd_arg_write(argno) \ __attribute__ ((__fd_arg_write__ (argno))) # else # define __attr_fd_arg_write(argno) # endif #endif #ifndef __wur # define __wur #endif #endif fapolicyd-2.0/src/library/llist.c000066400000000000000000000063351524071644000170650ustar00rootroot00000000000000/* * llist.c - Linked list as a temporary memory storage * for trust database data * Copyright (c) 2016,2018 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka * Zoltan Fridrich */ #include #include #include #include "message.h" #include "llist.h" #pragma GCC optimize("O3") void list_init(list_t *list) { list->count = 0; list->first = NULL; list->last = NULL; } list_item_t *list_get_first(const list_t *list) { return list->first; } static list_item_t * create_item(const char *index, const char *data) { list_item_t *item = malloc(sizeof(list_item_t)); if (!item) { msg(LOG_ERR, "Malloc failed"); return item; } item->index = index; item->data = data; item->next = NULL; return item; } int list_prepend(list_t *list, const char *index, const char *data) { list_item_t *item = create_item(index, data); if (item == NULL) return 1; item->next = list->first; list->first = item; ++list->count; return 0; } int list_append(list_t *list, const char *index, const char *data) { list_item_t *item = create_item(index, data); if (!item) return 1; if (list->first) { list->last->next = item; list->last = item; } else { list->first = item; list->last = item; } ++list->count; return 0; } void list_destroy_item(list_item_t **item) { free((void *)(*item)->index); free((void *)(*item)->data); free((*item)); *item = NULL; } void list_empty(list_t *list) { if (!list->first) return; list_item_t *actual = list->first; list_item_t *next = NULL; for (; actual; actual = next) { next = actual->next; list_destroy_item(&actual); } list_init(list); } // Return 1 if the list contains the string, 0 otherwise int list_contains(const list_t *list, const char *str) { for (list_item_t *lptr = list->first; lptr; lptr = lptr->next) { if (!strcmp(str, lptr->index)) return 1; } return 0; } // Return 1 if an item was removed, 0 otherwise int list_remove(list_t *list, const char *str) { list_item_t *lptr, *prev = NULL; for (lptr = list->first; lptr; lptr = lptr->next) { if (!strcmp(str, lptr->index)) { if (prev) prev->next = lptr->next; else list->first = lptr->next; if (!lptr->next) list->last = prev; --list->count; list_destroy_item(&lptr); return 1; } prev = lptr; } return 0; } void list_merge(list_t *dest, list_t *src) { if (!dest->last) { *dest = *src; } else { dest->last->next = src->first; dest->count += src->count; } list_init(src); } fapolicyd-2.0/src/library/llist.h000066400000000000000000000031301524071644000170600ustar00rootroot00000000000000/* * temporary_db.h - Header file for linked list * Copyright (c) 2018 Red Hat Inc., Durham, North Carolina. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka * Zoltan Fridrich */ #ifndef LLIST_H #define LLIST_H typedef struct item { const void *index; const void *data; struct item *next; } list_item_t; typedef struct list_header { long count; struct item *first; struct item *last; } list_t; void list_init(list_t *list); list_item_t *list_get_first(const list_t *list); int list_prepend(list_t *list, const char *index, const char *data); int list_append(list_t *list, const char *index, const char *data); void list_destroy_item(list_item_t **item); void list_empty(list_t *list); int list_contains(const list_t *list, const char *str); int list_remove(list_t *list, const char *str); void list_merge(list_t *dest, list_t *src); #endif fapolicyd-2.0/src/library/lru.c000066400000000000000000000343711524071644000165410ustar00rootroot00000000000000/* * lru.c - LRU cache implementation * Copyright (c) 2016,2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #include "config.h" #include #include #include "lru.h" #include "message.h" #include "gcc-attributes.h" //#define DEBUG // Local declarations static void dequeue(Queue *queue); static QNode *qnode_alloc(Queue *queue); static void qnode_free(Queue *queue, QNode *node); static Hash *create_hash(unsigned int hsize) { unsigned int i; Hash *hash = malloc(sizeof(Hash)); if (hash == NULL) return hash; hash->array = malloc(hsize * sizeof(QNode*)); if (hash->array == NULL) { free(hash); return NULL; } // Initialize all hash entries as empty for (i = 0; i < hsize; i++) hash->array[i] = NULL; hash->size = hsize; return hash; } static void destroy_hash(Hash *hash) { free(hash->array); free(hash); } /* * qnode_alloc - get a QNode from the pre-allocated pool * @queue: queue managing the pool */ static QNode *qnode_alloc(Queue *queue) { QNode *node; if (queue == NULL) return NULL; node = queue->free_list; if (node == NULL) return NULL; queue->free_list = node->next; node->prev = NULL; node->next = NULL; node->item = NULL; node->uses = 1; // Setting to 1 because its being used return node; } /* * qnode_free - return a QNode to the pre-allocated pool * @queue: queue managing the pool * @node: node to return */ static void qnode_free(Queue *queue, QNode *node) { if (queue == NULL || node == NULL) return; node->item = NULL; node->uses = 0; node->prev = NULL; node->next = queue->free_list; queue->free_list = node; } static void dump_queue_stats(const Queue *q) { unsigned int count = atomic_load_explicit(&q->count, memory_order_relaxed); unsigned long hits = atomic_load_explicit(&q->hits, memory_order_relaxed); unsigned long misses = atomic_load_explicit(&q->misses, memory_order_relaxed); unsigned long collisions = atomic_load_explicit(&q->collisions, memory_order_relaxed); unsigned long evictions = atomic_load_explicit(&q->evictions, memory_order_relaxed); msg(LOG_DEBUG, "%s cache size: %u", q->name, q->total); msg(LOG_DEBUG, "%s slots in use: %u (%u%%)", q->name, count, q->total ? (100*count)/q->total : 0); msg(LOG_DEBUG, "%s hits: %lu", q->name, hits); msg(LOG_DEBUG, "%s misses: %lu", q->name, misses); msg(LOG_DEBUG, "%s collisions: %lu", q->name, collisions); msg(LOG_DEBUG, "%s evictions: %lu (%lu%%)", q->name, evictions, hits ? (100*evictions)/hits : 0); } static Queue *create_queue(unsigned int qsize, const char *name) { unsigned int i; Queue *queue = malloc(sizeof(Queue)); if (queue == NULL) return queue; // The queue is empty atomic_init(&queue->count, 0); atomic_init(&queue->hits, 0); atomic_init(&queue->misses, 0); atomic_init(&queue->collisions, 0); atomic_init(&queue->evictions, 0); queue->front = queue->end = NULL; // Number of slots that can be stored in memory queue->total = qsize; queue->name = name; queue->cleanup = NULL; queue->evict_cb = NULL; queue->free_list = NULL; queue->pool = malloc(qsize * sizeof(QNode)); if (queue->pool == NULL) { free(queue); return NULL; } for (i = 0; i < qsize; i++) qnode_free(queue, &queue->pool[i]); return queue; } static void destroy_queue(Queue *queue) { dump_queue_stats(queue); // Some static analysis scanners try to flag this as a use after // free accessing queue->end. This is a false positive. It is freed. // However, static analysis apps are incapable of seeing that in // remove_node, end is updated to a prior node as part of detaching // the current end node. while (atomic_load_explicit(&queue->count, memory_order_relaxed)) dequeue(queue); free(queue->pool); free(queue); } static unsigned int are_all_slots_full(const Queue *queue) { return atomic_load_explicit(&queue->count, memory_order_relaxed) == queue->total; } static unsigned int queue_is_empty(const Queue *queue) { return queue->end == NULL; } #ifdef DEBUG static void sanity_check_queue(Queue *q, const char *id) { unsigned int i; QNode *n; if (q == NULL) { msg(LOG_DEBUG, "%s - q is NULL", id); abort(); } n = q->front; if (n == NULL) return; // Walk bottom to top i = 0; while (n->next) { if (n->next->prev != n) { msg(LOG_DEBUG, "%s - corruption found %u", id, i); abort(); } if (i == atomic_load_explicit(&q->count, memory_order_relaxed)) { msg(LOG_DEBUG, "%s - forward loop found %u", id, i); abort(); } i++; n = n->next; } // Walk top to bottom n = q->end; while (n->prev) { if (n->prev->next != n) { msg(LOG_DEBUG, "%s - Corruption found %u", id, i); abort(); } if (i == 0) { msg(LOG_DEBUG, "%s - backward loop found %u", id, i); abort(); } i--; n = n->prev; } } #else #define sanity_check_queue(a, b) do {} while(0) #endif static void insert_before(Queue *queue, QNode *node, QNode *new_node) { sanity_check_queue(queue, "1 insert_before"); if (queue == NULL || node == NULL || new_node == NULL) return; new_node->prev = node->prev; new_node->next = node; if (node->prev == NULL) queue->front = new_node; else node->prev->next = new_node; node->prev = new_node; sanity_check_queue(queue, "2 insert_before"); } static void insert_beginning(Queue *queue, QNode *new_node) { sanity_check_queue(queue, "1 insert_beginning"); if (queue == NULL || new_node == NULL) return; if (queue->front == NULL) { queue->front = new_node; queue->end = new_node; new_node->prev = NULL; new_node->next = NULL; } else insert_before(queue, queue->front, new_node); sanity_check_queue(queue, "2 insert_beginning"); } static void remove_node(Queue *queue, const QNode *node) { // If we are at the beginning sanity_check_queue(queue, "1 remove_node"); if (node->prev == NULL) { queue->front = node->next; if (queue->front) queue->front->prev = NULL; else queue->end = NULL; goto out; } else { if (node->prev->next != node) { /* * The LRU cache owns enforcement objects. Once neighbor * links disagree, continuing can detach or free the * wrong object, so this remains an invariant failure. */ msg(LOG_ERR, "Linked list corruption detected %s", queue->name); abort(); } node->prev->next = node->next; } // If we are at the end if (node->next == NULL) { queue->end = node->prev; if (queue->end) queue->end->next = NULL; } else { if (node->next->prev != node) { /* * The LRU cache owns enforcement objects. Once neighbor * links disagree, continuing can detach or free the * wrong object, so this remains an invariant failure. */ msg(LOG_ERR, "Linked List corruption detected %s", queue->name); abort(); } node->next->prev = node->prev; } out: sanity_check_queue(queue, "2 remove_node"); } // Remove from the end of the queue static void dequeue(Queue *queue) { if (queue_is_empty(queue)) return; QNode *temp = queue->end; remove_node(queue, queue->end); // Let caller know an entry is being evicted if (queue->evict_cb) queue->evict_cb(temp->item); queue->cleanup(temp->item); free(temp->item); qnode_free(queue, temp); // decrement the total of full slots by 1 atomic_fetch_sub_explicit(&queue->count, 1, memory_order_relaxed); } /* * lru_evict - remove the cache entry that should be at the front of the * queue * @queue: pointer to the LRU queue * @key: hash index for the entry to evict * * The caller must first move the desired entry to the front of the queue by * calling check_lru_cache() with the same key. This ensures that @key refers * to the front node. If the node at the front does not match @key, the * program will abort as this is a usage error. */ void lru_evict(Queue *queue, unsigned int key) { if (queue_is_empty(queue)) return; if (key >= queue->total) { msg(LOG_ERR, "lru_evict called with out of bounds key"); return; } Hash *hash = queue->hash; QNode *temp = queue->front; if (hash->array[key] != temp) { /* * Key-to-node mismatch means the hash table and eviction list * no longer describe the same cache. Evicting after that could * corrupt enforcement state, so stop at the invariant failure. */ msg(LOG_ERR, "lru_evict called with mismatched key %s", queue->name); abort(); } hash->array[key] = NULL; remove_node(queue, queue->front); // Let caller know an entry is being evicted if (queue->evict_cb) queue->evict_cb(temp->item); queue->cleanup(temp->item); free(temp->item); qnode_free(queue, temp); // decrement the total of full slots by 1 atomic_fetch_sub_explicit(&queue->count, 1, memory_order_relaxed); atomic_fetch_add_explicit(&queue->evictions, 1, memory_order_relaxed); } /* * lru_record_collision - count an unusable cache entry for this key. * @queue: cache queue whose lookup collided. * * The LRU layer indexes by a bounded hash key and callers compare the * complete subject or object identity after lookup. Callers use this counter * when a populated slot mapped by the key has to be evicted because that * complete identity did not match. */ void lru_record_collision(Queue *queue) { if (queue) atomic_fetch_add_explicit(&queue->collisions, 1, memory_order_relaxed); } /* * lru_peek_slot - inspect a cache slot without changing LRU order. * @queue: cache queue to inspect. * @key: hash index to read. * * Returns the node currently stored at @key, or NULL if the slot is empty or * @key is outside the cache. Callers must not free or relink the returned * node. */ QNode *lru_peek_slot(const Queue *queue, unsigned int key) { if (queue == NULL || key >= queue->total) return NULL; return queue->hash->array[key]; } // Make a new entry with item to be assigned later // and setup the hash key static void enqueue(Queue *queue, unsigned int key) { QNode *temp; Hash *hash = queue->hash; // If all slots are full, remove the page at the end if (are_all_slots_full(queue)) { // remove page from hash hash->array[key] = NULL; dequeue(queue); } // Create a new node with given page total, // And add the new node to the front of queue temp = qnode_alloc(queue); if (temp == NULL) { msg(LOG_ERR, "Unable to allocate node for %s", queue->name); return; } insert_beginning(queue, temp); hash->array[key] = temp; // increment number of full slots atomic_fetch_add_explicit(&queue->count, 1, memory_order_relaxed); } // This function is called needing an item from cache. // There are two scenarios: // 1. Item is not in cache, so add it to the front of the queue // 2. Item is in cache, we move the item to front of queue QNode *check_lru_cache(Queue *queue, unsigned int key) { QNode *reqPage; Hash *hash = queue->hash; // Check for out of bounds key if (key >= queue->total) { return NULL; } reqPage = hash->array[key]; // item is not in cache, make new spot for it if (reqPage == NULL) { enqueue(queue, key); atomic_fetch_add_explicit(&queue->misses, 1, memory_order_relaxed); // item is there but not at front. Move it } else if (reqPage != queue->front) { remove_node(queue, reqPage); reqPage->next = NULL; reqPage->prev = NULL; insert_beginning(queue, reqPage); // Increment cached object metrics queue->front->uses++; atomic_fetch_add_explicit(&queue->hits, 1, memory_order_relaxed); } else atomic_fetch_add_explicit(&queue->hits, 1, memory_order_relaxed); return queue->front; } Queue *init_lru(unsigned int qsize, void (*cleanup)(void *), const char *name, void (*evict_cb)(void *)) { Queue *q = create_queue(qsize, name); if (q == NULL) return q; q->cleanup = cleanup; q->evict_cb = evict_cb; q->hash = create_hash(qsize); return q; } void destroy_lru(Queue *queue) { if (queue == NULL) return; destroy_hash(queue->hash); destroy_queue(queue); } /* * lru_metrics_snapshot - copy cache counters, optionally resetting them. * @queue: cache queue to read. * @metrics: destination metrics snapshot. * @reset: non-zero resets interval counters after copying them. * * Cache occupancy and capacity are state values and are never reset. * A missing queue produces an empty "Unknown" snapshot so callers can report * partial startup or teardown state without reading uninitialized data. */ void lru_metrics_snapshot(Queue *queue, struct lru_metrics *metrics, int reset) { if (metrics == NULL) return; memset(metrics, 0, sizeof(*metrics)); metrics->name = "Unknown"; if (queue == NULL) return; metrics->name = queue->name ? queue->name : "Unknown"; metrics->count = atomic_load_explicit(&queue->count, memory_order_relaxed); metrics->total = queue->total; metrics->hits = atomic_load_explicit(&queue->hits, memory_order_relaxed); metrics->misses = atomic_load_explicit(&queue->misses, memory_order_relaxed); metrics->collisions = atomic_load_explicit(&queue->collisions, memory_order_relaxed); metrics->evictions = atomic_load_explicit(&queue->evictions, memory_order_relaxed); if (reset) { metrics->hits = atomic_exchange_explicit(&queue->hits, 0, memory_order_relaxed); metrics->misses = atomic_exchange_explicit(&queue->misses, 0, memory_order_relaxed); metrics->collisions = atomic_exchange_explicit( &queue->collisions, 0, memory_order_relaxed); metrics->evictions = atomic_exchange_explicit( &queue->evictions, 0, memory_order_relaxed); } } unsigned int compute_subject_key(const Queue *queue, unsigned int pid) { if (queue) return pid % queue->hash->size; else return 0; } unsigned long compute_object_key(const Queue *queue, unsigned long num) { if (queue) return num % queue->hash->size; else return 0; } fapolicyd-2.0/src/library/lru.h000066400000000000000000000054661524071644000165510ustar00rootroot00000000000000/* * lru.h - Header file for lru.c * Copyright (c) 2016 Red Hat Inc., Durham, North Carolina. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef LRU_HEADER #define LRU_HEADER #include #include "gcc-attributes.h" // Queue is implemented using double linked list typedef struct QNode { struct QNode *prev; struct QNode *next; unsigned long uses; void *item; // the data in the cache } QNode; // Collection of pointers to Queue Nodes typedef struct Hash { unsigned int size; // how many entries QNode **array; // an array of queue nodes } Hash; // FIFO of Queue Nodes typedef struct Queue { atomic_uint count; // Number of filled slots unsigned int total; // total number of slots atomic_ulong hits; // Number of times object was in cache atomic_ulong misses;// number of times object was not in cache atomic_ulong collisions;// cached object was unusable for this key atomic_ulong evictions;// number of times cached object was not usable QNode *front; QNode *end; Hash *hash; const char *name; // Used for reporting void (*cleanup)(void *); // Function to call when releasing memory void (*evict_cb)(void *); // Optional callback when evicting item QNode *pool; // Pre-allocated queue nodes QNode *free_list; // Free list for queue nodes } Queue; struct lru_metrics { const char *name; unsigned int count; unsigned int total; unsigned long hits; unsigned long misses; unsigned long collisions; unsigned long evictions; }; void destroy_lru(Queue *queue); Queue *init_lru(unsigned int qsize, void (*cleanup)(void *), const char *name, void (*evict_cb)(void *)) __attribute_malloc__ __attr_dealloc (destroy_lru, 1); void lru_metrics_snapshot(Queue *queue, struct lru_metrics *metrics, int reset); void lru_record_collision(Queue *queue); void lru_evict(Queue *queue, unsigned int key); QNode *lru_peek_slot(const Queue *queue, unsigned int key); QNode *check_lru_cache(Queue *q, unsigned int key); unsigned int compute_subject_key(const Queue *queue, unsigned int pid); unsigned long compute_object_key(const Queue *queue, unsigned long num); #endif fapolicyd-2.0/src/library/md5-backend.c000066400000000000000000000126421524071644000200060ustar00rootroot00000000000000/* * md5-backend.c - functions for adding files to the trust database * based on MD5 hashes. * Copyright (c) 2023-26 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Stephen Tridgell * Matt Jolly */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include "file.h" #include "fapolicyd-backend.h" #include "message.h" #include "md5-backend.h" /* * Given a path to a file with an expected MD5 digest, add * the file to the trust database if it matches. * * Dpkg does not provide sha256 sums or file sizes to verify against. * The only source for verification is MD5. The logic implemented is: * 1) Calculate the MD5 sum and compare to the expected hash. If it does * not match, skip the file. * 2) Calculate the SHA256 and file size on the local files. * 3) Add to database. * * Security considerations: * An attacker would need to craft a file with a MD5 hash collision. * While MD5 is considered broken, this is still some effort. * This function would compute a sha256 and file size on the attackers * crafted file so they do not secure this backend. * * Returns MD5_BACKEND_ADDED when the file was recorded or already present, * MD5_BACKEND_SKIPPED for per-file conditions expected from dpkg state drift, * and MD5_BACKEND_FATAL when the backend snapshot is unsafe to use. */ md5_backend_result_t add_file_to_backend_by_md5(const char *path, const char *expected_md5, struct _hash_record **hashtable, trust_src_t trust_src, backend *dstbackend) { if (path == NULL || expected_md5 == NULL || hashtable == NULL || dstbackend == NULL || dstbackend->memfd < 0) { msg(LOG_ERR, "Invalid MD5 backend destination for %s", path ? path : "(null)"); return MD5_BACKEND_FATAL; } #ifdef DEBUG msg(LOG_DEBUG, "Adding %s", path); msg(LOG_DEBUG, "\tExpected MD5: %s", expected_md5); #endif int fd = open(path, O_RDONLY|O_NOFOLLOW); struct stat path_stat; if (fd < 0) { if (errno != ELOOP) // Don't report symlinks as a warning msg(LOG_WARNING, "Could not open %s, %s", path, strerror(errno)); return MD5_BACKEND_SKIPPED; } if (fstat(fd, &path_stat)) { close(fd); msg(LOG_WARNING, "fstat file %s failed %s", path, strerror(errno)); return MD5_BACKEND_SKIPPED; } // If its not a regular file, skip. if (!S_ISREG(path_stat.st_mode)) { close(fd); msg(LOG_DEBUG, "Not regular file %s", path); return MD5_BACKEND_SKIPPED; } size_t file_size = path_stat.st_size; trustdb_size_t stored_size; if (trustdb_size_from_unsigned(file_size, &stored_size)) { close(fd); msg(LOG_ERR, "File size for %s exceeds trust DB format", path); return MD5_BACKEND_FATAL; } #ifdef DEBUG msg(LOG_DEBUG, "\tFile size: %zu", file_size); #endif char *md5_digest = get_hash_from_fd2(fd, file_size, FILE_HASH_ALG_MD5); if (md5_digest == NULL) { close(fd); msg(LOG_ERR, "MD5 digest returned NULL"); return MD5_BACKEND_FATAL; } if (strcmp(md5_digest, expected_md5) != 0) { msg(LOG_WARNING, "Skipping %s: hash mismatch. Got %s, expected %s", path, md5_digest, expected_md5); close(fd); free(md5_digest); return MD5_BACKEND_SKIPPED; } free(md5_digest); // It's OK so create a sha256 of the file char *sha_digest = get_hash_from_fd2(fd, file_size, FILE_HASH_ALG_SHA256); close(fd); if (sha_digest == NULL) { msg(LOG_ERR, "Sha digest returned NULL"); return MD5_BACKEND_FATAL; } char *data; if (asprintf(&data, DATA_FORMAT, trust_src, stored_size, sha_digest) == -1) { msg(LOG_ERR, "Out of memory formatting trust data for %s", path); free(sha_digest); return MD5_BACKEND_FATAL; } free(sha_digest); // Getting rid of the duplicates. struct _hash_record *rcd = NULL; char *key = NULL; if (asprintf(&key, "%s %s", path, data) == -1) { msg(LOG_ERR, "Out of memory tracking %s", path); free((void *)data); return MD5_BACKEND_FATAL; } HASH_FIND_STR(*hashtable, key, rcd); if (!rcd) { rcd = (struct _hash_record *)malloc( sizeof(struct _hash_record)); if (rcd == NULL) { msg(LOG_ERR, "Out of memory tracking %s", path); free(key); free((void *)data); return MD5_BACKEND_FATAL; } rcd->key = key; if (dprintf(dstbackend->memfd, "%s %s\n", path, data) < 0) { msg(LOG_ERR, "dprintf failed writing %s to memfd (%s)", path, strerror(errno)); free(key); free(rcd); free((void *)data); return MD5_BACKEND_FATAL; } HASH_ADD_KEYPTR(hh, *hashtable, rcd->key, strlen(rcd->key), rcd); } else { free(key); } free((void *)data); return MD5_BACKEND_ADDED; } fapolicyd-2.0/src/library/md5-backend.h000066400000000000000000000026571524071644000200200ustar00rootroot00000000000000/* * md5-backend.h - header file for md5-backend.c * Copyright (c) 2023-24,2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Stephen Tridgell * Matt Jolly */ #ifndef MD5_BACKEND_HEADER #define MD5_BACKEND_HEADER #include #include "fapolicyd-backend.h" struct _hash_record { const char *key; UT_hash_handle hh; }; static const int kMd5HexSize = 32; typedef enum { MD5_BACKEND_FATAL = -1, MD5_BACKEND_ADDED = 0, MD5_BACKEND_SKIPPED = 1, } md5_backend_result_t; md5_backend_result_t add_file_to_backend_by_md5(const char *path, const char *expected_md5, struct _hash_record **hashtable, trust_src_t trust_src, backend *dstbackend); #endif fapolicyd-2.0/src/library/message.c000066400000000000000000000357041524071644000173640ustar00rootroot00000000000000/* * message.c - function to syslog or write to stderr * Copyright (c) 2016 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "message.h" /* The message mode refers to where informational messages go 0 - stderr, 1 - syslog, 2 - quiet. The default is quiet. */ static message_t message_mode = MSG_QUIET; static debug_message_t debug_message = DBG_NO; static atomic_int stderr_color_state = ATOMIC_VAR_INIT(-1); /* journald can deadlock with fanotify events, so no thread that answers * events may write to syslog/stderr directly. Producers enqueue here * instead (full ring == drop + count) and one drain thread does the writes. */ #define LOG_SLOT_SIZE (PATH_MAX * 2) #define LOG_QUEUE_DEPTH 256 enum { LOG_ASYNC_OFF, LOG_ASYNC_RUNNING, LOG_ASYNC_STOPPING }; struct log_slot { int priority; size_t len; /* bytes actually written */ char text[LOG_SLOT_SIZE]; }; static struct log_slot log_ring[LOG_QUEUE_DEPTH]; static unsigned log_head, log_count; /* guarded by log_lock */ static pthread_mutex_t log_lock = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP; static sem_t log_sem; static pthread_t log_thread; static atomic_int log_state = ATOMIC_VAR_INIT(LOG_ASYNC_OFF); static atomic_ulong log_dropped = ATOMIC_VAR_INIT(0); static struct message_rate_limit log_drop_limit = MESSAGE_RATE_LIMIT_INIT(30); struct message_level { const char *name; const char *color; }; #ifdef __GNUC__ #define MESSAGE_PRINTF(fmt, args) __attribute__ ((format (printf, fmt, args))) #else #define MESSAGE_PRINTF(fmt, args) #endif static void msg_stderr(int priority, const char *fmt, va_list ap) MESSAGE_PRINTF(2, 0); static void msg_stderr_f(int priority, const char *fmt, ...) MESSAGE_PRINTF(2, 3); static void nonblocking_stderr(int priority, const char *fmt, va_list ap) MESSAGE_PRINTF(2, 0); static void nonblocking_syslog(int priority, const char *fmt, va_list ap) MESSAGE_PRINTF(2, 0); static void msg_direct_deliver(int priority, const char *fmt, va_list ap) MESSAGE_PRINTF(2, 0); static void log_enqueue(int priority, const char *fmt, va_list ap) MESSAGE_PRINTF(2, 0); /* * message_level_info - return display metadata for one syslog priority. * @priority: syslog LOG_* priority value. * Returns the printable level name and ANSI color for @priority. */ static struct message_level message_level_info(int priority) { switch (priority) { case LOG_EMERG: return (struct message_level){ "EMERGENCY", "\x1b[31m" }; case LOG_ALERT: return (struct message_level){ "ALERT", "\x1b[35m" }; case LOG_CRIT: return (struct message_level){ "CRITICAL", "\x1b[33m" }; case LOG_ERR: return (struct message_level){ "ERROR", "\x1b[31m" }; case LOG_WARNING: return (struct message_level){ "WARNING", "\x1b[33m" }; case LOG_NOTICE: return (struct message_level){ "NOTICE", "\x1b[32m" }; case LOG_INFO: return (struct message_level){ "INFO", "\x1b[36m" }; case LOG_DEBUG: return (struct message_level){ "DEBUG", "\x1b[34m" }; default: return (struct message_level){ "UNKNOWN", "" }; } } /* * detect_stderr_color - determine whether stderr should receive ANSI color. * Returns 1 for color-capable interactive stderr, 0 otherwise. */ static int detect_stderr_color(void) { const char *no_color = getenv("NO_COLOR"); const char *term = getenv("TERM"); if (no_color && no_color[0] != '\0') return 0; if (term && strcmp(term, "dumb") == 0) return 0; return isatty(fileno(stderr)); } /* * stderr_color_enabled - return the cached stderr color decision. * Returns 1 when color should be used, 0 when plain text should be used. */ static int stderr_color_enabled(void) { for (;;) { int cached = atomic_load_explicit(&stderr_color_state, memory_order_relaxed); int expected = -1; int detected; if (cached != -1) return cached; detected = detect_stderr_color(); if (atomic_compare_exchange_strong_explicit(&stderr_color_state, &expected, detected, memory_order_relaxed, memory_order_relaxed)) return detected; } } /* * msg_stderr - emit one complete formatted message record to stderr. * @priority: syslog LOG_* priority used for level display. * @fmt: printf-style format string. * @ap: argument list for @fmt. * * Returns nothing. The shared stderr stream is locked only while writing the * record so prefix/body/newline output cannot interleave with other threads. */ static void msg_stderr(int priority, const char *fmt, va_list ap) { struct message_level level = message_level_info(priority); time_t rawtime = time(NULL); struct tm timeinfo; char buffer[80]; const char *time_prefix = "time unavailable [ "; const int use_color = stderr_color_enabled(); if (rawtime != (time_t)-1 && localtime_r(&rawtime, &timeinfo) != NULL && strftime(buffer, sizeof(buffer), "%x %T [ ", &timeinfo) != 0) time_prefix = buffer; flockfile(stderr); fputs(time_prefix, stderr); if (use_color) fputs(level.color, stderr); fputs(level.name, stderr); if (use_color) fputs("\x1b[0m", stderr); fputs(" ]: ", stderr); vfprintf(stderr, fmt, ap); fputc('\n', stderr); fflush(stderr); funlockfile(stderr); } static void msg_stderr_f(int priority, const char *fmt, ...) { va_list ap; va_start(ap, fmt); msg_stderr(priority, fmt, ap); va_end(ap); } /* only the drain thread calls this; text may contain '%' so it's passed * through as a literal "%s" body, never as a format string */ static void log_write(int priority, const char *text) { if (message_mode == MSG_SYSLOG) syslog(priority, "%s", text); else msg_stderr_f(priority, "%s", text); } /* nonblocking_stderr - emit one best-effort stderr record without stdio locks. * Returns nothing. Full pipes/sockets and formatting failures drop the record. */ static void nonblocking_stderr(int priority, const char *fmt, va_list ap) { struct message_level level = message_level_info(priority); time_t rawtime = time(NULL); struct tm timeinfo; char timebuf[80]; char body[LOG_SLOT_SIZE]; char record[LOG_SLOT_SIZE + 128]; const char *time_prefix = "time unavailable [ "; int flags = fcntl(STDERR_FILENO, F_GETFL); int body_len; int record_len; if (flags == -1) return; body_len = vsnprintf(body, sizeof(body), fmt, ap); if (body_len < 0) return; if ((size_t)body_len >= sizeof(body)) body_len = sizeof(body) - 1; if (rawtime != (time_t)-1 && localtime_r(&rawtime, &timeinfo) != NULL && strftime(timebuf, sizeof(timebuf), "%x %T [ ", &timeinfo) != 0) time_prefix = timebuf; record_len = snprintf(record, sizeof(record), "%s%s ]: %.*s\n", time_prefix, level.name, body_len, body); if (record_len < 0) return; if ((size_t)record_len >= sizeof(record)) record_len = sizeof(record) - 1; if (fcntl(STDERR_FILENO, F_SETFL, flags | O_NONBLOCK) == -1) return; write(STDERR_FILENO, record, (size_t)record_len); fcntl(STDERR_FILENO, F_SETFL, flags); } /* open_log_socket - connect a nonblocking datagram socket to syslog. * Returns a connected descriptor, or -1 on failure. */ static int open_log_socket(void) { struct sockaddr_un addr; int fd; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; strncpy(addr.sun_path, _PATH_LOG, sizeof(addr.sun_path) - 1); fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_NONBLOCK, 0); if (fd != -1 && connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { close(fd); fd = -1; } return fd; } /* deliver to syslog without ever blocking. vsyslog() has no non-blocking * mode and can stall on a wedged journald - the exact deadlock this whole * async design exists to avoid - so a minimal datagram sender is used * instead; any failure is silently dropped, never retried/waited on */ static void nonblocking_syslog(int priority, const char *fmt, va_list ap) { static atomic_int log_fd = ATOMIC_VAR_INIT(-1); char text[LOG_SLOT_SIZE]; char packet[LOG_SLOT_SIZE + 64]; int fd; int n; n = vsnprintf(text, sizeof(text), fmt, ap); if (n < 0) return; if ((size_t)n >= sizeof(text)) n = sizeof(text) - 1; n = snprintf(packet, sizeof(packet), "<%d>%s[%d]: %.*s", LOG_DAEMON | priority, program_invocation_short_name, getpid(), n, text); if (n < 0) return; if ((size_t)n >= sizeof(packet)) n = sizeof(packet) - 1; fd = atomic_load_explicit(&log_fd, memory_order_relaxed); if (fd == -1) { int newfd = open_log_socket(); int expected = -1; if (newfd == -1) return; if (atomic_compare_exchange_strong_explicit(&log_fd, &expected, newfd, memory_order_relaxed, memory_order_relaxed)) fd = newfd; else { close(newfd); fd = expected; } } if (fd != -1) send(fd, packet, (size_t)n, MSG_DONTWAIT); } /* best-effort, non-blocking delivery shared by msg_direct() and by msg() * whenever the async drain thread isn't running */ static void msg_direct_deliver(int priority, const char *fmt, va_list ap) { if (message_mode == MSG_SYSLOG) nonblocking_syslog(priority, fmt, ap); else nonblocking_stderr(priority, fmt, ap); } /* * msg_direct - log a message immediately, best-effort and non-blocking, * bypassing the async queue entirely. For callers that cannot rely on the * drain thread. */ void msg_direct(int priority, const char *fmt, ...) { va_list ap; if (message_mode == MSG_QUIET) return; if (priority == LOG_DEBUG && debug_message == DBG_NO) return; va_start(ap, fmt); msg_direct_deliver(priority, fmt, ap); va_end(ap); } static void log_enqueue(int priority, const char *fmt, va_list ap) { struct log_slot slot; unsigned tail; int n; slot.priority = priority; n = vsnprintf(slot.text, LOG_SLOT_SIZE, fmt, ap); if (n < 0) { slot.text[0] = 0; n = 0; } else if (n >= LOG_SLOT_SIZE) { static const char mark[] = " [truncated]"; memcpy(slot.text + LOG_SLOT_SIZE - sizeof(mark), mark, sizeof(mark)); n = LOG_SLOT_SIZE - 1; } slot.len = (size_t)n; if (pthread_mutex_lock(&log_lock)) { atomic_fetch_add_explicit(&log_dropped, 1, memory_order_relaxed); return; } if (atomic_load_explicit(&log_state, memory_order_relaxed) != LOG_ASYNC_RUNNING || log_count == LOG_QUEUE_DEPTH) { pthread_mutex_unlock(&log_lock); /* can't report via msg(): that would re-enter this same * full queue and just drop the report too */ atomic_fetch_add_explicit(&log_dropped, 1, memory_order_relaxed); return; } tail = (log_head + log_count) % LOG_QUEUE_DEPTH; log_ring[tail] = slot; log_count++; /* post while still holding the lock so stop() can't destroy log_sem * before this post lands */ sem_post(&log_sem); pthread_mutex_unlock(&log_lock); } static void *log_thread_main(void *arg) { sigset_t sigs; (void)arg; /* worker thread: don't handle external signals */ sigemptyset(&sigs); sigaddset(&sigs, SIGTERM); sigaddset(&sigs, SIGHUP); sigaddset(&sigs, SIGUSR1); sigaddset(&sigs, SIGINT); sigaddset(&sigs, SIGQUIT); pthread_sigmask(SIG_SETMASK, &sigs, NULL); for (;;) { struct log_slot slot; unsigned long dropped; int have = 0; while (sem_wait(&log_sem) && errno == EINTR) ; pthread_mutex_lock(&log_lock); if (log_count) { slot.priority = log_ring[log_head].priority; memcpy(slot.text, log_ring[log_head].text, log_ring[log_head].len + 1); log_head = (log_head + 1) % LOG_QUEUE_DEPTH; log_count--; have = 1; } pthread_mutex_unlock(&log_lock); if (have) log_write(slot.priority, slot.text); /* log_write(), not msg(): this thread never competes for a * ring slot, so its own report can't be dropped */ dropped = atomic_load_explicit(&log_dropped, memory_order_relaxed); if (dropped && message_rate_limit_allow(&log_drop_limit, time(NULL))) { char note[64]; dropped = atomic_exchange_explicit(&log_dropped, 0, memory_order_relaxed); snprintf(note, sizeof(note), "Dropped %lu messages - log queue unavailable", dropped); log_write(LOG_WARNING, note); } if (!have && atomic_load_explicit(&log_state, memory_order_acquire) == LOG_ASYNC_STOPPING) break; } return NULL; } int message_async_start(void) { int rc; if (atomic_load_explicit(&log_state, memory_order_relaxed) != LOG_ASYNC_OFF) return 0; /* file_init() clears MAGIC later in startup. Cache the stderr color * environment before the drain thread can read it concurrently. */ if (message_mode == MSG_STDERR) (void)stderr_color_enabled(); if (sem_init(&log_sem, 0, 0)) return errno; log_head = log_count = 0; rc = pthread_create(&log_thread, NULL, log_thread_main, NULL); if (rc) { sem_destroy(&log_sem); return rc; } atomic_store_explicit(&log_state, LOG_ASYNC_RUNNING, memory_order_release); return 0; } void message_async_stop(void) { if (atomic_load_explicit(&log_state, memory_order_relaxed) != LOG_ASYNC_RUNNING) return; /* Producers recheck log_state while holding log_lock, so no enqueue can * start after this transition reaches LOG_ASYNC_STOPPING. */ pthread_mutex_lock(&log_lock); atomic_store_explicit(&log_state, LOG_ASYNC_STOPPING, memory_order_release); pthread_mutex_unlock(&log_lock); sem_post(&log_sem); /* bounded wait: don't let a wedged journald/syslog hang shutdown */ struct timespec deadline; clock_gettime(CLOCK_REALTIME, &deadline); deadline.tv_sec += 5; if (pthread_timedjoin_np(log_thread, NULL, &deadline) != 0) { /* leak the thread/semaphore rather than destroy them * out from under a still-running thread */ atomic_store_explicit(&log_state, LOG_ASYNC_OFF, memory_order_relaxed); return; } sem_destroy(&log_sem); atomic_store_explicit(&log_state, LOG_ASYNC_OFF, memory_order_relaxed); } void set_message_mode(message_t mode, debug_message_t debug) { message_mode = mode; debug_message = debug; if (mode == MSG_STDERR) atomic_store_explicit(&stderr_color_state, -1, memory_order_relaxed); } void msg(int priority, const char *fmt, ...) { va_list ap; if (message_mode == MSG_QUIET) return; if (priority == LOG_DEBUG && debug_message == DBG_NO) return; va_start(ap, fmt); if (atomic_load_explicit(&log_state, memory_order_relaxed) == LOG_ASYNC_RUNNING) log_enqueue(priority, fmt, ap); else msg_direct_deliver(priority, fmt, ap); va_end(ap); } fapolicyd-2.0/src/library/message.h000066400000000000000000000051671524071644000173710ustar00rootroot00000000000000/* * message.h - Header file for message.c * Copyright (c) 2016 Red Hat Inc., Durham, North Carolina. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef MESSAGE_HEADER #define MESSAGE_HEADER #include #include #include typedef enum { MSG_STDERR, MSG_SYSLOG, MSG_QUIET } message_t; typedef enum { DBG_NO, DBG_YES } debug_message_t; struct message_rate_limit { atomic_long last_log; long interval; }; #define MESSAGE_RATE_LIMIT_INIT(seconds) \ { .last_log = ATOMIC_VAR_INIT(0), .interval = (seconds) } /* * message_rate_limit_allow - test and update a log throttle. * @limit: caller-owned rate limit state. * @now: current wall-clock time, or (time_t)-1 when unavailable. * * Returns 1 when the caller should log, 0 when the message is suppressed. */ static inline int message_rate_limit_allow(struct message_rate_limit *limit, time_t now) { long current, last; if (limit == NULL || limit->interval <= 0 || now == (time_t)-1) return 1; current = (long)now; last = atomic_load_explicit(&limit->last_log, memory_order_relaxed); while (last == 0 || current < last || current - last >= limit->interval) { /* * A wall-clock rollback should emit one message immediately * and reset the stored timestamp to avoid a long silence. */ if (atomic_compare_exchange_weak_explicit(&limit->last_log, &last, current, memory_order_relaxed, memory_order_relaxed)) return 1; } return 0; } void set_message_mode(message_t mode, debug_message_t debug); int message_async_start(void); void message_async_stop(void); void msg(int priority, const char *fmt, ...) #ifdef __GNUC__ __attribute__ ((format (printf, 2, 3))); #else ; #endif /* best-effort, non-blocking - bypasses the async queue entirely */ void msg_direct(int priority, const char *fmt, ...) #ifdef __GNUC__ __attribute__ ((format (printf, 2, 3))); #else ; #endif #endif fapolicyd-2.0/src/library/nv.h000066400000000000000000000022021524071644000163530ustar00rootroot00000000000000/* * nv.h - Header file for name value struct * Copyright (c) 2016 Red Hat Inc., Durham, North Carolina. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef NV_HEADER #define NV_HEADER #define SUBJ_START 0 #define OBJ_START 16 typedef struct nv { unsigned int value; const char *name; }nv_t; typedef struct nv_list { const char *name; int item; }nvlist_t; #endif fapolicyd-2.0/src/library/object-attr.c000066400000000000000000000040411524071644000201440ustar00rootroot00000000000000/* * object-attr.c - abstract object attribute access * Copyright (c) 2016,2019 Red Hat Inc., Durham, North Carolina. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * Radovan Sroka */ #include "config.h" #include // For NULL #include #include #include "message.h" #include "object-attr.h" static const nv_t table[] = { { ALL_OBJ, "all" }, { PATH, "path" }, { ODIR, "dir" }, { DEVICE, "device" }, { FTYPE, "ftype" }, { OBJ_TRUST, "trust"}, { FILE_HASH, "filehash" }, }; #define MAX_OBJECTS (sizeof(table)/sizeof(table[0])) int obj_name_to_val(const char *name) { static bool warned; // Accept the legacy name for compatibility with older rule sets. if (strcmp(name, "sha256hash") == 0) { /* Announce deprecation once per start while keeping the alias usable. */ if (!warned) { msg(LOG_NOTICE, "sha256hash object name is deprecated; use filehash instead"); warned = true; } return FILE_HASH; } unsigned int i = 0; while (i < MAX_OBJECTS) { if (strcmp(name, table[i].name) == 0) return table[i].value; i++; } return -1; } const char *obj_val_to_name(unsigned int v) { if (v < OBJ_START || v > OBJ_END) return NULL; unsigned int index = v - OBJ_START; if (index < MAX_OBJECTS) return table[index].name; return NULL; } fapolicyd-2.0/src/library/object-attr.h000066400000000000000000000030721524071644000201540ustar00rootroot00000000000000/* * object-attr.h - Header file for object-attr.c * Copyright (c) 2016 Red Hat Inc., Durham, North Carolina. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * Radovan Sroka */ #ifndef OBJECT_ATTR_HEADER #define OBJECT_ATTR_HEADER #include #include "nv.h" #include "attr-sets.h" typedef enum { ALL_OBJ = OBJ_START, PATH, ODIR, DEVICE, FTYPE, OBJ_TRUST, FILE_HASH } object_type_t; #define OBJ_END FILE_HASH #define OBJ_COUNT (OBJ_END - OBJ_START + 1) // Retain SHA256HASH as a public alias for backward compatibility #define SHA256HASH FILE_HASH typedef struct o { object_type_t type; int val; // holds trust value char *o; // Everything is a string union { attr_sets_entry_t * set; }; } object_attr_t; int obj_name_to_val(const char *name); const char *obj_val_to_name(unsigned int v); #endif fapolicyd-2.0/src/library/object.c000066400000000000000000000060251524071644000172000ustar00rootroot00000000000000/* * object.c - Minimal linked list set of object attributes * Copyright (c) 2016 Red Hat Inc., Durham, North Carolina. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #include "config.h" #include #include #include #include "policy.h" #include "object.h" #include "message.h" //#define DEBUG int object_create(o_array *a) { if (a == NULL) return 1; a->cnt = 0; a->info = NULL; a->obj = calloc(OBJ_COUNT, sizeof(object_attr_t *)); if (a->obj == NULL) return 1; return 0; } #ifdef DEBUG static void sanity_check_array(const o_array *a, const char *id) { int i; unsigned int num = 0; for (i = 0; i < OBJ_COUNT; i++) if (a->obj[i]) num++; if (num != a->cnt) { msg(LOG_DEBUG, "%s - array corruption %u!=%u", id, num, a->cnt); abort(); } } #else #define sanity_check_array(a, b) do {} while(0) #endif object_attr_t *object_access(const o_array *a, object_type_t t) { if (a == NULL || a->obj == NULL) return NULL; sanity_check_array(a, "object_access"); if (t >= OBJ_START && t <= OBJ_END) return a->obj[t - OBJ_START]; else return NULL; } // Returns 1 on failure and 0 on success int object_add(o_array *a, const object_attr_t *obj) { object_attr_t *newnode; object_attr_t **slot; if (a == NULL || a->obj == NULL) return 1; sanity_check_array(a, "object_add 1"); if (obj) { if (obj->type >= OBJ_START && obj->type <= OBJ_END) { newnode = malloc(sizeof(object_attr_t)); if (newnode == NULL) return 1; newnode->type = obj->type; newnode->o = obj->o; newnode->val = obj->val; } else return 1; } else return 1; slot = &a->obj[obj->type - OBJ_START]; if (*slot) { free((*slot)->o); free(*slot); } else a->cnt++; *slot = newnode; return 0; } object_attr_t *object_find_file(const o_array *a) { if (a == NULL || a->obj == NULL) return NULL; sanity_check_array(a, "object_find_file"); if (a->obj[PATH - OBJ_START]) return a->obj[PATH - OBJ_START]; else return a->obj[ODIR - OBJ_START]; } void object_clear(o_array *a) { int i; object_attr_t *current; if (a == NULL) return; if (a->obj) { for (i = 0; i < OBJ_COUNT; i++) { current = a->obj[i]; if (current == NULL) continue; free(current->o); free(current); a->obj[i] = NULL; } free(a->obj); } free(a->info); a->info = NULL; a->obj = NULL; a->cnt = 0; } fapolicyd-2.0/src/library/object.h000066400000000000000000000030021524071644000171750ustar00rootroot00000000000000/* * object.h - Header file for object.c * Copyright (c) 2016,2019 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef OBJECT_HEADER #define OBJECT_HEADER #include "object-attr.h" #include "file.h" /* This is the linked list head. Only data elements that are 1 per * event goes here. */ typedef struct { object_attr_t **obj; // Object array unsigned int cnt; // How many items in this list struct file_info *info; // unique file fingerprint } o_array; int object_create(o_array *a); object_attr_t *object_access(const o_array *a, object_type_t t); int object_add(o_array *a, const object_attr_t *obj); object_attr_t *object_find_file(const o_array *a); void object_clear(o_array *a); static inline int type_is_obj(int type) {if (type >= OBJ_START) return 1; else return 0;} #endif fapolicyd-2.0/src/library/paths.h000066400000000000000000000036531524071644000170620ustar00rootroot00000000000000/* globals.h - Constant paths used throughout fapolicyd * Copyright 2022 Red Hat Inc. * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Authors: * Steve Grubb * */ #ifndef GLOBALS_H #define GLOBALS_H #define CONFIG_FILE "/etc/fapolicyd/fapolicyd.conf" #define OLD_RULES_FILE "/etc/fapolicyd/fapolicyd.rules" #define RULES_FILE "/etc/fapolicyd/compiled.rules" #define LANGUAGE_RULES_FILE "/etc/fapolicyd/rules.d/10-languages.rules" #define MOUNTS_FILE "/proc/mounts" #define TRUST_DIR_PATH "/etc/fapolicyd/trust.d/" #define TRUST_FILE_PATH "/etc/fapolicyd/fapolicyd.trust" #define DB_DIR "/var/lib/fapolicyd" #define DB_NAME "trust.db" #define REPORT "/var/log/fapolicyd-access.log" #define RUN_DIR "/run/fapolicyd/" #define STAT_REPORT "/run/fapolicyd/fapolicyd.state" #define METRICS_REPORT "/run/fapolicyd/fapolicyd.metrics" #define TIMING_REPORT "/run/fapolicyd/fapolicyd.timing" #define fifo_path "/run/fapolicyd/fapolicyd.fifo" #define pidfile RUN_DIR "fapolicyd.pid" #define OLD_FILTER_FILE "/etc/fapolicyd/rpm-filter.conf" #define FILTER_FILE "/etc/fapolicyd/fapolicyd-filter.conf" #define MAGIC_PATH "/usr/share/fapolicyd/fapolicyd-magic.mgc" #endif fapolicyd-2.0/src/library/policy-metrics.c000066400000000000000000000276151524071644000207050ustar00rootroot00000000000000/* * policy-metrics.c - policy decision counters and default-allow details * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #include "config.h" #include #include #include #include "decision-context.h" #include "event.h" #include "policy-metrics.h" static atomic_uint ruleset_generation; static atomic_llong ruleset_effective_since; /* * policy_metrics_record_ruleset_update - publish active policy generation. * @generation: policy snapshot generation just published. * @effective_since: time the policy snapshot became active. * Returns nothing. */ void policy_metrics_record_ruleset_update(unsigned int generation, time_t effective_since) { atomic_store_explicit(&ruleset_effective_since, (long long)effective_since, memory_order_relaxed); atomic_store_explicit(&ruleset_generation, generation, memory_order_relaxed); } /* * cached_object_attr - read an object attribute without lazy materialization * @e: event whose cached object attribute list should be inspected. * @type: object attribute type to read. * Returns the cached attribute, or NULL when the event never needed it. */ static object_attr_t *cached_object_attr(const event_t *e, object_type_t type) { if (!e || !e->o) return NULL; return object_access(e->o, type); } /* * ftype_is_programmatic - classify ftypes commonly loaded by interpreters * @ftype: MIME type reported for the object. * Returns 1 when @ftype names language source, bytecode, jars, or scripts. */ static int ftype_is_programmatic(const char *ftype) { if (strncmp(ftype, "text/x-", 7) == 0) return 1; if (strstr(ftype, "javascript")) return 1; if (strstr(ftype, "bytecode")) return 1; if (strstr(ftype, "script")) return 1; if (strcmp(ftype, "application/java-archive") == 0) return 1; if (strcmp(ftype, "application/x-java-applet") == 0) return 1; if (strcmp(ftype, "application/x-elc") == 0) return 1; return 0; } /* * count_fallthrough_ftype - bucket cached object ftype for reporting * @counters: decision counter block to update. * @e: event whose object ftype should be classified if it is already cached. * Returns nothing. */ static void count_fallthrough_ftype(struct decision_policy_counters *counters, event_t *e) { object_attr_t *ftype = cached_object_attr(e, FTYPE); const char *name = ftype ? ftype->o : NULL; if (!name || name[0] == 0) { atomic_fetch_add_explicit( &counters->fallthrough_unknown_ftype, 1, memory_order_relaxed); return; } if (strcmp(name, "application/x-sharedlib") == 0) { atomic_fetch_add_explicit(&counters->fallthrough_sharedlib, 1, memory_order_relaxed); return; } if (strstr(name, "executable") || strcmp(name, "application/x-bad-elf") == 0) { atomic_fetch_add_explicit(&counters->fallthrough_executable, 1, memory_order_relaxed); return; } if (ftype_is_programmatic(name)) { atomic_fetch_add_explicit( &counters->fallthrough_programmatic, 1, memory_order_relaxed); return; } atomic_fetch_add_explicit(&counters->fallthrough_other_ftype, 1, memory_order_relaxed); } /* * count_fallthrough_details - record low-cardinality default-allow dimensions * @counters: decision counter block to update. * @e: event that reached the no-opinion allow path. * Returns nothing. */ static void count_fallthrough_details( struct decision_policy_counters *counters, event_t *e) { object_attr_t *trust; if (e->type & FAN_OPEN_EXEC_PERM) atomic_fetch_add_explicit(&counters->fallthrough_execute, 1, memory_order_relaxed); else atomic_fetch_add_explicit(&counters->fallthrough_open, 1, memory_order_relaxed); /* * Decision metrics run before the fanotify response is written. Use only * cached attributes from policy evaluation; get_obj_attr() can perform * trust database, integrity hash, and MIME/libmagic work here. */ trust = cached_object_attr(e, OBJ_TRUST); if (!trust) atomic_fetch_add_explicit( &counters->fallthrough_trust_unknown, 1, memory_order_relaxed); else if (trust->val) atomic_fetch_add_explicit(&counters->fallthrough_trusted, 1, memory_order_relaxed); else atomic_fetch_add_explicit(&counters->fallthrough_untrusted, 1, memory_order_relaxed); count_fallthrough_ftype(counters, e); } /* * count_allow_source - record whether an allow came from a rule or fallback * @counters: decision counter block to update. * @e: event that was allowed. * @source: source reported by process_event_with_source(). * Returns nothing. */ static void count_allow_source(struct decision_policy_counters *counters, event_t *e, decision_source_t source) { if (source == DECISION_SOURCE_RULE) { atomic_fetch_add_explicit(&counters->allowed_by_rule, 1, memory_order_relaxed); return; } atomic_fetch_add_explicit(&counters->allowed_by_fallthrough, 1, memory_order_relaxed); if (e) count_fallthrough_details(counters, e); } /* * policy_metrics_record_decision - count a policy decision. * @decision: decision returned by policy evaluation. * @e: event used for allow detail bucketing, or NULL when unavailable. * @source: whether the allow came from a rule or default fallthrough. * Returns nothing. */ void policy_metrics_record_decision(decision_t decision, event_t *e, decision_source_t source) { struct decision_policy_counters *counters = &decision_context_current()->policy_counters; if ((decision & DENY) == DENY) { atomic_fetch_add_explicit(&counters->denied, 1, memory_order_relaxed); return; } atomic_fetch_add_explicit(&counters->allowed, 1, memory_order_relaxed); count_allow_source(counters, e, source); } /* * getAllowed - copy the lifetime allowed counter. * Returns the current allowed decision count. */ unsigned long getAllowed(void) { return getAllowedReset(0); } /* * getDenied - copy the lifetime denied counter. * Returns the current denied decision count. */ unsigned long getDenied(void) { return getDeniedReset(0); } /* * policy_counter_snapshot - copy one policy counter and optionally reset it. * @counter: atomic counter to read. * @reset: non-zero resets the counter after copying it. * Returns the copied counter value. */ static unsigned long policy_counter_snapshot(atomic_ulong *counter, int reset) { if (reset) return atomic_exchange_explicit(counter, 0, memory_order_relaxed); return atomic_load_explicit(counter, memory_order_relaxed); } enum policy_counter_id { POLICY_COUNTER_ALLOWED, POLICY_COUNTER_DENIED, }; struct policy_counter_snapshot { enum policy_counter_id id; unsigned long total; int reset; }; struct decision_metrics_snapshot { decision_metrics_t *metrics; int reset; }; /* * policy_counter_from_context - select one lifetime counter from a context. * @counters: worker-local policy counter block. * @id: counter requested by a public getter. * * Returns the requested atomic counter, or NULL if @id is not valid. */ static atomic_ulong *policy_counter_from_context( struct decision_policy_counters *counters, enum policy_counter_id id) { switch (id) { case POLICY_COUNTER_ALLOWED: return &counters->allowed; case POLICY_COUNTER_DENIED: return &counters->denied; default: return NULL; } } /* * policy_counter_snapshot_context - add one worker counter to an aggregate. * @ctx: worker context being sampled. * @data: struct policy_counter_snapshot aggregate. * Returns nothing. */ static void policy_counter_snapshot_context(struct decision_context *ctx, void *data) { struct policy_counter_snapshot *snapshot = data; atomic_ulong *counter; if (ctx == NULL || snapshot == NULL) return; counter = policy_counter_from_context(&ctx->policy_counters, snapshot->id); if (counter) snapshot->total += policy_counter_snapshot(counter, snapshot->reset); } /* * policy_counter_total_reset - aggregate one policy counter across workers. * @id: counter to copy. * @reset: non-zero resets counters after copying them. * Returns the aggregate counter value. */ static unsigned long policy_counter_total_reset(enum policy_counter_id id, int reset) { struct policy_counter_snapshot snapshot = { .id = id, .reset = reset, }; decision_context_for_each(policy_counter_snapshot_context, &snapshot); return snapshot.total; } /* * getAllowedReset - copy the allowed counter, optionally resetting it. * @reset: non-zero resets the counter after copying it. * Returns the copied counter value. */ unsigned long getAllowedReset(int reset) { return policy_counter_total_reset(POLICY_COUNTER_ALLOWED, reset); } /* * getDeniedReset - copy the denied counter, optionally resetting it. * @reset: non-zero resets the counter after copying it. * Returns the copied counter value. */ unsigned long getDeniedReset(int reset) { return policy_counter_total_reset(POLICY_COUNTER_DENIED, reset); } /* * decision_metrics_add_context - add one worker's decision counters. * @ctx: worker context being sampled. * @data: struct decision_metrics_snapshot aggregate. * Returns nothing. */ static void decision_metrics_add_context(struct decision_context *ctx, void *data) { struct decision_metrics_snapshot *snapshot = data; decision_metrics_t *metrics; struct decision_policy_counters *counters; if (ctx == NULL || snapshot == NULL || snapshot->metrics == NULL) return; metrics = snapshot->metrics; counters = &ctx->policy_counters; metrics->allowed_by_rule += policy_counter_snapshot(&counters->allowed_by_rule, snapshot->reset); metrics->allowed_by_fallthrough += policy_counter_snapshot(&counters->allowed_by_fallthrough, snapshot->reset); metrics->fallthrough_open += policy_counter_snapshot(&counters->fallthrough_open, snapshot->reset); metrics->fallthrough_execute += policy_counter_snapshot(&counters->fallthrough_execute, snapshot->reset); metrics->fallthrough_trusted += policy_counter_snapshot(&counters->fallthrough_trusted, snapshot->reset); metrics->fallthrough_untrusted += policy_counter_snapshot(&counters->fallthrough_untrusted, snapshot->reset); metrics->fallthrough_trust_unknown += policy_counter_snapshot(&counters->fallthrough_trust_unknown, snapshot->reset); metrics->fallthrough_executable += policy_counter_snapshot(&counters->fallthrough_executable, snapshot->reset); metrics->fallthrough_programmatic += policy_counter_snapshot(&counters->fallthrough_programmatic, snapshot->reset); metrics->fallthrough_sharedlib += policy_counter_snapshot(&counters->fallthrough_sharedlib, snapshot->reset); metrics->fallthrough_unknown_ftype += policy_counter_snapshot(&counters->fallthrough_unknown_ftype, snapshot->reset); metrics->fallthrough_other_ftype += policy_counter_snapshot(&counters->fallthrough_other_ftype, snapshot->reset); } /* * getDecisionMetrics - copy policy decision counters for reporting * @metrics: destination metrics snapshot. * Returns nothing. */ void getDecisionMetrics(decision_metrics_t *metrics) { getDecisionMetricsReset(metrics, 0); } /* * getDecisionMetricsReset - copy policy decision counters for reporting. * @metrics: destination metrics snapshot. * @reset: non-zero resets interval counters after copying them. * * Ruleset generation identifies the active policy and is never reset. * Returns nothing. */ void getDecisionMetricsReset(decision_metrics_t *metrics, int reset) { struct decision_metrics_snapshot snapshot = { .metrics = metrics, .reset = reset, }; if (!metrics) return; memset(metrics, 0, sizeof(*metrics)); decision_context_for_each(decision_metrics_add_context, &snapshot); metrics->ruleset_generation = atomic_load_explicit(&ruleset_generation, memory_order_relaxed); metrics->ruleset_effective_since = (time_t)atomic_load_explicit(&ruleset_effective_since, memory_order_relaxed); } fapolicyd-2.0/src/library/policy-metrics.h000066400000000000000000000021701524071644000206770ustar00rootroot00000000000000/* * policy-metrics.h - internal policy decision metrics * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef POLICY_METRICS_HEADER #define POLICY_METRICS_HEADER #include "policy.h" /* * policy_metrics_record_ruleset_update - publish active policy generation. * @generation: policy snapshot generation just published. * @effective_since: time the policy snapshot became active. * Returns nothing. */ void policy_metrics_record_ruleset_update(unsigned int generation, time_t effective_since); /* * policy_metrics_record_decision - count a policy decision. * @decision: decision returned by policy evaluation. * @e: event used for allow detail bucketing, or NULL when unavailable. * @source: whether the allow came from a rule or default fallthrough. * Returns nothing. */ void policy_metrics_record_decision(decision_t decision, event_t *e, decision_source_t source); #endif fapolicyd-2.0/src/library/policy.c000066400000000000000000001353361524071644000172410ustar00rootroot00000000000000/* * policy.c - functions that encapsulate the notion of a policy * Copyright (c) 2016-26 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * Radovan Sroka */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include "database.h" #include "decision-config.h" #include "decision-context.h" #include "decision-timing.h" #include "escape.h" #include "failure-action.h" #include "file.h" #include "policy-metrics.h" #include "rules.h" #include "policy.h" #include "nv.h" #include "message.h" #include "gcc-attributes.h" #include "string-util.h" #include "paths.h" #include "conf.h" #include "process.h" #define MAX_SYSLOG_FIELDS 21 // Only 20 fields are defined for // decision, permission, obj & subj #define NGID_LIMIT 32 // Limit buffer size allocated for // subject to not waste memory #define WB_SIZE 512 #define VALIDATION_SYSLOG_FORMAT "rule,dec,perm,auid,pid,exe,:,path,ftype" /* * policy_snapshot - coherent policy generation used for decisions * * The rule list owns parsed rule nodes and attribute sets. The same snapshot * also owns syslog fields, proc-status masks, the rule count, and the hashed * rule-file identity so all parser side effects publish as one unit. */ struct policy_snapshot { atomic_uint refs; llist rules; nvlist_t fields[MAX_SYSLOG_FIELDS]; unsigned int num_fields; unsigned int rules_proc_status_mask; unsigned int syslog_proc_status_mask; unsigned int rule_count; unsigned int generation; time_t effective_since; char *rule_file_identity; }; struct policy_log_record { const struct policy_snapshot *policy; unsigned int rule_num; decision_t results; char message[WB_SIZE]; int priority; bool enabled; }; /* * active_policy - currently published policy generation * * policy_snapshot_lock serializes active pointer replacement with readers * taking a reference. Evaluation uses a pinned snapshot reference so reload can * publish a new policy without waiting for old decisions to finish. */ static _Atomic(struct policy_snapshot *) active_policy; static pthread_mutex_t policy_snapshot_lock = PTHREAD_MUTEX_INITIALIZER; static atomic_uint next_policy_generation; static __thread struct policy_snapshot *pinned_policy_snapshot; static __thread int policy_snapshot_is_pinned; extern atomic_bool stop; atomic_bool reload_rules = false; static const nv_t table[] = { { NO_OPINION, "no-opinion" }, { ALLOW, "allow" }, { DENY, "deny" }, #ifdef USE_AUDIT { ALLOW_AUDIT, "allow_audit" }, { DENY_AUDIT, "deny_audit" }, #endif { ALLOW_SYSLOG, "allow_syslog" }, { DENY_SYSLOG, "deny_syslog" }, { ALLOW_LOG, "allow_log" }, { DENY_LOG, "deny_log" } }; extern unsigned int debug_mode; #define MAX_DECISIONS (sizeof(table)/sizeof(table[0])) // These are the constants for things not subj or obj #define F_RULE 30 #define F_DECISION 31 #define F_PERM 32 #define F_COLON 33 #ifdef FAN_AUDIT_RULE_NUM struct fan_audit_response { struct fanotify_response r; struct fanotify_response_info_audit_rule a; }; #endif // This function returns 1 on success and 0 on failure static int parsing_obj; static void *fmemccpy(void* restrict dst, const void* restrict src, size_t n) __attr_access((__write_only__, 1, 3)) __attr_access((__read_only__, 2, 3)); /* * free_syslog_fields - release syslog format fields in a policy snapshot * @policy: snapshot whose syslog field array should be reset. * Returns nothing. */ static void free_syslog_fields(struct policy_snapshot *policy) { unsigned int i = 0; while (i < policy->num_fields) { free((void *)policy->fields[i].name); policy->fields[i].name = NULL; i++; } policy->num_fields = 0; policy->syslog_proc_status_mask = 0; } /* * policy_snapshot_free - release one unreferenced policy snapshot * @policy: snapshot to free, or NULL. * Returns nothing. */ static void policy_snapshot_free(struct policy_snapshot *policy) { if (!policy) return; rules_clear(&policy->rules); free_syslog_fields(policy); free(policy->rule_file_identity); free(policy); } /* * policy_snapshot_get - take a read reference on a policy snapshot * @policy: snapshot to reference, or NULL. * Returns nothing. */ static void policy_snapshot_get(struct policy_snapshot *policy) { if (policy) atomic_fetch_add_explicit(&policy->refs, 1, memory_order_relaxed); } /* * policy_snapshot_put - release a policy snapshot read reference * @policy: snapshot reference to release, or NULL. * Returns nothing. */ static void policy_snapshot_put(struct policy_snapshot *policy) { if (!policy) return; if (atomic_fetch_sub_explicit(&policy->refs, 1, memory_order_acq_rel) == 1) policy_snapshot_free(policy); } /* * policy_snapshot_pin_active - pin the current active policy * @void: no arguments are required. * * The lock keeps a publisher from dropping the active reference between the * pointer load and the reader's reference increment. * * Returns the pinned policy snapshot, or NULL if no policy is active. */ static struct policy_snapshot *policy_snapshot_pin_active(void) { struct policy_snapshot *policy; pthread_mutex_lock(&policy_snapshot_lock); /* * The active pointer owns one reference. Readers must increment that * count while publication is blocked, otherwise a reload could exchange * the pointer and drop the last reference before this reader is pinned. */ policy = atomic_load_explicit(&active_policy, memory_order_acquire); policy_snapshot_get(policy); pthread_mutex_unlock(&policy_snapshot_lock); return policy; } /* * policy_snapshot_begin_read - get the policy for this read path * @release: set to nonzero when the caller must release @policy. * * Permission-event threads pin one policy through event construction and rule * evaluation. Other callers take a short-lived reference here. * * Returns the snapshot to use, or NULL if no policy is active. */ static struct policy_snapshot *policy_snapshot_begin_read(int *release) { if (release) *release = 0; /* * A permission event pins one policy at make_policy_decision() entry. * Reuse it so /proc status collection, rule evaluation, and log * formatting cannot accidentally observe different reload generations. */ if (policy_snapshot_is_pinned) return pinned_policy_snapshot; if (release) *release = 1; return policy_snapshot_pin_active(); } /* * policy_snapshot_end_read - release a read snapshot when needed * @policy: snapshot returned by policy_snapshot_begin_read(). * @release: nonzero when @policy has a private read reference. * Returns nothing. */ static void policy_snapshot_end_read(struct policy_snapshot *policy, int release) { if (release) policy_snapshot_put(policy); } /* * policy_snapshot_pin_current - pin active policy for this thread * @void: no arguments are required. * * Event construction and rule evaluation must use one generation so proc-status * collection, rules, attr sets, and syslog fields match. * * Returns the pinned policy snapshot, or NULL if no policy is active. */ static struct policy_snapshot *policy_snapshot_pin_current(void) { struct policy_snapshot *policy = policy_snapshot_pin_active(); pinned_policy_snapshot = policy; policy_snapshot_is_pinned = 1; return policy; } /* * policy_snapshot_unpin_current - unpin a thread policy generation * @policy: snapshot returned by policy_snapshot_pin_current(). * Returns nothing. */ static void policy_snapshot_unpin_current(struct policy_snapshot *policy) { if (policy_snapshot_is_pinned && pinned_policy_snapshot == policy) { pinned_policy_snapshot = NULL; policy_snapshot_is_pinned = 0; } policy_snapshot_put(policy); } /* * policy_snapshot_create - allocate an unpublished policy snapshot * @identity: optional rule file identity string, transferred to the snapshot * * The caller builds rules and syslog fields in this private object. It is * only installed as the active policy after every parser stage succeeds. * * Returns: snapshot pointer on success, NULL on allocation failure. */ static struct policy_snapshot *policy_snapshot_create(char *identity) { struct policy_snapshot *policy = calloc(1, sizeof(*policy)); if (!policy) { free(identity); return NULL; } atomic_init(&policy->refs, 1); if (rules_create(&policy->rules)) { free(identity); free(policy); return NULL; } policy->rule_file_identity = identity; return policy; } /* * add_syslog_field - append one parsed syslog format field * @policy: candidate policy snapshot receiving the field. * @name: field name to copy into the snapshot. * @item: field identifier used when formatting policy logs. * Returns 1 on success, 0 on allocation or capacity failure. */ static int add_syslog_field(struct policy_snapshot *policy, const char *name, int item) { if (policy->num_fields >= MAX_SYSLOG_FIELDS) return 0; policy->fields[policy->num_fields].name = strdup(name); if (!policy->fields[policy->num_fields].name) { msg(LOG_ERR, "No memory for syslog_format field %s", name); return 0; } policy->fields[policy->num_fields].item = item; policy->num_fields++; return 1; } static int lookup_field(struct policy_snapshot *policy, const char *ptr) { if (strcmp("rule", ptr) == 0) { return add_syslog_field(policy, ptr, F_RULE); } else if (strcmp("dec", ptr) == 0) { return add_syslog_field(policy, ptr, F_DECISION); } else if (strcmp("perm", ptr) == 0) { return add_syslog_field(policy, ptr, F_PERM); } else if (strcmp(":", ptr) == 0) { parsing_obj = 1; return add_syslog_field(policy, ptr, F_COLON); } if (parsing_obj == 0) { int ret_val = subj_name_to_val(ptr, RULE_FMT_COLON); if (ret_val >= 0) { if (ret_val == ALL_SUBJ || ret_val == PATTERN || ret_val > EXE) { msg(LOG_ERR, "%s cannot be used in syslog_format", ptr); } else { // Opportunistically mark the fields that might // be needed for logging so that we gather // them all at once later. switch (ret_val) { case UID: policy->syslog_proc_status_mask |= PROC_STAT_UID; break; case PPID: policy->syslog_proc_status_mask |= PROC_STAT_PPID; break; case GID: policy->syslog_proc_status_mask |= PROC_STAT_GID; break; case COMM: policy->syslog_proc_status_mask |= PROC_STAT_COMM; break; default: break; } return add_syslog_field(policy, ptr, ret_val); } } } else { int ret_val = obj_name_to_val(ptr); if (ret_val >= 0) { if (ret_val == ALL_OBJ) { msg(LOG_ERR, "%s cannot be used in syslog_format", ptr); } else { return add_syslog_field(policy, ptr, ret_val); } } } return 0; } // This function returns 1 on success, 0 on failure static int parse_syslog_format(struct policy_snapshot *policy, const char *syslog_format) { char *ptr, *saved, *tformat; int rc = 1; if (!syslog_format) { msg(LOG_ERR, "syslog_format is not configured"); return 0; } if (strchr(syslog_format, ':') == NULL) { msg(LOG_ERR, "syslog_format does not have a ':'"); return 0; } free_syslog_fields(policy); parsing_obj = 0; tformat = strdup(syslog_format); if (!tformat) { msg(LOG_ERR, "No memory for syslog_format"); return 0; } // Must be delimited by comma ptr = strtok_r(tformat, ",", &saved); while (ptr && rc && policy->num_fields < MAX_SYSLOG_FIELDS) { rc = lookup_field(policy, ptr); if (rc == 0) msg(LOG_ERR, "Field %s invalid for syslog_format", ptr); ptr = strtok_r(NULL, ",", &saved); } free(tformat); return rc; } int dec_name_to_val(const char *name) { unsigned int i = 0; while (i < MAX_DECISIONS) { if (strcmp(name, table[i].name) == 0) return table[i].value; i++; } return -1; } static const char *dec_val_to_name(unsigned int v) { unsigned int i = 0; while (i < MAX_DECISIONS) { if (v == table[i].value) return table[i].name; i++; } return NULL; } static FILE *open_file(char **identity) { int fd; FILE *f; if (identity) *identity = NULL; // Now open the file and load them one by one. We default to // opening the old file first in case there are both fd = open(OLD_RULES_FILE, O_NOFOLLOW|O_RDONLY); if (fd < 0) { // See if the new rules exist fd = open(RULES_FILE, O_NOFOLLOW|O_RDONLY); if (fd < 0) { msg(LOG_ERR, "Error opening rules file (%s)", strerror(errno)); return NULL; } } struct stat sb; if (fstat(fd, &sb)) { msg(LOG_ERR, "Failed to stat rule file %s", strerror(errno)); close(fd); return NULL; } char *sha_buf = get_hash_from_fd2(fd, sb.st_size, FILE_HASH_ALG_SHA256); if (sha_buf) { if (identity) *identity = sha_buf; else free(sha_buf); } else { msg(LOG_WARNING, "Failed to hash rule identity %s", strerror(errno)); } f = fdopen(fd, "r"); if (f == NULL) { msg(LOG_ERR, "Error - fdopen failed (%s)", strerror(errno)); free(identity ? *identity : NULL); if (identity) *identity = NULL; close(fd); } return f; } /* * log_policy_update_failure - report an unsuccessful policy update * @void: no arguments are required. * Returns nothing. */ static void log_policy_update_failure(void) { struct policy_snapshot *policy; policy = atomic_load_explicit(&active_policy, memory_order_acquire); if (policy) msg(LOG_ERR, "Daemon configuration update failed; " "previous policy preserved"); else msg(LOG_ERR, "Daemon configuration update failed; " "no policy installed"); } /* * copy_syslog_format - copy syslog format for one policy build * @_config: daemon configuration containing syslog_format. * @out: receives a copied syslog_format string, or NULL when absent. * * The daemon updates config.syslog_format under the legacy rule mutex during * SIGHUP reconfiguration. Snapshot builds only need the mutex long enough to * copy the string; holding it while parsing policy would block decisions. * * Returns 0 on success or 1 on allocation failure. */ static int copy_syslog_format(const conf_t *_config, char **out) { int had_format = 0; *out = NULL; lock_rule(); if (_config && _config->syslog_format) { had_format = 1; *out = strdup(_config->syslog_format); } unlock_rule(); if (had_format && *out == NULL) { msg(LOG_ERR, "No memory for syslog_format"); return 1; } return 0; } /* * publish_policy_snapshot - install a fully validated policy snapshot * @policy: candidate snapshot built by build_policy_snapshot(). * Returns nothing. */ static void publish_policy_snapshot(struct policy_snapshot *policy) { struct policy_snapshot *old; time_t now; policy->rule_count = policy->rules.cnt; policy->rules_proc_status_mask = rules_get_proc_status_mask(&policy->rules); /* * Transaction point: after this exchange, new decisions use the * candidate policy. Everything before this must be able to fail while * leaving the old active_policy untouched. * * The candidate arrives with one reference, which becomes the active * pointer's reference. Readers that already pinned the old generation * continue using it until they drop their references below. */ pthread_mutex_lock(&policy_snapshot_lock); now = time(NULL); policy->effective_since = now == (time_t)-1 ? 0 : now; policy->generation = atomic_fetch_add_explicit( &next_policy_generation, 1, memory_order_relaxed) + 1; old = atomic_exchange_explicit(&active_policy, policy, memory_order_acq_rel); policy_metrics_record_ruleset_update(policy->generation, policy->effective_since); pthread_mutex_unlock(&policy_snapshot_lock); if (policy->rule_file_identity) msg(LOG_INFO, "Ruleset identity: %s", policy->rule_file_identity); msg(LOG_INFO, "Daemon rules updated"); /* * Drop the old active reference after publishing. If a reader still has * the old snapshot pinned, this only decrements the count; final * reclamation happens when the last reader calls policy_snapshot_put(). */ policy_snapshot_put(old); } /* * build_policy_snapshot - parse rules and syslog fields into a candidate * @_config: daemon configuration containing the syslog format. * @f: already opened rule file stream. * @identity: optional rule-file identity string consumed by the candidate. * @out: receives the validated snapshot on success. * * Returns 0 on success, 1 on parser, read, or allocation failure. On failure, * the active policy is not changed and @identity has been consumed. */ static int build_policy_snapshot(const conf_t *_config, FILE *f, char *identity, struct policy_snapshot **out) { int rc, lineno = 1; char *line = NULL; char *syslog_format = NULL; size_t len = 0; struct policy_snapshot *policy = policy_snapshot_create(identity); *out = NULL; if (!policy) return 1; if (copy_syslog_format(_config, &syslog_format)) { policy_snapshot_put(policy); return 1; } msg(LOG_DEBUG, "Loading rule file:"); while (getline(&line, &len, f) != -1) { char *ptr = strchr(line, 0x0a); if (ptr) *ptr = 0; msg(LOG_DEBUG, "%s", line); rc = rules_append(&policy->rules, line, lineno); if (rc) { free(line); free(syslog_format); policy_snapshot_put(policy); return 1; } lineno++; } free(line); if (ferror(f)) { msg(LOG_ERR, "Error reading rules file (%s)", strerror(errno)); free(syslog_format); policy_snapshot_put(policy); return 1; } if (policy->rules.cnt == 0) { msg(LOG_INFO, "No rules in file - exiting"); free(syslog_format); policy_snapshot_put(policy); return 1; } else { msg(LOG_DEBUG, "Loaded %u rules", policy->rules.cnt); } rc = parse_syslog_format(policy, syslog_format); free(syslog_format); if (!rc || policy->num_fields == 0) { policy_snapshot_put(policy); return 1; } *out = policy; return 0; } int load_rules(const conf_t *_config) { char *identity = NULL; struct policy_snapshot *policy = NULL; FILE * f = open_file(&identity); if (f == NULL) { log_policy_update_failure(); return 1; } int res = build_policy_snapshot(_config, f, identity, &policy); fclose(f); if (res) { log_policy_update_failure(); return 1; } publish_policy_snapshot(policy); return 0; } /* * load_rules_from_stream - load policy from a caller-owned stream * @_config: daemon configuration containing the syslog format. * @f: rule stream positioned at the beginning. * * Returns 0 on success, 1 on failure. This helper exists so tests can exercise * the same transactional publish path without depending on /etc paths. */ int load_rules_from_stream(const conf_t *_config, FILE *f) { struct policy_snapshot *policy = NULL; if (!f) { log_policy_update_failure(); return 1; } if (build_policy_snapshot(_config, f, NULL, &policy)) { log_policy_update_failure(); return 1; } publish_policy_snapshot(policy); return 0; } /* * validate_rules_from_stream - parse policy without publishing it. * @f: rule stream positioned at the beginning. * * Returns 0 when the daemon parser accepts the candidate policy, 1 otherwise. */ int validate_rules_from_stream(FILE *f) { conf_t validation_config = { .syslog_format = VALIDATION_SYSLOG_FORMAT }; struct policy_snapshot *policy = NULL; if (!f) return 1; /* * Rule validation only needs a valid syslog_format so the daemon policy * snapshot parser can run without depending on /etc/fapolicyd.conf. */ if (build_policy_snapshot(&validation_config, f, NULL, &policy)) return 1; policy_snapshot_put(policy); return 0; } void destroy_rules(void) { struct decision_context *ctx = decision_context_current(); struct policy_snapshot *policy, *pinned; pthread_mutex_lock(&policy_snapshot_lock); policy = atomic_exchange_explicit(&active_policy, NULL, memory_order_acq_rel); atomic_store_explicit(&next_policy_generation, 0, memory_order_relaxed); pthread_mutex_unlock(&policy_snapshot_lock); pinned = pinned_policy_snapshot; pinned_policy_snapshot = NULL; policy_snapshot_is_pinned = 0; policy_snapshot_put(pinned); policy_snapshot_put(policy); if (stop) { free(ctx->working_buffer); ctx->working_buffer = NULL; } } unsigned int policy_get_syslog_proc_status_mask(void) { struct policy_snapshot *policy; unsigned int mask = 0; int release; /* * Separate mask getters are kept for existing callers. Code that needs * both rule and syslog masks should use policy_get_proc_status_mask() * so both values definitely come from one pinned generation. */ policy = policy_snapshot_begin_read(&release); if (policy) mask = policy->syslog_proc_status_mask; policy_snapshot_end_read(policy, release); return mask; } /* * policy_get_rules_proc_status_mask - return active rule proc-status mask * @void: no arguments are required. * Returns a bitmap of PROC_STAT_* fields required by the active rules. */ unsigned int policy_get_rules_proc_status_mask(void) { struct policy_snapshot *policy; unsigned int mask = 0; int release; /* * This returns one field from the currently pinned generation when the * caller is inside a decision, otherwise from a short-lived active * snapshot reference. */ policy = policy_snapshot_begin_read(&release); if (policy) mask = policy->rules_proc_status_mask; policy_snapshot_end_read(policy, release); return mask; } /* * policy_get_proc_status_mask - return all active policy proc-status needs * @void: no arguments are required. * Returns the combined rule and syslog proc-status mask for one generation. */ unsigned int policy_get_proc_status_mask(void) { struct policy_snapshot *policy; unsigned int mask = 0; int release; policy = policy_snapshot_begin_read(&release); if (policy) { /* * Read both masks from the same snapshot. Splitting this into * independent globals lets reload mix old rule requirements * with new syslog fields, which is exactly what snapshots avoid. */ mask = policy->rules_proc_status_mask; mask |= policy->syslog_proc_status_mask; } policy_snapshot_end_read(policy, release); return mask; } /* * getReplyErrors - return fanotify response write error count. * Returns the number of fanotify response writes that failed or appeared * incomplete. */ unsigned long getReplyErrors(void) { return failure_action_count(FAILURE_REASON_RESPONSE_WRITE_FAILURE); } void set_reload_rules(void) { reload_rules = true; } /* * ff - pending reload rule file opened before parsing starts. * ff_identity - SHA256 identity for @ff, transferred to the new snapshot. * * load_rule_file() prepares these so do_reload_rules() can spend its time * parsing and publishing rather than opening and hashing policy files. */ static FILE * ff = NULL; static char *ff_identity; int load_rule_file(void) { if (ff) { fclose(ff); ff = NULL; } free(ff_identity); ff_identity = NULL; ff = open_file(&ff_identity); if (ff == NULL) return 1; return 0; } int do_reload_rules(const conf_t *_config) { struct policy_snapshot *policy = NULL; char *identity = ff_identity; ff_identity = NULL; if (!ff) { free(identity); msg(LOG_ERR, "Rule reload failed: no rule file is open"); failure_action_record(FAILURE_REASON_RULE_RELOAD_FAILURE); log_policy_update_failure(); return 1; } int rc = build_policy_snapshot(_config, ff, identity, &policy); fclose(ff); ff = NULL; if (rc) { failure_action_record(FAILURE_REASON_RULE_RELOAD_FAILURE); log_policy_update_failure(); return 1; } publish_policy_snapshot(policy); return 0; } static char *format_value(int item, unsigned int num, decision_t results, event_t *e) __attr_dealloc_free; static char *format_value(int item, unsigned int num, decision_t results, event_t *e) { char *out = NULL; if (item >= F_RULE) { switch (item) { case F_RULE: if (asprintf(&out, "%u", num+1) < 0) out = NULL; break; case F_DECISION: if (asprintf(&out, "%s", dec_val_to_name(results)) < 0) out = NULL; break; case F_PERM: if (asprintf(&out, "%s", e->type & FAN_OPEN_EXEC_PERM ? "execute" : "open") < 0) out = NULL; break; case F_COLON: if (asprintf(&out, ":") < 0) out = NULL; break; } } else if (item >= OBJ_START) { object_attr_t *obj = get_obj_attr(e, item); if (item != OBJ_TRUST) { char * str = obj ? obj->o : "?"; char *tmp = NULL; size_t need_escape = check_escape_shell(str); if (need_escape) { // need_escape contains potential size of escaped string tmp = escape_shell(str, need_escape); str = tmp; } if (asprintf(&out, "%s", str ? str : "??") < 0) out = NULL; free(tmp); } else if (asprintf(&out, "%d", obj ? (obj->val ? 1 : 0) : 9) < 0) out = NULL; } else { subject_attr_t *subj = get_subj_attr(e, item); if (item == PID || item == PPID) { if (asprintf(&out, "%d", subj ? subj->pid : 0) < 0) out = NULL; } else if (item < GID && item != UID) { if (asprintf(&out, "%u", subj ? subj->uval : 0) < 0) out = NULL; } else if (item >= COMM) { char * str = subj ? subj->str : "?"; char *tmp = NULL; size_t need_escape = check_escape_shell(str); if (need_escape) { // need_escape contains potential size of escaped string tmp = escape_shell(str, need_escape); str = tmp; } if (asprintf(&out, "%s", str ? str : "??") < 0) out = NULL; free(tmp); } else { // UID/GID only log first 32 out = malloc(NGID_LIMIT*12); if (out == NULL) return NULL; /* * A process can exit while a non-permission event is being * reported. Failed procfs lookups and incomplete status data * must not turn logging into a NULL dereference or an * uninitialized string read. */ if (subj == NULL || subj->set == NULL || attr_set_empty(subj->set)) { strcpy(out, "?"); } else { char buf[12]; char *ptr = out; int cnt = 0; avl_iterator i; avl_int_data_t *grp; for (grp = (avl_int_data_t *) avl_first(&i, &(subj->set->tree)); grp && cnt < NGID_LIMIT; grp=(avl_int_data_t *)avl_next(&i)) { if (ptr == out) { snprintf(buf, sizeof(buf), "%llu", (unsigned long long)grp->num); } else { snprintf(buf, sizeof(buf), ",%llu", (unsigned long long)grp->num); } ptr = stpcpy(ptr, buf); cnt++; } if (ptr == out) strcpy(out, "?"); } } } return out; } // This is like memccpy except it returns the pointer to the NIL byte so // that we are positioned for the next concatenation. Also, since we know // we are always looking for NIL, just hard code it. static void *fmemccpy(void* restrict dst, const void* restrict src, size_t n) { if (n == 0) return dst; const char *s = src; char *ret = dst; for ( ; n; ++ret, ++s, --n) { *ret = *s; if ((unsigned char)*ret == (unsigned char)'\0') return ret; } return ret; } /* * policy_log_record_format - fully render one decision log record. * @e: event whose attributes must be materialized before a permission reply. * @record: owned destination retained until post-reply emission. * * Returns nothing. The formatted text is copied out of the worker buffer so * the post-reply emitter cannot make lazy process or file lookups. */ static void policy_log_record_format(struct policy_log_record *record, event_t *e) { struct decision_context *ctx = decision_context_current(); struct decision_timing_span timing; int mode; unsigned int i; size_t dsize; ptrdiff_t written; char *p1, *p2, *val; if (record == NULL) return; record->enabled = false; if (record->policy == NULL) return; mode = record->results & SYSLOG ? LOG_INFO : LOG_DEBUG; decision_timing_stage_begin( DECISION_TIMING_STAGE_SYSLOG_DEBUG_FORMAT, &timing); if (ctx->working_buffer == NULL) { ctx->working_buffer = malloc(WB_SIZE); if (ctx->working_buffer == NULL) { strcpy(record->message, "No working buffer for logging"); record->policy = NULL; record->priority = LOG_ERR; record->enabled = true; decision_timing_stage_end(&timing); return; } } ctx->working_buffer[0] = '\0'; dsize = WB_SIZE; p1 = p2 = ctx->working_buffer; // Dummy assignment for p1 to quiet warnings for (i = 0; i < record->policy->num_fields && dsize; i++) { if (dsize < WB_SIZE) { // This is skipped first pass, p1 is initialized below p2 = fmemccpy(p1, " ", dsize); written = p2 - p1; if ((size_t)written > dsize) break; dsize -= (size_t)written; } p1 = fmemccpy(p2, record->policy->fields[i].name, dsize); written = p1 - p2; if ((size_t)written > dsize) break; dsize -= (size_t)written; if (record->policy->fields[i].item != F_COLON) { p2 = fmemccpy(p1, "=", dsize); written = p2 - p1; if ((size_t)written > dsize) break; dsize -= (size_t)written; val = format_value(record->policy->fields[i].item, record->rule_num, record->results, e); p1 = fmemccpy(p2, val ? val : "?", dsize); written = p1 - p2; if ((size_t)written > dsize) { free(val); break; } dsize -= (size_t)written; free(val); } } ctx->working_buffer[WB_SIZE-1] = 0; // Just in case strcpy(record->message, ctx->working_buffer); record->policy = NULL; record->priority = mode; record->enabled = true; decision_timing_stage_end(&timing); } /* * policy_log_record_emit - emit a pre-rendered decision log record. * @record: fully formatted log text captured during policy evaluation. * * Returns nothing. This only queues or writes the finished text; it never * consults the event after the fanotify response is sent. */ static void policy_log_record_emit(const struct policy_log_record *record) { decision_timing_driver_t previous_driver; if (record == NULL || !record->enabled) return; previous_driver = decision_timing_driver_push( DECISION_TIMING_DRIVER_RESPONSE); msg(record->priority, "%s", record->message); decision_timing_driver_pop(previous_driver); } /* * process_event_evaluate - evaluate policy and optionally delay logging * @e: event to evaluate. * @source: optional output receiving rule or fallthrough source. * @response_timing: optional response timing span started after evaluation. * @log_record: optional destination for a delayed log record. * * Returns the access decision. When @log_record is supplied and the caller is * using a pinned policy snapshot, decision logging is captured for later * emission. Otherwise logging happens before a temporary policy reference is * released. */ static decision_t process_event_evaluate(event_t *e, decision_source_t *source, struct decision_timing_span *response_timing, struct policy_log_record *log_record) { decision_t results = NO_OPINION; decision_t decision; struct policy_snapshot *policy; decision_timing_driver_t previous_driver; struct decision_timing_span eval_timing; struct policy_log_record immediate_log; struct policy_log_record *record; lnode *r; int release_policy; if (log_record) memset(log_record, 0, sizeof(*log_record)); if (source) *source = DECISION_SOURCE_FALLTHROUGH; policy = policy_snapshot_begin_read(&release_policy); if (!policy) { if (response_timing) decision_timing_stage_begin( DECISION_TIMING_STAGE_RESPONSE_TOTAL, response_timing); return ALLOW; } /* * The snapshot reference keeps rules, attr sets, and syslog fields * alive for this whole evaluation. The list cursor is local so * concurrent readers can walk the same immutable rule list safely. */ //int cnt = 0; previous_driver = decision_timing_driver_push( DECISION_TIMING_DRIVER_EVALUATION); decision_timing_stage_begin(DECISION_TIMING_STAGE_RULE_EVALUATION, &eval_timing); for (r = rules_first_node(&policy->rules); r; r = rules_next_node(r)) { //msg(LOG_INFO, "process_event: rule %d", cnt); results = rule_evaluate(r, e); // If a rule has an opinion, stop and use it if (results != NO_OPINION) break; //cnt++; } if (r) rules_record_hit(r); decision_timing_stage_end(&eval_timing); decision_timing_driver_pop(previous_driver); if (response_timing) decision_timing_stage_begin(DECISION_TIMING_STAGE_RESPONSE_TOTAL, response_timing); // Output some information if debugging on or syslogging requested if ( (results & SYSLOG) || (debug_mode == 1) || (debug_mode > 1 && (results & DENY)) ) { /* * FIXME: Pre-reply log formatting, and object trust added to * FAN_AUDIT in reply_event_write(), can lazily hash files or * invoke libmagic. Investigate cached audit values and * splitting live subject capture from post-reply object work. */ if (log_record) { /* * Permission events keep the process pinned until the caller * writes a verdict. Resolve every syslog field now, then let * the caller defer only the potentially blocking log delivery. */ record = log_record; } else { memset(&immediate_log, 0, sizeof(immediate_log)); record = &immediate_log; } previous_driver = decision_timing_driver_push( DECISION_TIMING_DRIVER_RESPONSE); record->policy = policy; record->rule_num = r ? r->num : 0xFFFFFFFF; record->results = results; policy_log_record_format(record, e); decision_timing_driver_pop(previous_driver); if (!log_record) policy_log_record_emit(record); } // Record which rule (rules are 1 based when listed by the cli tool) if (r) { e->num = r->num + 1; if (source) *source = DECISION_SOURCE_RULE; } // If we are not in permissive mode, return any decision if (results != NO_OPINION) decision = results; else decision = ALLOW; /* * All callers format logs before releasing the policy snapshot because the * formatter reads its syslog field array. Permission callers retain only * the owned text for emission after the fanotify response is written. */ policy_snapshot_end_read(policy, release_policy); return decision; } /* * process_event_with_source - evaluate policy and report decision source * @e: event to evaluate. * @source: optional output receiving rule or fallthrough source. * @response_timing: optional response timing span started after evaluation. * * Returns the access decision. A no-opinion policy result remains compatible * with historical behavior by returning ALLOW and reporting fallthrough. */ decision_t process_event_with_source(event_t *e, decision_source_t *source, struct decision_timing_span *response_timing) { return process_event_evaluate(e, source, response_timing, NULL); } /* * process_event - evaluate policy using the compatibility decision API * @e: event to evaluate. * Returns the access decision without exposing source metadata. */ decision_t process_event(event_t *e) { return process_event_with_source(e, NULL, NULL); } #ifdef FAN_AUDIT_RULE_NUM static int response_info_supported = 0; static int test_info_api(int fd) { int rc; struct fan_audit_response f; f.r.fd = FAN_NOFD; f.r.response = FAN_DENY | FAN_INFO; f.a.hdr.type = FAN_RESPONSE_INFO_AUDIT_RULE; f.a.hdr.pad = 0; f.a.hdr.len = sizeof(struct fanotify_response_info_audit_rule); f.a.rule_number = 0; f.a.subj_trust = 2; f.a.obj_trust = 2; rc = write(fd, &f, sizeof(struct fan_audit_response)); msg(LOG_DEBUG, "Rule number API supported %s", rc < 0 ? "no" : "yes"); if (rc < 0) return 0; else return 1; } #endif /* * The daemon has one fanotify permission group. Keep its original descriptor * number after shutdown so workers holding that number can recognize a retired * group instead of writing to an unrelated file that reused the descriptor. */ static pthread_rwlock_t response_fd_lock = PTHREAD_RWLOCK_INITIALIZER; static int managed_response_fd = -1; static bool managed_response_closed = true; /* * reply_event_init - pre-probe extended fanotify response support. * @fd: fanotify listener fd used for permission responses. * Returns 0 on success or 1 when initialization cannot be attempted. */ int reply_event_init(int fd) { #ifdef FAN_AUDIT_RULE_NUM if (fd < 0) { errno = EBADF; msg(LOG_ERR, "Cannot initialize fanotify response info support: " "bad fd (%s)", strerror(errno)); response_info_supported = 0; return 1; } response_info_supported = test_info_api(fd); #endif pthread_rwlock_wrlock(&response_fd_lock); managed_response_fd = fd; managed_response_closed = false; pthread_rwlock_unlock(&response_fd_lock); return 0; } /* * reply_event_close_group - retire and close the permission fanotify group. * @group_fd: atomic descriptor owned by notify.c. * * Normal shutdown closes the group before joining threads because a worker * can be blocked in a watched open that only group closure can release after * the main reader exits. Serialize that close with response writes and retain * the original descriptor number so late worker cleanup cannot write to a * reused descriptor. This function is not async-signal-safe. * * Returns nothing. */ void reply_event_close_group(atomic_int *group_fd) { int fd; if (group_fd == NULL) return; pthread_rwlock_wrlock(&response_fd_lock); managed_response_closed = true; fd = atomic_exchange_explicit(group_fd, -1, memory_order_relaxed); if (fd >= 0) (void)close(fd); pthread_rwlock_unlock(&response_fd_lock); } /* * reply_event_write - write a fanotify response without closing event fd. * @fd: fanotify listener fd used for permission responses. * @metadata: permission event metadata to answer. * @reply: FAN_ALLOW/FAN_DENY response bits. * @e: optional event used for audit response details. * * Returns nothing. The caller keeps ownership of metadata->fd and closes it * after writing the response; permission-event log fields are pre-rendered. */ static void reply_event_write(int fd, const struct fanotify_event_metadata *metadata, unsigned reply, event_t *e) { struct decision_timing_span prep_timing; struct decision_timing_span write_timing; #ifdef FAN_AUDIT_RULE_NUM /* * FIXME: Persist FAN_ENABLE_AUDIT support across policy reloads, mask * FAN_AUDIT when unavailable, and investigate safely retrying a failed * FAN_INFO response as a plain response so permission events are * answered. */ if (reply & FAN_AUDIT && response_info_supported) { struct fan_audit_response f; subject_attr_t *sn; object_attr_t *obj; decision_timing_stage_begin( DECISION_TIMING_STAGE_AUDIT_RESPONSE_PREP, &prep_timing); f.r.fd = metadata->fd; f.r.response = reply | FAN_INFO; f.a.hdr.type = FAN_RESPONSE_INFO_AUDIT_RULE; f.a.hdr.pad = 0; f.a.hdr.len = sizeof(struct fanotify_response_info_audit_rule); if (e) f.a.rule_number = e->num; else f.a.rule_number = 0; // Subj trust is rare. See if we have it. if (e && (sn = subject_access(e->s, SUBJ_TRUST))) f.a.subj_trust = sn->uval; else f.a.subj_trust = 2; // All objects have a trust value if (e && (obj = get_obj_attr(e, OBJ_TRUST))) { f.a.obj_trust = obj->val; } else f.a.obj_trust = 2; decision_timing_stage_end(&prep_timing); errno = 0; decision_timing_stage_begin( DECISION_TIMING_STAGE_FANOTIFY_RESPONSE_WRITE, &write_timing); // FAN_INFO replies include the audit record after the base response. if (write(fd, &f, sizeof(f)) < (ssize_t)sizeof(f) || errno) failure_action_record( FAILURE_REASON_RESPONSE_WRITE_FAILURE); decision_timing_stage_end(&write_timing); goto out; } #endif struct fanotify_response response; decision_timing_stage_begin( DECISION_TIMING_STAGE_AUDIT_RESPONSE_PREP, &prep_timing); response.fd = metadata->fd; response.response = reply; decision_timing_stage_end(&prep_timing); errno = 0; decision_timing_stage_begin( DECISION_TIMING_STAGE_FANOTIFY_RESPONSE_WRITE, &write_timing); if (write(fd, &response, sizeof(struct fanotify_response)) < (ssize_t)sizeof(struct fanotify_response) || errno) failure_action_record( FAILURE_REASON_RESPONSE_WRITE_FAILURE); decision_timing_stage_end(&write_timing); out: return; } void reply_event(int fd, const struct fanotify_event_metadata *metadata, unsigned reply, event_t *e) { int managed = managed_response_fd >= 0 && fd == managed_response_fd; if (managed) pthread_rwlock_rdlock(&response_fd_lock); if (!managed || !managed_response_closed) reply_event_write(fd, metadata, reply, e); if (managed) pthread_rwlock_unlock(&response_fd_lock); if (metadata->fd >= 0) close(metadata->fd); } /* * log_event_build_deny - explain a deny before rule evaluation exists. * @decision_event: event envelope that failed construction. * * The normal debug-deny path logs from process_event_with_source(), but event * construction failures deny before there is an event_t or rule context to * format. Emit a minimal diagnostic so denied counters are visible during * --debug-deny runs. */ static void log_event_build_deny(const decision_event_t *decision_event) { const struct fanotify_event_metadata *metadata; if (debug_mode <= 1 || decision_event == NULL) return; metadata = &decision_event->metadata; msg(LOG_DEBUG, "dec=deny reason=event-build pid=%d fd=%d mask=0x%llx " "subject_slot=%u", metadata->pid, metadata->fd, (unsigned long long)metadata->mask, decision_event->subject_slot); } /* * make_policy_decision - build an event, evaluate policy, and reply. * @decision_event: internal event envelope owning the fanotify metadata fd. * @fd: fanotify listener fd used for permission responses. * @mask: permission-event mask that requires a fanotify reply. * * completed_subject_slot is set when processing leaves the event's subject * slot empty or at STATE_FULL or later, allowing the decision thread to * release deferred events for that slot. */ void make_policy_decision(decision_event_t *decision_event, int fd, uint64_t mask) { const struct decision_config *decision_config = decision_config_pin(); struct policy_snapshot *policy_snapshot; const struct fanotify_event_metadata *metadata = &decision_event->metadata; event_t e = { 0 }; int decision; event_t *metric_event = NULL; decision_source_t source = DECISION_SOURCE_FALLTHROUGH; struct decision_timing_span event_timing; struct decision_timing_span response_timing = { 0 }; decision_timing_driver_t previous_driver; struct policy_log_record log_record = { 0 }; bool log_build_deny = false; bool event_fd_closed = false; /* * Pin before new_event(): building subject attributes may call * policy_get_proc_status_mask(). That mask must describe the same * generation that rule evaluation and syslog formatting will use. */ policy_snapshot = policy_snapshot_pin_current(); decision_timing_stage_begin(DECISION_TIMING_STAGE_EVENT_BUILD, &event_timing); if (decision_event->subject_slot == DECISION_EVENT_NO_SLOT) decision_event->subject_slot = event_subject_slot(metadata->pid); decision_event->completed_subject_slot = DECISION_EVENT_NO_SLOT; if (new_event(metadata, &e)) { decision = FAN_DENY; log_build_deny = true; } else { decision_timing_stage_end(&event_timing); metric_event = &e; /* * No rule mutex is needed here. policy_snapshot_pin_current() * keeps the selected immutable generation alive while reload can * parse and publish a replacement without blocking decisions. */ decision = process_event_evaluate(&e, &source, &response_timing, &log_record); } if (metric_event == NULL) decision_timing_stage_end(&event_timing); previous_driver = decision_timing_driver_push( DECISION_TIMING_DRIVER_RESPONSE); policy_metrics_record_decision(decision, metric_event, source); decision_timing_driver_pop(previous_driver); if (metadata->mask & mask) { previous_driver = decision_timing_driver_push( DECISION_TIMING_DRIVER_RESPONSE); // if in debug mode, do not allow audit events if (debug_mode) decision &= ~AUDIT; // If permissive, always allow and honor the audit bit // if not in debug mode if (decision_config_permissive(decision_config)) reply_event_write(fd, metadata, FAN_ALLOW | (decision & AUDIT), metric_event); else reply_event_write(fd, metadata, decision & FAN_RESPONSE_MASK, metric_event); decision_timing_driver_pop(previous_driver); /* Log fields were materialized before the reply, so release it now. */ if (metadata->fd >= 0) { close(metadata->fd); event_fd_closed = true; } } /* * process_event_evaluate() rendered the complete decision log while this * permission event still pinned the subject. Emit only that owned text * after the verdict so journald, rsyslog, or another logging component * cannot delay the fanotify response. */ if (log_build_deny) log_event_build_deny(decision_event); else policy_log_record_emit(&log_record); if (!event_fd_closed && metadata->fd >= 0) close(metadata->fd); decision_timing_stage_end(&response_timing); if (decision_event->subject_slot != DECISION_EVENT_NO_SLOT && event_subject_slot_is_unblocked(decision_event->subject_slot)) decision_event->completed_subject_slot = decision_event->subject_slot; policy_snapshot_unpin_current(policy_snapshot); decision_config_unpin(decision_config); } void policy_no_audit(void) { struct policy_snapshot *policy; /* * This runs during fanotify initialization, before decision workers are * active. Hold the publication lock anyway so the active pointer cannot * change while the one permitted in-place rule adjustment is made. */ pthread_mutex_lock(&policy_snapshot_lock); policy = atomic_load_explicit(&active_policy, memory_order_acquire); if (policy) rules_unsupport_audit(&policy->rules); pthread_mutex_unlock(&policy_snapshot_lock); } /* * policy_rule_hits_report - write per-rule hit counters for the active policy. * @f: output stream. * * A snapshot reference protects the active rules while the report walks rule * nodes and source text. */ void policy_rule_hits_report(FILE *f) { policy_rule_hits_report_reset(f, 0); } /* * policy_rule_hits_report_reset - write per-rule hit counters. * @f: output stream. * @reset: non-zero resets counters after copying them. * * Rule hit counters naturally start fresh when a new ruleset generation is * published. Manual metric resets also clear them so operators can run a * focused test against the currently loaded rules without reloading policy. */ void policy_rule_hits_report_reset(FILE *f, int reset) { struct policy_snapshot *policy; int release_policy; if (f == NULL) return; /* * Reports are not permission events, so they take their own short * snapshot reference. A concurrent reload may publish a new generation, * but this report keeps walking the generation it started with. */ policy = policy_snapshot_begin_read(&release_policy); if (policy) rules_hits_report_reset(f, &policy->rules, reset); policy_snapshot_end_read(policy, release_policy); } fapolicyd-2.0/src/library/policy.h000066400000000000000000000070731524071644000172420ustar00rootroot00000000000000/* * policy.h - Header file for policy.c * Copyright (c) 2016,2020,2023 Red Hat * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef POLICY_HEADER #define POLICY_HEADER #include #include #include #include #include "decision-event.h" #include "event.h" #ifdef USE_AUDIT #if HAVE_DECL_FAN_AUDIT #define AUDIT FAN_AUDIT #else #define AUDIT 0x0010 #define FAN_ENABLE_AUDIT 0x00000040 #endif #else #define AUDIT 0x0 #endif #define SYSLOG 0x0020 #define FAN_RESPONSE_MASK (FAN_ALLOW|FAN_DENY|FAN_AUDIT) typedef enum { NO_OPINION = 0, ALLOW = FAN_ALLOW, DENY = FAN_DENY, #ifdef USE_AUDIT ALLOW_AUDIT = FAN_ALLOW | AUDIT, DENY_AUDIT = FAN_DENY | AUDIT, #endif ALLOW_SYSLOG = FAN_ALLOW | SYSLOG, DENY_SYSLOG = FAN_DENY | SYSLOG, ALLOW_LOG = FAN_ALLOW | AUDIT | SYSLOG, DENY_LOG = FAN_DENY | AUDIT | SYSLOG } decision_t; typedef enum { DECISION_SOURCE_RULE, DECISION_SOURCE_FALLTHROUGH } decision_source_t; struct decision_timing_span; typedef struct { unsigned long allowed_by_rule; unsigned long allowed_by_fallthrough; unsigned long fallthrough_open; unsigned long fallthrough_execute; unsigned long fallthrough_trusted; unsigned long fallthrough_untrusted; unsigned long fallthrough_trust_unknown; unsigned long fallthrough_executable; unsigned long fallthrough_programmatic; unsigned long fallthrough_sharedlib; unsigned long fallthrough_unknown_ftype; unsigned long fallthrough_other_ftype; unsigned int ruleset_generation; time_t ruleset_effective_since; } decision_metrics_t; int dec_name_to_val(const char *name); int load_rules(const conf_t *config); int load_rules_from_stream(const conf_t *config, FILE *f); int validate_rules_from_stream(FILE *f); int load_rule_file(void); int do_reload_rules(const conf_t *config); void set_reload_rules(void); decision_t process_event(event_t *e); decision_t process_event_with_source(event_t *e, decision_source_t *source, struct decision_timing_span *response_timing); int reply_event_init(int fd); void reply_event_close_group(atomic_int *group_fd); void reply_event(int fd, const struct fanotify_event_metadata *metadata, unsigned reply, event_t *e); void make_policy_decision(decision_event_t *decision_event, int fd, uint64_t mask); unsigned long getAllowed(void); unsigned long getDenied(void); unsigned long getAllowedReset(int reset); unsigned long getDeniedReset(int reset); unsigned long getReplyErrors(void); void getDecisionMetrics(decision_metrics_t *metrics); void getDecisionMetricsReset(decision_metrics_t *metrics, int reset); void policy_rule_hits_report(FILE *f); void policy_rule_hits_report_reset(FILE *f, int reset); void policy_no_audit(void); void destroy_rules(void); unsigned int policy_get_rules_proc_status_mask(void); unsigned int policy_get_syslog_proc_status_mask(void); unsigned int policy_get_proc_status_mask(void); #endif fapolicyd-2.0/src/library/process.c000066400000000000000000000364221524071644000174140ustar00rootroot00000000000000/* * process.c - functions to access attributes of processes * Copyright (c) 2016,2020-22 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #include "config.h" #include //#ifdef HAVE_STDIO_EXT_H # include //#endif #include #include #include #include #include #include #include #include #include "process.h" #include "file.h" #include "fd-fgets.h" #include "attr-sets.h" #define BUFSZ 12 // Largest unsigned int is 10 characters long /* * This is an optimized integer to string conversion. It only * does base 10 which is exactly what you need to access per * process files in the proc file system. It is about 30% faster * than snprint. */ static const char *uitoa(unsigned int j) { static __thread char buf[BUFSZ]; if (j == 0) return "0"; char *ptr = &buf[BUFSZ - 1]; *ptr = 0; do { *--ptr = '0' + (j % 10); j /= 10; } while (j); return ptr; } static __thread char ppath[40] = "/proc/"; static inline const char *proc_path(pid_t pid, const char *file) { char *p = stpcpy(ppath + 6, uitoa((unsigned int)pid)); if (file) stpcpy(p, file); return ppath; } struct proc_info *stat_proc_entry(pid_t pid) { struct stat sb; const char *path = proc_path(pid, NULL); if (stat(path, &sb) == 0) { struct proc_info *info = malloc(sizeof(struct proc_info)); if (info == NULL) return info; info->pid = pid; info->device = sb.st_dev; info->inode = sb.st_ino; info->time.tv_sec = sb.st_ctim.tv_sec; info->time.tv_nsec = sb.st_ctim.tv_nsec; // Make all paths empty info->path1 = NULL; info->path2 = NULL; info->building_started_ns = 0; info->building_event_count = 0; info->state = STATE_COLLECTING; info->elf_info = 0; return info; } return NULL; } void clear_proc_info(struct proc_info *info) { if (info == NULL) return; free(info->path1); free(info->path2); info->path1 = NULL; info->path2 = NULL; } // Returns 0 if equal and 1 if not equal int compare_proc_infos(const struct proc_info *p1, const struct proc_info *p2) { if (p1 == NULL || p2 == NULL) return 1; // Compare in the order to find likely mismatch first if (p1->inode != p2->inode) return 1; if (p1->pid != p2->pid) return 1; if (p1->time.tv_nsec != p2->time.tv_nsec) return 1; if (p1->time.tv_sec != p2->time.tv_sec) return 1; if (p1->device != p2->device) return 1; return 0; } char *get_program_from_pid(pid_t pid, size_t blen, char *buf) { ssize_t path_len; if (blen == 0) return NULL; const char *path = proc_path(pid, "/exe"); path_len = readlink(path, buf, blen - 1); if (path_len <= 0) { snprintf(buf, blen, "Error-getting-exe(errno=%d,pid=%d)", errno, pid); return buf; } size_t len; if ((size_t)path_len < blen) len = path_len; else len = blen - 1; buf[len] = '\0'; if (len == 0) return buf; // some binaries can be deleted after execution // then we need to delete the suffix so they are // trusted even after deletion // strlen(" deleted") == 10 if (len > 10 && buf[len-1] == ')') { struct stat sb; if (strcmp(&buf[len - 10], " (deleted)") == 0 && stat(buf, &sb) != 0) { buf[len - 10] = '\0'; // reset errno back to 0 so it does not confuse get_subj_attr() if (errno == ENOENT) errno = 0; } } return buf; } char *get_type_from_pid(pid_t pid, size_t blen, char *buf) { int fd; const char *type_path; char fd_path[64]; char exe_path[PATH_MAX]; if (blen == 0) return NULL; const char *path = proc_path(pid, "/exe"); fd = open(path, O_RDONLY|O_NOATIME|O_CLOEXEC); if (fd >= 0) { const char *ptr; struct stat sb; struct file_info i; int len; ssize_t path_len; type_path = path; // Resolve through our fd so the type hint matches the opened file. len = snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", fd); if (len > 0 && (size_t)len < sizeof(fd_path)) { path_len = readlink(fd_path, exe_path, sizeof(exe_path) - 1); if (path_len > 0) { exe_path[path_len] = '\0'; type_path = exe_path; } } // We have to wait for stat to finish so we can set file_info values // for get_file_type_from_fd. if (fstat(fd, &sb) == 0) { i.device = sb.st_dev; i.mode = sb.st_mode; i.size = sb.st_size; ptr = get_file_type_from_fd(fd, &i, type_path, blen, buf); close(fd); return (char *)ptr; } close(fd); } return NULL; } uid_t get_program_auid_from_pid(pid_t pid) { ssize_t rc; int fd; const char *path = proc_path(pid, "/loginuid"); fd = open(path, O_RDONLY|O_CLOEXEC); if (fd >= 0) { char buf[16]; uid_t auid; rc = read(fd, buf, sizeof(buf)-1); close(fd); if (rc > 0) { buf[rc] = 0; // manually terminate, read doesn't errno = 0; auid = strtol(buf, NULL, 10); if (errno == 0) return auid; } } return -1; } int get_program_sessionid_from_pid(pid_t pid) { ssize_t rc; int fd; const char *path = proc_path(pid, "/sessionid"); fd = open(path, O_RDONLY|O_CLOEXEC); if (fd >= 0) { char buf[16]; int ses; rc = read(fd, buf, sizeof(buf)-1); close(fd); if (rc > 0) { buf[rc] = 0; // manually terminate, read doesn't errno = 0; ses = strtol(buf, NULL, 10); if (errno == 0) return ses; } } return -1; } /* * append_group_from_text - Parse one gid token and append it to a set. * @groups: attribute set receiving parsed gids * @text: NUL-terminated text expected to contain one numeric gid * * The helper follows project conversion rules by clearing errno before * strtoul() and checking errno afterward. Non-numeric or out-of-range * values are ignored. */ static void append_group_from_text(attr_sets_entry_t *groups, const char *text) { char *end = NULL; unsigned long value; if (text == NULL || *text == '\0') return; errno = 0; value = strtoul(text, &end, 10); if (errno || end == text || *end != '\0' || value > UINT_MAX) return; attr_set_append_int(groups, (int64_t)value); } /* * consume_groups_fragment - Parse a fragment from /proc//status Groups. * @groups: attribute set receiving parsed gids * @fragment: text fragment containing part (or all) of Groups payload * @line_complete: non-zero when this fragment ends the Groups line * @partial: carry buffer for tokens split across fragments * @partial_len: in/out length of bytes currently stored in @partial * * The helper consumes gid tokens from @fragment while preserving a trailing * partial token when the line is split across read chunks. Parsed gids are * appended to @groups when complete numeric tokens are seen. */ static void consume_groups_fragment(attr_sets_entry_t *groups, const char *fragment, int line_complete, char *partial, size_t *partial_len) { for (const char *p = fragment; *p && *p != '\n'; p++) { if (isdigit((unsigned char)*p)) { if (*partial_len < 31) partial[(*partial_len)++] = *p; continue; } if (*partial_len) { partial[*partial_len] = '\0'; append_group_from_text(groups, partial); *partial_len = 0; } } if (line_complete && *partial_len) { partial[*partial_len] = '\0'; append_group_from_text(groups, partial); *partial_len = 0; } } /* * clear_proc_status_fields - discard requested proc status results. * @info: result storage whose requested fields should be reset. * @fields: bitmap of PROC_STAT_* fields to clear. * * Returns nothing. This leaves requested pointer fields NULL so callers never * observe partial procfs data after a reader or parse failure. */ static void clear_proc_status_fields(struct proc_status_info *info, unsigned int fields) { if (fields & PROC_STAT_UID) { attr_set_destroy(info->uid); info->uid = NULL; } if (fields & PROC_STAT_GID) { attr_set_destroy(info->groups); info->groups = NULL; } if (fields & PROC_STAT_COMM) { free(info->comm); info->comm = NULL; } if (fields & PROC_STAT_PPID) info->ppid = -1; if (fields & PROC_STAT_TRACER) info->tracer_state = PROC_TRACER_UNKNOWN; } /* * read_proc_status_fd - Parse selected fields from a status-like stream. * @fd: descriptor positioned at the beginning of proc status content * @fields: bitmap of PROC_STAT_* flags describing desired data * @info: storage describing the results for the requested fields * * The helper parses the status file once and populates @info for every * requested field. Existing data for the requested fields is released before * new values are recorded. The function returns 0 only when every requested * field is complete. It returns -1 and clears requested results on reader * errors, result-set allocation failures, or incomplete status data. */ int read_proc_status_fd(int fd, unsigned int fields, struct proc_status_info *info) { char buf[80]; char gid_partial[32]; int rc = 0; int in_groups_line = 0; /* GID matching needs both primary credentials and supplemental groups. */ int gid_credentials_found = 0; int gid_groups_found = 0; unsigned int found = 0; size_t gid_partial_len = 0; fd_fgets_state_t *st = NULL; if (info == NULL || fields == 0) return 0; clear_proc_status_fields(info, fields); if (fields & PROC_STAT_UID) { info->uid = attr_set_create(NULL, UNSIGNED); if (info->uid == NULL) goto fail; } if (fields & PROC_STAT_GID) { info->groups = attr_set_create(NULL, UNSIGNED); if (info->groups == NULL) goto fail; } if (fd < 0) goto fail; st = fd_fgets_init(); if (st == NULL) goto fail; do { rc = fd_fgets_r(st, buf, sizeof(buf), fd); if (rc == -1) break; else if (rc > 0) { int line_complete = buf[rc - 1] == '\n'; if ((fields & PROC_STAT_GID) && in_groups_line) { if (info->groups) consume_groups_fragment(info->groups, buf, line_complete, gid_partial, &gid_partial_len); if (line_complete) { gid_groups_found = 1; if (gid_credentials_found) found |= PROC_STAT_GID; in_groups_line = 0; } continue; } if ((fields & PROC_STAT_COMM) && info->comm == NULL && memcmp(buf, "Name:", 5) == 0) { char *name = buf + 5; while (*name == ' ' || *name == '\t') name++; char *newline = strchr(name, '\n'); if (newline) *newline = '\0'; info->comm = strdup(name); if (info->comm == NULL) rc = -1; else found |= PROC_STAT_COMM; continue; } if ((fields & PROC_STAT_PPID) && info->ppid == -1 && memcmp(buf, "PPid:", 5) == 0) { long value; if (sscanf(buf, "PPid: %ld", &value) == 1) { info->ppid = (pid_t)value; found |= PROC_STAT_PPID; } continue; } if ((fields & PROC_STAT_TRACER) && info->tracer_state == PROC_TRACER_UNKNOWN && memcmp(buf, "TracerPid:", 10) == 0) { long value; if (sscanf(buf, "TracerPid: %ld", &value) == 1) { info->tracer_state = value > 0 ? PROC_TRACER_TRACED : PROC_TRACER_NOT_TRACED; found |= PROC_STAT_TRACER; } continue; } /* * UID/GID credentials may differ between the real, * effective, saved, and filesystem slots. Cache all * but saved so the rule engine can evaluate all * possible identities during matching. */ if ((fields & PROC_STAT_UID) && attr_set_empty(info->uid) && memcmp(buf, "Uid:", 4) == 0) { unsigned int real_uid = 0, eff_uid = 0; unsigned int saved_uid = 0, fs_uid = 0; int fields_read = sscanf(buf, "Uid: %u %u %u %u", &real_uid, &eff_uid, &saved_uid, &fs_uid); if (info->uid) { if (fields_read >= 1) attr_set_append_int(info->uid, (int64_t)real_uid); if (fields_read >= 2) attr_set_append_int(info->uid, (int64_t)eff_uid); if (fields_read >= 4) attr_set_append_int(info->uid, (int64_t)fs_uid); } if (fields_read >= 1) found |= PROC_STAT_UID; continue; } if ((fields & PROC_STAT_GID) && attr_set_empty(info->groups) && memcmp(buf, "Gid:", 4) == 0) { unsigned int real_gid = 0, eff_gid = 0; unsigned int saved_gid = 0, fs_gid = 0; int fields_read = sscanf(buf, "Gid: %u %u %u %u", &real_gid, &eff_gid, &saved_gid, &fs_gid); if (info->groups) { if (fields_read >= 1) attr_set_append_int(info->groups, (int64_t)real_gid); if (fields_read >= 2) attr_set_append_int(info->groups, (int64_t)eff_gid); if (fields_read >= 4) attr_set_append_int(info->groups, (int64_t)fs_gid); } if (fields_read >= 1) { gid_credentials_found = 1; if (gid_groups_found) found |= PROC_STAT_GID; } // Wait for the supplemental Groups line before succeeding. continue; } /* * The "Groups" line enumerates supplemental group * memberships as a whitespace separated list; walk the * tokens in place rather than reallocating buffers. * Not checking if empty cause it shouldn't be. */ if ((fields & PROC_STAT_GID) && memcmp(buf, "Groups:", 7) == 0) { if (info->groups) { consume_groups_fragment(info->groups, buf + 7, line_complete, gid_partial, &gid_partial_len); } if (line_complete) { gid_groups_found = 1; if (gid_credentials_found) found |= PROC_STAT_GID; } else { in_groups_line = 1; continue; } continue; } } // if more text, no errors, and we're not done, loop again } while (!fd_fgets_eof_r(st) && rc > 0 && found != fields); if (rc < 0 || found != fields) goto fail; fd_fgets_destroy(st); return 0; fail: if (st) fd_fgets_destroy(st); clear_proc_status_fields(info, fields); return -1; } /* * read_proc_status - Open and parse selected fields from /proc//status. * @pid: identifier of the process to inspect * @fields: bitmap of PROC_STAT_* flags describing desired data * @info: storage describing the results for the requested fields * * Return: 0 on success, -1 if status cannot be opened or parsed. */ int read_proc_status(pid_t pid, unsigned int fields, struct proc_status_info *info) { int fd, rc; const char *path = proc_path(pid, "/status"); fd = open(path, O_RDONLY|O_CLOEXEC); if (fd < 0) return -1; rc = read_proc_status_fd(fd, fields, info); close(fd); return rc; } // Returns 0 if environ is clean, 1 if problems, -1 on error int check_environ_from_pid(pid_t pid) { int rc = -1; char *line = NULL; size_t len = 0; ssize_t nread; FILE *f; const char *path = proc_path(pid, "/environ"); f = fopen(path, "rt"); if (f) { __fsetlocking(f, FSETLOCKING_BYCALLER); while ((nread = getdelim(&line, &len, '\0', f)) != -1) { if (nread < 2) continue; if (strncmp(line, "LD_PRELOAD=", 11) == 0 || strncmp(line, "LD_AUDIT=", 9) == 0) { rc = 1; break; } } fclose(f); if (rc == -1) rc = 0; free(line); } return rc; } fapolicyd-2.0/src/library/process.h000066400000000000000000000076451524071644000174260ustar00rootroot00000000000000/* * process.h - Header file for process.c * Copyright (c) 2016,2019-22 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef PROCESS_HEADER #define PROCESS_HEADER #include #include #include "attr-sets.h" #include "gcc-attributes.h" typedef enum { STATE_COLLECTING=0, // initial state - execute STATE_REOPEN, // anticipating open perm next, always skips the path STATE_DEFAULT_REOPEN, // reopen after dyn. linker exec, never skips the path STATE_STATIC_REOPEN, // static app aniticipating STATE_PARTIAL, // second path collected STATE_STATIC_PARTIAL, // second path collected STATE_FULL, // third path seen - decision time STATE_NORMAL, // normal pattern STATE_NOT_ELF, // not elf, ignore STATE_LD_SO, // app started by ld.so STATE_STATIC, // app is static STATE_BAD_ELF, // app is elf but malformed STATE_LD_PRELOAD // app has LD_PRELOAD or LD_AUDIT set } state_t; // This is used to determine what kind of elf file we are looking at. // HAS_LOAD but no HAS_DYNAMIC is staticly linked app. Normally you see both. #define IS_ELF 0x00001 #define HAS_ERROR 0x00002 // #define HAS_RPATH 0x00004 #define HAS_DYNAMIC 0x00008 #define HAS_LOAD 0x00010 #define HAS_INTERP 0x00020 #define HAS_BAD_INTERP 0x00040 #define HAS_EXEC 0x00080 #define HAS_CORE 0x00100 #define HAS_REL 0x00200 #define HAS_DEBUG 0x00400 #define HAS_RWE_LOAD 0x00800 #define HAS_PHDR 0x01000 #define HAS_EXE_STACK 0x02000 // These next two are used to suppress eviction warnings when the // state is STATE_REOPEN because that is when an interpreter shows up #define HAS_SHEBANG 0x10000 // script with a leading shebang #define TEXT_SCRIPT 0x20000 // likely to be a script /* Bit mask of fields available in /proc//status */ enum { PROC_STAT_PPID = 0x0001, PROC_STAT_UID = 0x0002, PROC_STAT_GID = 0x0004, PROC_STAT_COMM = 0x0008, PROC_STAT_TRACER = 0x0010, }; typedef enum { PROC_TRACER_UNKNOWN, PROC_TRACER_NOT_TRACED, PROC_TRACER_TRACED, } proc_tracer_state_t; /* * Results from read_proc_status() */ struct proc_status_info { pid_t ppid; proc_tracer_state_t tracer_state; attr_sets_entry_t *uid; attr_sets_entry_t *groups; char *comm; }; // Information we will cache to identify the same executable struct proc_info { pid_t pid; dev_t device; ino_t inode; struct timespec time; state_t state; char *path1; char *path2; uint64_t building_started_ns; unsigned int building_event_count; uint32_t elf_info; }; struct proc_info *stat_proc_entry(pid_t pid) __attr_dealloc_free; void clear_proc_info(struct proc_info *info); int compare_proc_infos(const struct proc_info *p1, const struct proc_info *p2); char *get_program_from_pid(pid_t pid, size_t blen, char *buf) __attr_access ((__write_only__, 3, 2)); char *get_type_from_pid(pid_t pid, size_t blen, char *buf) __attr_access ((__write_only__, 3, 2)); uid_t get_program_auid_from_pid(pid_t pid); int get_program_sessionid_from_pid(pid_t pid); int read_proc_status_fd(int fd, unsigned int fields, struct proc_status_info *info) __attr_fd_arg_read (1); int read_proc_status(pid_t pid, unsigned int fields, struct proc_status_info *info); int check_environ_from_pid(pid_t pid); #endif fapolicyd-2.0/src/library/queue.c000066400000000000000000000333061524071644000170600ustar00rootroot00000000000000/* * queue.c - a simple queue implementation * Copyright 2016,2018,2022 Red Hat Inc. * All Rights Reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include "decision-timing.h" #include "queue.h" #include "message.h" #include "string-util.h" struct queue_entry { decision_event_t event; atomic_ullong queued_ns; }; /* * Ring buffer queue * * The queue is a fixed-size ring of decision event envelopes. A semaphore * tracks how many events are queued while atomic indices maintain the next * slot to use for enqueueing and dequeueing. This avoids blocking producers * and consumers on a mutex which improves latency under load. * * q_open() allocates the array and initializes the semaphore and indices. * q_enqueue() copies a new event into the producer slot, advances it and posts * to the semaphore. q_dequeue() waits on the semaphore, reads from the * consumer slot and advances it. * * queue_length is read without locking by the health monitor thread and is * therefore atomic. Using a ring buffer avoids per-event malloc/free and * keeps memory usage predictable. Per-queue metrics record queue pressure for * diagnostics and future structured status output. * * Fanotify reply ownership crosses the thread boundary at q_enqueue(): after a * successful enqueue, the consumer worker owns the event fd inside the * decision_event_t and must eventually call reply_event() or shutdown cleanup. * Producers may answer only when enqueue fails. */ /* * queue_now_ns - read monotonic time for queue age reporting. * Returns monotonic nanoseconds, or zero if the clock cannot be read. */ static uint64_t queue_now_ns(void) { struct timespec ts; if (clock_gettime(CLOCK_MONOTONIC, &ts)) return 0; return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; } /* * q_oldest_age_ns - compute the age of the oldest queued event. * @q: queue to inspect. * @current_depth: current queue depth already captured by the caller. * * The queue is SPSC, so the consumer index identifies the oldest queued slot. * The timestamp is separate from decision_event_t timing because queue health * reports must work even when decision timing collection is disabled. * * Returns age in nanoseconds, or zero when the queue is empty. */ static uint64_t q_oldest_age_ns(const struct queue *q, unsigned int current_depth) { unsigned int n; uint64_t now, queued; if (current_depth == 0) return 0; n = atomic_load_explicit(&q->q_last, memory_order_acquire); if (n >= q->num_entries) return 0; queued = atomic_load_explicit(&q->events[n].queued_ns, memory_order_acquire); if (queued == 0) return 0; now = queue_now_ns(); if (now < queued) return 0; return now - queued; } /* Initialize a queue */ struct queue *q_open(size_t num_entries) { struct queue *q; int saved_errno; size_t i; if (num_entries == 0 || num_entries > UINT32_MAX || num_entries > SIZE_MAX / sizeof(struct queue_entry)) { errno = EINVAL; return NULL; } q = malloc(sizeof(*q)); if (q == NULL) return NULL; q->events = calloc(num_entries, sizeof(struct queue_entry)); if (q->events == NULL) goto err; for (i = 0; i < num_entries; i++) atomic_init(&q->events[i].queued_ns, 0); q->num_entries = num_entries; atomic_store_explicit(&q->q_next, 0, memory_order_relaxed); atomic_store_explicit(&q->q_last, 0, memory_order_relaxed); atomic_store_explicit(&q->queue_length, 0, memory_order_relaxed); atomic_store_explicit(&q->max_depth, 0, memory_order_relaxed); atomic_store_explicit(&q->full_count, 0, memory_order_relaxed); if (sem_init(&q->sem, 0, 0) == -1) { free(q->events); goto err; } return q; err: saved_errno = errno; free(q); errno = saved_errno; return NULL; } void q_close(struct queue *q) { struct queue_metrics metrics; q_metrics_snapshot(q, &metrics); sem_destroy(&q->sem); free(q->events); msg(LOG_DEBUG, "Inter-thread max queue depth %u", metrics.max_depth); free(q); } /* * q_metrics_snapshot - copy the current queue counters. * @q: queue to read. * @metrics: destination for the metric values. * Returns nothing. */ void q_metrics_snapshot(const struct queue *q, struct queue_metrics *metrics) { metrics->current_depth = atomic_load_explicit(&q->queue_length, memory_order_relaxed); metrics->max_depth = atomic_load_explicit(&q->max_depth, memory_order_relaxed); metrics->full_count = atomic_load_explicit(&q->full_count, memory_order_relaxed); metrics->oldest_age_ns = q_oldest_age_ns(q, metrics->current_depth); } /* * q_metrics_snapshot_reset - copy queue counters, optionally resetting them. * @q: queue to read. * @metrics: destination for the metric values. * @reset: non-zero resets interval counters after copying them. * * The current depth is state, not an interval counter. A reset starts max * depth at the current depth so the next report never claims a max lower than * the number of events already queued. */ void q_metrics_snapshot_reset(struct queue *q, struct queue_metrics *metrics, int reset) { unsigned int current; if (!reset) { q_metrics_snapshot(q, metrics); return; } current = atomic_load_explicit(&q->queue_length, memory_order_relaxed); metrics->current_depth = current; metrics->max_depth = atomic_exchange_explicit(&q->max_depth, current, memory_order_relaxed); metrics->full_count = atomic_exchange_explicit(&q->full_count, 0, memory_order_relaxed); metrics->oldest_age_ns = q_oldest_age_ns(q, current); current = atomic_load_explicit(&q->queue_length, memory_order_relaxed); for (;;) { unsigned int max = atomic_load_explicit(&q->max_depth, memory_order_relaxed); if (max >= current || atomic_compare_exchange_weak_explicit(&q->max_depth, &max, current, memory_order_relaxed, memory_order_relaxed)) break; } } /* * q_max_depth_snapshot_reset - copy and reset only the max queue depth. * @q: queue to read. * * Returns the max depth observed before the reset. */ unsigned int q_max_depth_snapshot_reset(struct queue *q) { unsigned int current, saved; current = atomic_load_explicit(&q->queue_length, memory_order_relaxed); saved = atomic_exchange_explicit(&q->max_depth, current, memory_order_relaxed); current = atomic_load_explicit(&q->queue_length, memory_order_relaxed); for (;;) { unsigned int max = atomic_load_explicit(&q->max_depth, memory_order_relaxed); if (max >= current || atomic_compare_exchange_weak_explicit(&q->max_depth, &max, current, memory_order_relaxed, memory_order_relaxed)) break; } return saved; } /* * q_max_depth_snapshot_restore - copy max depth and restore a larger value. * @q: queue to read. * @saved: max depth value saved before a timing run reset. * * Returns the max depth observed for the timing run. */ unsigned int q_max_depth_snapshot_restore(struct queue *q, unsigned int saved) { unsigned int current, reported; current = atomic_load_explicit(&q->max_depth, memory_order_relaxed); reported = current; while (saved > current && !atomic_compare_exchange_weak_explicit(&q->max_depth, ¤t, saved, memory_order_relaxed, memory_order_relaxed)) ; return reported; } /* * q_metrics_report - write queue metrics in the legacy text format. * @f: output stream. * @metrics: queue metrics to report. * Returns nothing. */ void q_metrics_report(FILE *f, const struct queue_metrics *metrics) { char age[32]; fapolicyd_format_ns(metrics->oldest_age_ns, age, sizeof(age)); fprintf(f, "Inter-thread current queue depth: %u\n", metrics->current_depth); fprintf(f, "Inter-thread max queue depth: %u\n", metrics->max_depth); fprintf(f, "Inter-thread queue full count: %lu\n", metrics->full_count); fprintf(f, "Inter-thread oldest queued age: %s\n", age); } /* * q_metrics_report_worker - write metrics for one decision worker queue. * @f: output stream. * @worker_id: decision worker that owns the queue. * @metrics: queue metrics to report. * Returns nothing. */ void q_metrics_report_worker(FILE *f, unsigned int worker_id, const struct queue_metrics *metrics) { char age[32]; fapolicyd_format_ns(metrics->oldest_age_ns, age, sizeof(age)); fprintf(f, " Decision worker %u current queue depth: %u\n", worker_id, metrics->current_depth); fprintf(f, " Decision worker %u max queue depth: %u\n", worker_id, metrics->max_depth); fprintf(f, " Decision worker %u queue full count: %lu\n", worker_id, metrics->full_count); fprintf(f, " Decision worker %u oldest queued age: %s\n", worker_id, age); } /* * q_report - snapshot and write queue metrics for a live queue. * @f: output stream. * @q: queue to report. * Returns nothing. */ void q_report(FILE *f, const struct queue *q) { struct queue_metrics metrics; q_metrics_snapshot(q, &metrics); q_metrics_report(f, &metrics); } /* add DATA to Q */ int q_enqueue(struct queue *q, const decision_event_t *data) { unsigned int n; uint64_t queued_ns; if (atomic_load_explicit(&q->queue_length, memory_order_relaxed) == q->num_entries) { atomic_fetch_add_explicit(&q->full_count, 1, memory_order_relaxed); errno = ENOSPC; return -1; } /* * Load the producer index with relaxed ordering. sem_post() acts as a * release barrier and sem_wait() in q_dequeue() provides the matching * acquire barrier. Because the threads synchronize on the semaphore, * a relaxed load of q_next is sufficient here. */ n = atomic_load_explicit(&q->q_next, memory_order_relaxed); queued_ns = queue_now_ns(); q->events[n].event = *data; q->events[n].event.enqueue_ns = decision_timing_queue_enqueue_time(); atomic_store_explicit(&q->events[n].queued_ns, queued_ns, memory_order_release); n++; if (n == q->num_entries) n = 0; /* * Store the updated producer index with release semantics. The event * was written to q->events above and sem_post() will be issued next. * sem_post() itself is a release barrier and sem_wait() in * q_dequeue() will acquire it, so the combination guarantees the * consumer sees the event before noticing that q_next advanced. */ atomic_store_explicit(&q->q_next, n, memory_order_release); n = atomic_fetch_add_explicit(&q->queue_length, 1, memory_order_relaxed) + 1; unsigned int old = atomic_load(&q->max_depth); while (n > old && !atomic_compare_exchange_weak(&q->max_depth, &old, n)) ; sem_post(&q->sem); return 0; } /* remove one event from Q */ int q_dequeue(struct queue *q, decision_event_t *data) { for (;;) { if (sem_wait(&q->sem)) { if (errno == EINTR) continue; return -1; } if (atomic_load_explicit(&q->queue_length, memory_order_relaxed) == 0) return 0; /* * The consumer waits on sem_wait() above which provides an * acquire barrier for the producer's sem_post(). Because of * that synchronization a relaxed load of the consumer index is * safe here. */ unsigned int n = atomic_load_explicit(&q->q_last, memory_order_relaxed); *data = q->events[n].event; n++; if (n == q->num_entries) n = 0; /* * Release ensures the slot is cleared before we advance the * consumer index. The following sem_post() pairs with the * producer's sem_wait(), so the semaphore again provides the * cross-thread ordering needed for the queue operations. */ atomic_store_explicit(&q->q_last, n, memory_order_release); atomic_fetch_sub_explicit(&q->queue_length, 1, memory_order_relaxed); return 1; } } int q_timed_dequeue(struct queue *q, decision_event_t *data, const struct timespec *ts) { /* * FIXME: sem_timedwait uses CLOCK_REALTIME, so backward wall-clock * steps extend deferred-event and report waits. Investigate * configure-detected sem_clockwait with CLOCK_MONOTONIC and a * fallback for older libc, keeping worker deadlines and the report * timerfd on the same clock. */ for (;;) { if (sem_timedwait(&q->sem, ts)) { if (errno == EINTR) continue; if (errno == ETIMEDOUT) return 0; return -1; } break; } if (atomic_load_explicit(&q->queue_length, memory_order_relaxed) == 0) return 0; /* * The consumer waits on sem_timedwait() above which provides an * acquire barrier for the producer's sem_post(). Because of that * synchronization a relaxed load of the consumer index is safe here. */ unsigned int n = atomic_load_explicit(&q->q_last, memory_order_relaxed); *data = q->events[n].event; n++; if (n == q->num_entries) n = 0; /* * Release ensures the slot is cleared before we advance the consumer * index. The semaphore again provides the cross-thread ordering needed * for the queue operations. */ atomic_store_explicit(&q->q_last, n, memory_order_release); atomic_fetch_sub_explicit(&q->queue_length, 1, memory_order_relaxed); return 1; } void q_shutdown(struct queue *q) { if (q == NULL) return; sem_post(&q->sem); } fapolicyd-2.0/src/library/queue.h000066400000000000000000000065111524071644000170630ustar00rootroot00000000000000/* * queue.h -- a queue abstraction * Copyright 2016,2018,2022 Red Hat Inc. * All Rights Reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef QUEUE_HEADER #define QUEUE_HEADER #include #include #include #include #include #include "decision-event.h" #include "gcc-attributes.h" struct queue_entry; struct queue { /* Ring buffer of fanotify events */ struct queue_entry *events; size_t num_entries; atomic_uint q_next; atomic_uint q_last; atomic_uint queue_length; atomic_uint max_depth; atomic_ulong full_count; sem_t sem; }; struct queue_metrics { unsigned int current_depth; unsigned int max_depth; unsigned long full_count; uint64_t oldest_age_ns; }; /* Close Q. */ void q_close(struct queue *q); /* Open a queue for use */ struct queue *q_open(size_t num_entries) __attribute_malloc__ __attr_dealloc (q_close, 1); /* Copy queue metrics into METRICS. */ void q_metrics_snapshot(const struct queue *q, struct queue_metrics *metrics); /* Copy queue metrics into METRICS and optionally reset interval counters. */ void q_metrics_snapshot_reset(struct queue *q, struct queue_metrics *metrics, int reset); /* Copy and reset only the max depth counter. */ unsigned int q_max_depth_snapshot_reset(struct queue *q); /* Copy the max depth counter and restore SAVED if it was larger. */ unsigned int q_max_depth_snapshot_restore(struct queue *q, unsigned int saved); /* Write queue metrics using the current text report format. */ void q_metrics_report(FILE *f, const struct queue_metrics *metrics); /* Write queue metrics for one decision worker queue. */ void q_metrics_report_worker(FILE *f, unsigned int worker_id, const struct queue_metrics *metrics); /* Write out queue metrics for Q. */ void q_report(FILE *f, const struct queue *q); /* Add DATA to tail of Q. Return 0 on success, -1 on error and set errno. */ int q_enqueue(struct queue *q, const decision_event_t *data); /* Remove one event from Q, storing it into DATA. Return 1 on success or 0 if * the queue is empty. */ int q_dequeue(struct queue *q, decision_event_t *data); /* Remove one event from Q, blocking until timeout. On success return 1. On * timeout return 0 and set errno to ETIMEDOUT. */ int q_timed_dequeue(struct queue *q, decision_event_t *data, const struct timespec *ts); /* Wake up anyone waiting on the queue. */ void q_shutdown(struct queue *q); /* Return the number of entries in Q. */ static inline size_t q_queue_length(const struct queue *q) { return atomic_load_explicit(&q->queue_length, memory_order_relaxed); } #endif fapolicyd-2.0/src/library/rpm-backend.c000066400000000000000000000306571524071644000201250ustar00rootroot00000000000000/* * rpm-backend.c - rpm backend * Copyright (c) 2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "message.h" #include "backend-manager.h" #include "gcc-attributes.h" #include "fd-fgets.h" #include "fapolicyd-backend.h" #include "filter.h" #include "file.h" extern atomic_bool stop; int do_rpm_init_backend(void); int do_rpm_load_list(const conf_t *, int memfd); int do_rpm_destroy_backend(void); static int rpm_init_backend(void); static int rpm_load_list(const conf_t *); static int rpm_destroy_backend(void); backend rpm_backend = { "rpmdb", rpm_init_backend, rpm_load_list, rpm_destroy_backend, -1, -1, }; static rpmts ts = NULL; static rpmdbMatchIterator mi = NULL; static int init_rpm(void) { return rpmReadConfigFiles ((const char *)NULL, (const char *)NULL); } static Header h = NULL; static int get_next_package_rpm(void) { // If this is the first time, create a package iterator if (mi == NULL) { ts = rpmtsCreate(); mi = rpmtsInitIterator(ts, RPMDBI_PACKAGES, NULL, 0); if (mi == NULL) return 0; } if (h) // Decrement reference count, and free memory headerFree(h); h = rpmdbNextIterator(mi); if (h == NULL) return 0; // No more packages, done // Increment reference count headerLink(h); return 1; } static rpmfi fi = NULL; static int get_next_file_rpm(void) { // If its the first time, make file iterator if (fi == NULL) fi = rpmfiNew(NULL, h, RPMTAG_BASENAMES, RPMFI_KEEPHEADER); if (fi) { if (rpmfiNext(fi) == -1) { // No more files, cleanup iterator rpmfiFree(fi); fi = NULL; return 0; } } return 1; } static inline const char *get_file_name_rpm(void) { return rpmfiFN(fi); } static inline rpm_loff_t get_file_size_rpm(void) { return rpmfiFSize(fi); } static char *get_sha256_rpm(int *len) { // The rpm database has SHA512, SHA26, and SHA1 hashes. This uses // a static buffer to avoid a short lived malloc/free cycle. static char sha[SHA512_LEN * 2 + 1]; const unsigned char *digest; size_t tlen = 0; // This gets the binary form of the hash. digest = rpmfiFDigest(fi, NULL, &tlen); if (digest && tlen) // clip to sha512 size. bytes2hex(sha, digest, tlen > SHA512_LEN ? SHA512_LEN : tlen); else sha[0] = 0; // Return the length to avoid a strlen call later. *len = 2*tlen; return sha; } static int is_dir_link_rpm(void) { mode_t mode = rpmfiFMode(fi); if (S_ISDIR(mode) || S_ISLNK(mode)) return 1; return 0; } /* We don't want doc files in the database */ static int is_doc_rpm(void) { if (rpmfiFFlags(fi) & (RPMFILE_DOC|RPMFILE_README| RPMFILE_GHOST|RPMFILE_LICENSE|RPMFILE_PUBKEY)) return 1; return 0; } /* Config files can have a changed hash. We want them in the db since * they are trusted. */ static int is_config_rpm(void) { if (rpmfiFFlags(fi) & (RPMFILE_CONFIG|RPMFILE_MISSINGOK|RPMFILE_NOREPLACE)) return 1; return 0; } /* Files with checksum excluded from %verify should be ignored */ static int is_checksum_ignored_rpm(void) { if (rpmfiVFlags(fi) & RPMVERIFY_FILEDIGEST) { return 0; } return 1; } static void close_rpm(void) { rpmfiFree(fi); fi = NULL; headerFree(h); h = NULL; rpmdbFreeIterator(mi); mi = NULL; rpmtsFree(ts); ts = NULL; rpmFreeCrypto(); rpmFreeRpmrc(); rpmFreeMacros(NULL); rpmlogClose(); } struct _hash_record { const char * key; UT_hash_handle hh; }; #define BUFFER_SIZE 4096 #define MAX_DELIMS 3 // Trustdb has 4 fields - therefore 3 delimiters static const char *rpm_loader_path = LIBEXECDIR "/fapolicyd-rpm-loader"; static int rpm_loader_timeout_ms = BACKEND_LOADER_TIMEOUT_MS; static int rpm_load_list(const conf_t *conf) { // before the spawn int sv[2]; if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, sv) < 0) { msg(LOG_ERR, "socketpair failed"); return 1; } posix_spawn_file_actions_t actions; posix_spawn_file_actions_init(&actions); // dup sv[1] to FD 3 for the child; SOCK_CLOEXEC ensures both // sv[0] and sv[1] are closed on exec, and dup2 clears CLOEXEC // on the target FD 3. When sv[1] is already 3, dup2(3,3) is a // no-op that does NOT clear CLOEXEC, so move sv[1] to a // different FD to avoid the overlap. if (sv[1] == 3) { int tmp = fcntl(sv[1], F_DUPFD_CLOEXEC, 4); if (tmp < 0) { msg(LOG_ERR, "fcntl F_DUPFD_CLOEXEC failed"); close(sv[0]); close(sv[1]); posix_spawn_file_actions_destroy(&actions); return 1; } close(sv[1]); sv[1] = tmp; } posix_spawn_file_actions_adddup2(&actions, sv[1], 3); char *argv[] = { "fapolicyd-rpm-loader", NULL }; char *custom_env[] = { "FAPO_SOCK_FD=3", NULL }; pid_t pid = -1; int spawn_rc = posix_spawn(&pid, rpm_loader_path, &actions, NULL, argv, custom_env); close(sv[1]); // Parent doesn't write if (spawn_rc == 0) { msg(LOG_DEBUG, "fapolicyd-rpm-loader spawned with pid: %d",pid); if (backend_wait_for_loader(sv[0], rpm_loader_timeout_ms)) { char err_buff[BUFFER_SIZE]; msg(LOG_ERR, "rpm loader snapshot wait failed (%s)", strerror_r(errno, err_buff, BUFFER_SIZE)); close(sv[0]); posix_spawn_file_actions_destroy(&actions); return 1; } struct msghdr _msg = {0}; struct iovec iov = { .iov_base = (char[1]){0}, .iov_len = 1 }; union { struct cmsghdr align; char buf[CMSG_SPACE(sizeof(int))]; } cmsgbuf = {0}; _msg.msg_iov = &iov; _msg.msg_iovlen = 1; _msg.msg_control = cmsgbuf.buf; _msg.msg_controllen = sizeof cmsgbuf.buf; ssize_t rc; do { rc = recvmsg(sv[0], &_msg, MSG_DONTWAIT); } while (rc < 0 && errno == EINTR); if (rc < 0) { char err_buff[BUFFER_SIZE]; msg(LOG_ERR, "recvmsg failed (%s)", strerror_r(errno, err_buff, BUFFER_SIZE)); close(sv[0]); posix_spawn_file_actions_destroy(&actions); return 1; } close(sv[0]); struct cmsghdr *c = CMSG_FIRSTHDR(&_msg); if (!c || c->cmsg_type != SCM_RIGHTS) { msg(LOG_ERR, "missing fd"); posix_spawn_file_actions_destroy(&actions); return 1; } int memfd; memcpy(&memfd, CMSG_DATA(c), sizeof memfd); // Mark entries unknown until a fresh snapshot is available. rpm_backend.entries = -1; if (fcntl(memfd, F_SETFD, FD_CLOEXEC) == -1) { char err_buff[BUFFER_SIZE]; msg(LOG_WARNING, "Failed to set CLOEXEC on rpm memfd (%s)", strerror_r(errno, err_buff, BUFFER_SIZE)); } // Pass the memfd to the backend representation rpm_backend.memfd = memfd; } else { close(sv[0]); msg(LOG_ERR, "posix_spawn failed: %s\n", strerror(spawn_rc)); } posix_spawn_file_actions_destroy(&actions); return spawn_rc; } /* * rpm_backend_load_from_path_for_tests - exercise rpm loader IPC errors. * @conf: test configuration. * @loader_path: helper executable path to spawn. * @timeout_ms: maximum snapshot wait used by the test. * * Returns the rpm backend load result without terminating the caller. */ int rpm_backend_load_from_path_for_tests(const conf_t *conf, const char *loader_path, int timeout_ms) { const char *previous_loader_path = rpm_loader_path; int previous_timeout_ms = rpm_loader_timeout_ms; int rc; rpm_loader_path = loader_path; rpm_loader_timeout_ms = timeout_ms; rc = rpm_load_list(conf); rpm_loader_path = previous_loader_path; rpm_loader_timeout_ms = previous_timeout_ms; return rc; } // this function is used in fapolicyd-rpm-loader extern unsigned int debug_mode; int do_rpm_load_list(const conf_t *conf, int memfd) { int rc; unsigned int msg_count = 0; unsigned int tsource = SRC_RPM; // hash table struct _hash_record *hashtable = NULL; long entries = 0; if (memfd < 0) { msg(LOG_ERR, "Invalid memfd supplied to rpm loader"); return 1; } msg(LOG_INFO, "Loading rpmdb backend"); if ((rc = init_rpm())) { msg(LOG_ERR, "init_rpm() failed (%d)", rc); return rc; } // Loop across the rpm database while (!stop && get_next_package_rpm()) { // Loop across the packages while (!stop && get_next_file_rpm()) { // We do not want directories or symlinks in the // database. Multiple packages can own the same // directory and that causes problems in the size info. if (is_dir_link_rpm()) continue; // We do not want any documentation in the database if (is_doc_rpm()) continue; // We do not want any configuration files in database if (is_config_rpm()) continue; // We do not want any files excluded from verifying checksum in database if (is_checksum_ignored_rpm()) { continue; } // Get specific file information const char *tmp = get_file_name_rpm(); // should we drop a path? filter_rc_t f_res = filter_check(tmp); if (f_res != FILTER_ALLOW) { if (f_res == FILTER_ERR_DEPTH) msg(LOG_WARNING, "filter nesting exceeds MAX_FILTER_DEPTH for %s; excluding", tmp); continue; } const char *file_name = strdup(tmp); if (file_name == NULL) continue; trustdb_size_t sz = get_file_size_rpm(); int len; const char *sha = get_sha256_rpm(&len); char *data; // Filter out short digests when rpm_sha256_only is // set. SHA256 and larger are unconditionally accepted. if (len < (SHA256_LEN*2)) { if (conf && conf->rpm_sha256_only) { // Limit this to 5 if production if (debug_mode || msg_count++ < 5) msg(LOG_WARNING, "No acceptable digest for %s", file_name); free((void *)file_name); continue; } } // We use asprintf here so that we can reuse existing // cleanup paths and avoid manual buffer management. if (asprintf( &data, DATA_FORMAT, tsource, sz, sha) == -1) { data = NULL; } if (data) { // getting rid of the duplicates struct _hash_record *rcd = NULL; char key[4096]; snprintf(key, sizeof(key), "%s %s", file_name, data); HASH_FIND_STR( hashtable, key, rcd ); if (!rcd) { rcd = (struct _hash_record*) malloc(sizeof(struct _hash_record)); if (rcd == NULL) { free((void*)file_name); free((void*)data); rc = 1; goto out; } rcd->key = strdup(key); if (rcd->key == NULL) { free(rcd); free((void*)file_name); free((void*)data); rc = 1; goto out; } HASH_ADD_KEYPTR( hh, hashtable, rcd->key, strlen(rcd->key), rcd ); if (dprintf(memfd, "%s %s\n", file_name, data) < 0) { msg(LOG_ERR, "dprintf failed writing %s to memfd (%s)", file_name, strerror(errno)); free((void*)file_name); free((void*)data); rc = 1; goto out; } entries++; free((void*)file_name); free((void*)data); } else { free((void*)file_name); free((void*)data); } } else { free((void*)file_name); } } } rc = 0; out: close_rpm(); // cleaning up struct _hash_record *item, *tmp; HASH_ITER( hh, hashtable, item, tmp) { HASH_DEL( hashtable, item ); free((void*)item->key); free((void*)item); } if (rc == 0) rpm_backend.entries = entries; return rc; } static int rpm_init_backend(void) { return 0; } // this function is used in fapolicyd-rpm-loader int do_rpm_init_backend(void) { if (filter_init()) return 1; if (filter_load_file(NULL)) { filter_destroy(); return 1; } return 0; } static int rpm_destroy_backend(void) { return 0; } // this function is used in fapolicyd-rpm-loader int do_rpm_destroy_backend(void) { filter_destroy(); return 0; } fapolicyd-2.0/src/library/rules.c000066400000000000000000001422041524071644000170640ustar00rootroot00000000000000/* * rules.c - Minimal linked list set of rules * Copyright (c) 2016,2018,2019-20 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * Radovan Sroka */ #include "config.h" #include #include #include #include #include #include #include #include #include #include "attr-sets.h" #include "policy.h" #include "rules.h" #include "nv.h" #include "message.h" #include "file.h" // This seems wrong #include "database.h" #include "process.h" #include "subject-attr.h" #include "object-attr.h" #include "string-util.h" #include "gcc-attributes.h" //#define DEBUG #define UNUSED 0xFF enum rule_parse_result { RULE_PARSE_SKIP = -1, RULE_PARSE_OK = 0, RULE_PARSE_ERROR = 1 }; // Pattern detection #define SYSTEM_LD_CACHE "/etc/ld.so.cache" #define PATTERN_NORMAL_STR "normal" #define PATTERN_NORMAL_VAL 0 #define PATTERN_LD_SO_STR "ld_so" #define PATTERN_LD_SO_VAL 1 #define PATTERN_STATIC_STR "static" #define PATTERN_STATIC_VAL 2 #define PATTERN_LD_PRELOAD_STR "ld_preload" #define PATTERN_LD_PRELOAD_VAL 3 static int assign_subject(llist *l, lnode *n, int type, const char *ptr2, int lineno) __wur; static int assign_object(llist *l, lnode *n, int type, const char *ptr2, int lineno) __wur; enum rule_token_error { RULE_TOKEN_OK = 0, RULE_TOKEN_UNTERMINATED_QUOTE, RULE_TOKEN_TRAILING_TEXT }; /* * next_rule_token - get one space-delimited rule token with quoted values. * @cursor: current mutable position in the rule line, advanced on success. * @error: receives a quoted-value syntax error, if one occurs. * * A double quote begins quoted text only at the start of an attribute value * (immediately after '=' or ','). This preserves literal quotes elsewhere in * existing unquoted values. Quotes are removed in place, and only '\\"' and * '\\\\' are unescaped inside quoted text so fnmatch backslash escapes such as * '\\*' reach glob evaluation unchanged. * * Returns: the next token, or NULL when no token remains or @error is set. */ static char *next_rule_token(char **cursor, enum rule_token_error *error) { char *read, *write, *token; bool quoted = false; if (error) *error = RULE_TOKEN_OK; if (!cursor || !*cursor) return NULL; read = *cursor; while (*read == ' ') read++; if (*read == '\0') { *cursor = read; return NULL; } token = read; write = read; while (*read) { if (quoted) { if (*read == '\\' && (read[1] == '"' || read[1] == '\\')) { *write++ = read[1]; read += 2; continue; } if (*read == '"') { quoted = false; read++; if (*read && *read != ' ' && *read != ',') { if (error) *error = RULE_TOKEN_TRAILING_TEXT; return NULL; } continue; } *write++ = *read++; continue; } if (*read == ' ') break; if (*read == '"' && read > token && (read[-1] == '=' || read[-1] == ',')) { quoted = true; read++; continue; } *write++ = *read++; } if (quoted) { if (error) *error = RULE_TOKEN_UNTERMINATED_QUOTE; return NULL; } while (*read == ' ') read++; *write = '\0'; *cursor = read; return token; } /* * report_rule_token_error - emit an actionable rule-value lexer diagnostic. * @error: quoted-value error reported by next_rule_token(). * @lineno: source line used for the diagnostic. * * Returns: none. */ static void report_rule_token_error(enum rule_token_error error, int lineno) { if (error == RULE_TOKEN_UNTERMINATED_QUOTE) msg(LOG_ERR, "Unterminated quoted value in line %d", lineno); else if (error == RULE_TOKEN_TRAILING_TEXT) msg(LOG_ERR, "Characters after quoted value in line %d", lineno); } /* * validate_glob_set - validate explicitly marked glob values for an attribute * @type: parsed subject or object attribute type. * @set: parsed attribute value set. * @lineno: rule line number used for diagnostics. * @side: rule side used to make the diagnostic actionable. * * The glob: marker distinguishes pattern matching from the historical exact * meaning of every unmarked value. Only exe and path consume patterns, and * their patterns must describe absolute paths. * * Returns: 0 when the set is valid for the attribute, otherwise 1. */ static int validate_glob_set(int type, attr_sets_entry_t *set, int lineno, const char *side) { avl_iterator iterator; avl_str_data_t *entry; const char *attribute; /* Other attributes use the same union member for scalar values. */ if (type != COMM && type != EXE && type != EXE_DIR && type != EXE_TYPE && type != PATH && type != ODIR && type != DEVICE && type != FTYPE && type != FILE_HASH) return 0; if (!set || !set->has_glob) return 0; attribute = type >= SUBJ_START && type <= SUBJ_END ? subj_val_to_name(type, RULE_FMT_COLON) : obj_val_to_name(type); if (type != EXE && type != PATH) { msg(LOG_ERR, "rules: line:%d: %s %s does not support glob patterns; " "glob: is valid only with exe and path", lineno, side, attribute); return 1; } for (entry = (avl_str_data_t *)avl_first(&iterator, &set->tree); entry; entry = (avl_str_data_t *)avl_next(&iterator)) { const char *pattern; if (strncmp(entry->str, ATTR_SET_GLOB_PREFIX, ATTR_SET_GLOB_PREFIX_LEN)) continue; pattern = entry->str + ATTR_SET_GLOB_PREFIX_LEN; if (pattern[0] != '/') { msg(LOG_ERR, "rules: line:%d: %s %s glob pattern must be an " "absolute path", lineno, side, attribute); return 1; } } return 0; } /* * warn_deprecated_untrusted_dir - warn about the legacy dir=untrusted macro * @type: parsed subject or object attribute type. * @set: parsed attribute value set. * @lineno: rule line number used for diagnostics. * @side: rule side used to make the warning actionable. * * The untrusted directory macro was superseded by explicit trust attributes, * but it remains accepted for old rules. Warn when a directory attribute uses * it, including through a named set, so administrators have time to migrate * before a future release removes this compatibility path. */ static void warn_deprecated_untrusted_dir(int type, attr_sets_entry_t *set, int lineno, const char *side) { if ((type == EXE_DIR || type == ODIR) && attr_set_check_str(set, "untrusted")) msg(LOG_WARNING, "rules: line:%d: %s dir=untrusted is deprecated and " "will be removed in a future release", lineno, side); } int rules_create(llist *l) { l->head = NULL; l->cur = NULL; l->cnt = 0; l->sets = attr_sets_create(); if (!l->sets) return 1; l->proc_status_mask = 0; return 0; } void rules_first(llist *l) { l->cur = l->head; } static void rules_last(llist *l) { register lnode* window; if (l->head == NULL) return; window = l->head; while (window->next) window = window->next; l->cur = window; } lnode *rules_next(llist *l) { if (l->cur == NULL) return NULL; l->cur = l->cur->next; return l->cur; } #ifdef DEBUG static void sanity_check_node(lnode *n, const char *id) { unsigned int j, cnt; if (n == NULL) { msg(LOG_DEBUG, "node is NULL"); abort(); } if (n->s_count > MAX_FIELDS) { msg(LOG_DEBUG, "%s - node s_count is out of range %u", id, n->s_count); abort(); } if (n->o_count > MAX_FIELDS) { msg(LOG_DEBUG, "%s - node o_count is out of range %u", id, n->o_count); abort(); } if (n->s_count) { cnt = 0; for (j = 0; j < MAX_FIELDS; j++) { if (n->s[j].type != UNUSED) { cnt++; if (n->s[j].type < SUBJ_START || n->s[j].type > SUBJ_END) { msg(LOG_DEBUG, "%s - subject type is out of range %d", id, n->s[j].type); abort(); } } } if (cnt != n->s_count) { msg(LOG_DEBUG, "%s - subject cnt mismatch %u!=%u", id, cnt, n->s_count); abort(); } } if (n->o_count) { cnt = 0; for (j = 0; j < MAX_FIELDS; j++) { if (n->o[j].type != UNUSED) { cnt++; if (n->o[j].type < OBJ_START || n->o[j].type > OBJ_END) { msg(LOG_DEBUG, "%s - object type is out of range %d", id, n->o[j].type); abort(); } } } if (cnt != n->o_count) { msg(LOG_DEBUG, "%s - object cnt mismatch %u!=%u", id, cnt, n->o_count); abort(); } } } #else #define sanity_check_node(a, b) do {} while(0) #endif #ifdef DEBUG static void sanity_check_list(llist *l, const char *id) { unsigned int i; lnode *n = l->head; if (n == NULL) return; if (l->cnt == 0) { msg(LOG_DEBUG, "%s - zero length cnt found", id); abort(); } i = 1; while (n->next) { if (i == l->cnt) { msg(LOG_DEBUG, "%s - forward loop found %u", id, i); abort(); } sanity_check_node(n, id); i++; n = n->next; } if (i != l->cnt) { msg(LOG_DEBUG, "%s - count mismatch %u!=%u", id, i, l->cnt); abort(); } } #else #define sanity_check_list(a, b) do {} while(0) #endif /* * If subject is trusted function returns true, false otherwise. */ static bool is_subj_trusted(event_t *e) { subject_attr_t *trusted = get_subj_attr(e, SUBJ_TRUST); if (!trusted) return 0; return trusted->uval; } /* * If object is trusted function returns true, false otherwise. */ static bool is_obj_trusted(event_t *e) { object_attr_t *trusted = get_obj_attr(e, OBJ_TRUST); if (!trusted) return 0; return trusted->val; } /* * It takes something like "% set " and it returns "set" */ static char * parse_set_name(char * buf) { // replace % with space buf[0] = ' '; char * name = fapolicyd_strtrim(buf); if (!name) return NULL; // little validation for (int i = 0 ; name[i] ; i++) { if (!(isalnum((unsigned char)name[i]) || name[i] == '_' )) { return NULL; } } return buf; } #define GROUP_NAME_SIZE 64 static const char *data_type_to_name(int type) { switch (type) { case STRING: return "STRING"; case SIGNED: return "SIGNED"; case UNSIGNED: return "UNSIGNED"; default: return "UNKNOWN"; } } static int assign_subject(llist *l, lnode *n, int type, const char *ptr2, int lineno) { // assign the subject unsigned int i = n->s_count; attr_sets_t *sets = l->sets; attr_sets_entry_t *set = NULL; attr_sets_entry_t *owned_set = NULL; sanity_check_node(n, "assign_subject - 1"); n->s[i].type = type; // Opportunistically mark the fields that might be needed for // rule evaluation so that we gather them all at once later. if (type == UID) l->proc_status_mask |= PROC_STAT_UID; else if (type == PPID) l->proc_status_mask |= PROC_STAT_PPID; else if (type == GID) l->proc_status_mask |= PROC_STAT_GID; else if (type == COMM) l->proc_status_mask |= PROC_STAT_COMM; char *ptr, *saved, *tmp = strdup(ptr2); if (tmp == NULL) { msg(LOG_ERR, "memory allocation error in line %d", lineno); return 1; } // use already defined set if (tmp[0] == '%') { char * defined_set = parse_set_name(tmp); if (!defined_set) { msg(LOG_ERR, "rules: line:%d: assign_subject: " "cannot obtain set name from \'%s\'", lineno, tmp); goto free_and_error; } set = attr_sets_find(sets, defined_set); if (!set) { msg(LOG_ERR, "rules: line:%d: assign_subject: " "set \'%s\' was not defined before", lineno, defined_set); goto free_and_error; } // we cannot assign any set to these attributes if (type == SUBJ_TRUST || type == PATTERN) { msg(LOG_ERR, "rules: line:%d: assign_subject: " "cannot assign any set to %s", lineno, subj_val_to_name(type, RULE_FMT_COLON)); goto free_and_error; } /* * GID is a numeric subject attribute, but its enum value lives * below the string attributes after SUBJ_TRUST. */ if (type <= PPID || type == GID) { int expected = (type == PID || type == PPID) ? SIGNED : UNSIGNED; if (set->type != expected) { msg(LOG_ERR, "rules: line:%d: assign_subject: " "cannot assign %%%s which has %s type " "to %s (%s expected)", lineno, defined_set, data_type_to_name(set->type), subj_val_to_name(type, RULE_FMT_COLON), data_type_to_name(expected)); goto free_and_error; } } if (type >= COMM && set->type != STRING) { msg(LOG_ERR, "rules: line:%d: assign_subject: " "cannot assign %%%s which has %s type to %s " "(STRING expected)", lineno, defined_set, data_type_to_name(set->type), subj_val_to_name(type, RULE_FMT_COLON)); goto free_and_error; } n->s[i].set = set; goto finalize; } // for debug output char name[GROUP_NAME_SIZE]; memset(name, 0, GROUP_NAME_SIZE); snprintf(name, GROUP_NAME_SIZE-1, "_rule-line-%d-subj-%s", lineno, subj_val_to_name(type, RULE_FMT_COLON)); switch(n->s[i].type) { case ALL_SUBJ: break; // numbers -> multiple value case AUID: case UID: case SESSIONID: case GID: case PID: case PPID: { int set_type = (n->s[i].type == PID || n->s[i].type == PPID) ? SIGNED : UNSIGNED; owned_set = attr_set_create(name, set_type); set = owned_set; if (!set) goto free_and_error; ptr = strtok_r(tmp, ",", &saved); while (ptr) { ptr = fapolicyd_strtrim(ptr); if (!ptr || *ptr == '\0') { ptr = strtok_r(NULL, ",", &saved); continue; } if (isdigit((unsigned char)*ptr) || *ptr == '-') { errno = 0; if (n->s[i].type == PID || n->s[i].type == PPID) { long val = strtol(ptr, NULL, 10); if (errno) { msg(LOG_ERR, "Error converting val (%s) in line %d", ptr, lineno); goto free_and_error; } else if (attr_set_append_int(set, (int64_t)val)) { goto free_and_error; } } else { if (*ptr == '-') { msg(LOG_ERR, "rules: line:%d: assign_subject: " "negative value %s not allowed for %s", lineno, ptr, subj_val_to_name(type, RULE_FMT_COLON)); goto free_and_error; } unsigned long val = strtoul(ptr, NULL, 10); if (errno) { msg(LOG_ERR, "Error converting val (%s) in line %d", ptr, lineno); goto free_and_error; } else if (attr_set_append_int(set, (int64_t)val)) { goto free_and_error; } } // Support names for auid and uid entries } else if (n->s[i].type == AUID || n->s[i].type == UID) { struct passwd *pw = getpwnam(ptr); if (pw == NULL) { msg(LOG_ERR, "user %s is unknown", ptr); goto free_and_error; } unsigned int val = pw->pw_uid; endpwent(); if (attr_set_append_int(set, (int64_t)val)) goto free_and_error; } else if (n->s[i].type == GID) { struct group *gr = getgrnam(ptr); if (gr == NULL) { msg(LOG_ERR, "group %s is unknown", ptr); goto free_and_error; } unsigned int val = gr->gr_gid; endgrent(); if (attr_set_append_int(set, (int64_t)val)) goto free_and_error; } ptr = strtok_r(NULL, ",", &saved); } if (attr_sets_add(sets, set)) goto free_and_error; n->s[i].set = set; owned_set = NULL; break; } // case // single value exception case PATTERN: { if (strchr(tmp, ',')) { msg(LOG_ERR, "rules: line:%d: assign_subject: " "pattern can handle only single value", lineno); goto free_and_error; } if (strcmp(tmp, PATTERN_LD_SO_STR) == 0) { n->s[i].uval = PATTERN_LD_SO_VAL; } else if (strcmp(tmp, PATTERN_NORMAL_STR) == 0) { n->s[i].uval = PATTERN_NORMAL_VAL; } else if (strcmp(tmp, PATTERN_STATIC_STR) == 0) { n->s[i].uval = PATTERN_STATIC_VAL; } else if (strcmp(tmp, PATTERN_LD_PRELOAD_STR) == 0) { n->s[i].uval = PATTERN_LD_PRELOAD_VAL; } else { msg(LOG_ERR, "Unknown pattern value %s in line %d", tmp, lineno); goto free_and_error; } break; } // case // single value exception case SUBJ_TRUST: { if (strchr(tmp, ',')) { msg(LOG_ERR, "rules: line:%d: assign_subject: " "trust can handle only single value", lineno); goto free_and_error; } errno = 0; unsigned long val = strtoul(tmp, NULL, 10); if (errno) { msg(LOG_ERR, "Error converting val (%s) in line %d", tmp, lineno); goto free_and_error; } else { if (val != 1 && val != 0) { msg(LOG_ERR, "rules: line:%d: assign_subject: " "trust can be set to 1 or 0", lineno); goto free_and_error; } n->s[i].uval = (unsigned int)val; } break; } // case // regular strings -> multiple value case COMM: case EXE: case EXE_DIR: case EXE_TYPE: { owned_set = attr_set_create(name, STRING); set = owned_set; if (!set) goto free_and_error; ptr = strtok_r(tmp, ",", &saved); while (ptr) { if (!attr_set_check_str(set, ptr) && attr_set_append_str(set, ptr)) goto free_and_error; ptr = strtok_r(NULL, ",", &saved); } if (attr_sets_add(sets, set)) goto free_and_error; n->s[i].set = set; owned_set = NULL; break; } // case // should not happen default: { msg(LOG_ERR, "assign_subject: fatal error " "-> this should not happen!"); goto free_and_error; } // case } // switch finalize: if (validate_glob_set(type, n->s[i].set, lineno, "subject")) goto free_and_error; warn_deprecated_untrusted_dir(type, n->s[i].set, lineno, "subject"); n->s_count++; free(tmp); sanity_check_node(n, "assign_subject - 2"); return 0; free_and_error: attr_set_destroy(owned_set); free(tmp); return 1; } static int assign_object(llist *l, lnode *n, int type, const char *ptr2, int lineno) { // assign the object unsigned int i = n->o_count; attr_sets_t *sets = l->sets; attr_sets_entry_t *set = NULL; attr_sets_entry_t *owned_set = NULL; sanity_check_node(n, "assign_object - 1"); n->o[i].type = type; char *ptr, *saved, *tmp = strdup(ptr2); if (tmp == NULL) { msg(LOG_ERR, "memory allocation error in line %d", lineno); return 1; } // use already defined set if (tmp[0] == '%') { char * defined_set = parse_set_name(tmp); if (!defined_set) { msg(LOG_ERR, "rules: line:%d: assign_object: " "cannot obtain set name from \'%s\'", lineno, tmp); goto free_and_error; } set = attr_sets_find(sets, defined_set); if (!set) { msg(LOG_ERR, "rules: line:%d: assign_object: " "set \'%s\' was not defined before", lineno, defined_set); goto free_and_error; } // we cannot assign any set to these attributes if (type == OBJ_TRUST) { msg(LOG_ERR, "rules: line:%d: assign_object: " "cannot assign any set to %s", lineno, obj_val_to_name(type)); goto free_and_error; } // strings if (set->type != STRING) { msg(LOG_ERR, "rules: line:%d: assign_object: " "cannot assign SIGNED set %s to the STRING " "attribute", lineno, defined_set); goto free_and_error; } n->o[i].set = set; goto finalize; } // for debug output char name[GROUP_NAME_SIZE]; memset(name, 0, GROUP_NAME_SIZE); snprintf(name, GROUP_NAME_SIZE-1, "_rule-line-%d-obj-%s", lineno, obj_val_to_name(type)); switch(n->o[i].type) { case ALL_OBJ: break; case OBJ_TRUST: { if (strchr(tmp, ',')) { msg(LOG_ERR, "rules: line:%d: assign_object: " "trust can handle only single value", lineno); goto free_and_error; } errno = 0; long val = strtol(tmp, NULL, 10); if (errno) { msg(LOG_ERR, "Error converting val (%s) in line %d", tmp, lineno); goto free_and_error; } else { if (val != 1 && val != 0) { msg(LOG_ERR, "rules: line:%d: assign_object: " "trust can be set to 1 or 0", lineno); goto free_and_error; } n->o[i].val = val; } break; } // case case ODIR: case PATH: case DEVICE: case FTYPE: case FILE_HASH: { owned_set = attr_set_create(name, STRING); set = owned_set; if (!set) goto free_and_error; ptr = strtok_r(tmp, ",", &saved); while (ptr) { if (!attr_set_check_str(set, ptr) && attr_set_append_str(set, ptr)) goto free_and_error; ptr = strtok_r(NULL, ",", &saved); } if (attr_sets_add(sets, set)) goto free_and_error; n->o[i].set = set; owned_set = NULL; break; } // case // should not happen default: { msg(LOG_ERR, "assign_object: fatal error " "-> this should not happen!"); goto free_and_error; } // case } // switch finalize: if (validate_glob_set(type, n->o[i].set, lineno, "object")) goto free_and_error; warn_deprecated_untrusted_dir(type, n->o[i].set, lineno, "object"); n->o_count++; free(tmp); sanity_check_node(n, "assign_object - 2"); return 0; free_and_error: attr_set_destroy(owned_set); free(tmp); return 1; } static int parse_new_format(llist *l, lnode *n, int lineno, char **cursor) { int state = 0; // 0 == subj, 1 == obj char *ptr; enum rule_token_error token_error; while ((ptr = next_rule_token(cursor, &token_error))) { int type; char *ptr2 = strchr(ptr, '='); if (ptr2) { *ptr2 = 0; ptr2++; if (state == 0) { type = subj_name_to_val(ptr, RULE_FMT_COLON); if (type == -1) { msg(LOG_ERR, "Field type (%s) is unknown in line %d", ptr, lineno); return 1; } if (assign_subject(l, n, type, ptr2, lineno)) return 1; } else { type = obj_name_to_val(ptr); if (type == -1) { msg(LOG_ERR, "Field type (%s) is unknown in line %d", ptr, lineno); return 2; } else if (assign_object(l, n, type, ptr2, lineno)) return 1; } } else if (state == 0 && strcmp(ptr, ":") == 0) state = 1; else if (strcmp(ptr, "all") == 0) { if (state == 0) { type = ALL_SUBJ; if (assign_subject(l, n, type, "", lineno)) return 1; } else { type = ALL_OBJ; if (assign_object(l, n, type, "", lineno)) return 1; } } else { msg(LOG_ERR, "'=' is missing for field %s, in line %d", ptr, lineno); return 5; } } if (token_error != RULE_TOKEN_OK) { report_rule_token_error(token_error, lineno); return 5; } return 0; } /* * append_language_set_values - add MIME types to the %languages set. * @set: existing string-valued %languages set. * @values: mutable comma-separated MIME list. * @lineno: rule file line number used for diagnostics. * * Existing MIME entries are intentionally ignored so independently packaged * policy fragments can name the same language without breaking fagenrules. * Returns 0 on success and 1 on invalid input or allocation failure. */ static int append_language_set_values(attr_sets_entry_t *set, char *values, int lineno) { char *ptr, *saved; int found = 0; if (set->type != STRING) { msg(LOG_ERR, "rules.conf:%d: parse_set_line: " "set languages must have STRING type", lineno); return 1; } ptr = strtok_r(values, ",", &saved); while (ptr) { ptr = fapolicyd_strtrim(ptr); if (*ptr) { found = 1; /* ftype sets use exact MIME strings, never path globs. */ if (strncmp(ptr, ATTR_SET_GLOB_PREFIX, ATTR_SET_GLOB_PREFIX_LEN) == 0) { msg(LOG_ERR, "rules.conf:%d: parse_set_line: " "glob values are not valid in %%languages", lineno); return 1; } /* Multiple packages may name the same MIME type. */ if (!attr_set_check_str(set, ptr) && attr_set_append_str(set, ptr)) return 1; } ptr = strtok_r(NULL, ",", &saved); } if (!found) { msg(LOG_ERR, "rules.conf:%d: parse_set_line: " "%%languages+= requires at least one MIME type", lineno); return 1; } return 0; } /* * parse_set_line - parse an attribute set definition. * @sets: rule-load registry that owns named sets. * @line: rule file line that starts with a '%' set name. * @lineno: rule file line number used for diagnostics. * * The parser validates the set name, infers whether the values are strings * or integers, creates the set, and appends every parsed value. The * %languages set additionally accepts += to let package rule fragments add * MIME types without replacing the distribution-owned definition. Set lines * define parser state only; they do not become policy rule nodes. * * Returns: 0 on success, 1 on parse or allocation errors. */ static int parse_set_line(attr_sets_t *sets, const char *line, int lineno) { attr_sets_entry_t *set = NULL; attr_sets_entry_t *existing; bool append = false; if (!line) return 1; char * l = strdup(line); if (!l) { return 1; } char * sep = strchr(l, '='); if (!sep) { msg(LOG_ERR, "rules.conf:%d: parse_set_line: " "Cannot parse line, no separator \"=\"", lineno); goto free_and_error; } else if (sep > l && sep[-1] == '+') { append = true; sep[-1] = '\0'; } else { *sep = '\0'; } char * name = parse_set_name(l); if (!name) { msg(LOG_ERR, "rules.conf:%d: parse_set_line: " "Cannot parse name of the set", lineno); goto free_and_error; } existing = attr_sets_find(sets, name); if (append) { /* * Keep mutable-set semantics confined to the policy's language * macro. General set appends could silently alter unrelated rules. */ if (strcmp(name, "languages") != 0) { msg(LOG_ERR, "rules.conf:%d: parse_set_line: " "set append is only supported for %%languages", lineno); goto free_and_error; } if (!existing) { msg(LOG_ERR, "rules.conf:%d: parse_set_line: " "set languages must be defined before it can be extended", lineno); goto free_and_error; } if (append_language_set_values(existing, fapolicyd_strtrim(sep + 1), lineno)) goto free_and_error; free(l); return 0; } if (existing) { msg(LOG_ERR, "rules.conf:%d: parse_set_line: " "set %s was already defined!", lineno, name); goto free_and_error; } char *ptr, *saved, *tmp = sep + 1; char *values = NULL; tmp = fapolicyd_strtrim(tmp); int type = STRING; bool numeric_found = false; values = strdup(tmp); if (!values) goto free_and_error; char *val_ptr, *val_saved; val_ptr = strtok_r(values, ",", &val_saved); while (val_ptr) { char *token = fapolicyd_strtrim(val_ptr); if (!token || *token == '\0') { val_ptr = strtok_r(NULL, ",", &val_saved); continue; } errno = 0; char *endptr = NULL; long long sval = strtoll(token, &endptr, 10); if (errno == 0 && endptr && *endptr == '\0') { numeric_found = true; if (sval < 0) type = SIGNED; else if (type != SIGNED) type = UNSIGNED; } else { type = STRING; numeric_found = false; break; } val_ptr = strtok_r(NULL, ",", &val_saved); } if (!numeric_found) type = STRING; free(values); values = NULL; set = attr_set_create(name, type); if (!set) goto free_and_error; ptr = strtok_r(tmp, ",", &saved); while (ptr) { ptr = fapolicyd_strtrim(ptr); if (!ptr || *ptr == '\0') { ptr = strtok_r(NULL, ",", &saved); continue; } if (type == STRING) { if (attr_set_append_str(set, ptr)) goto free_and_error; } else if (type == SIGNED) { errno = 0; long val = strtol(ptr, NULL, 10); if (errno) { msg(LOG_ERR, "Error converting val (%s) in line %d", ptr, lineno); goto free_and_error; } else if (attr_set_append_int(set, (int64_t)val)) goto free_and_error; } else if (type == UNSIGNED) { if (*ptr == '-') { msg(LOG_ERR, "Error converting val (%s) in line %d", ptr, lineno); goto free_and_error; } errno = 0; unsigned long val = strtoul(ptr, NULL, 10); if (errno) { msg(LOG_ERR, "Error converting val (%s) in line %d", ptr, lineno); goto free_and_error; } else if (attr_set_append_int(set, (int64_t)val)) goto free_and_error; } ptr = strtok_r(NULL, ",", &saved); } if (attr_sets_add(sets, set)) goto free_and_error; set = NULL; free(l); return 0; free_and_error: attr_set_destroy(set); free(l); return 1; } /* * nv_split - parse one rule file line * @sets: rule-load registry that owns parsed attribute sets * @buf: mutable rule file line to parse * @n: rule node populated when the line contains a policy rule * @lineno: rule file line number used for diagnostics * * Empty lines, comments, and attribute set definitions are handled by the * parser but should not be appended as policy rule nodes. Those successful * non-rule lines return RULE_PARSE_SKIP. * * Returns: RULE_PARSE_OK when @n contains a rule, RULE_PARSE_SKIP when the * line should not append a rule node, or RULE_PARSE_ERROR on parse failure. */ static enum rule_parse_result nv_split(llist *l, char *buf, lnode *n, int lineno) { char *ptr, *ptr2; char *cursor = buf; rformat_t format = RULE_FMT_ORIG; attr_sets_t *sets = l->sets; enum rule_token_error token_error; if (strchr(buf, ':')) format = RULE_FMT_COLON; n->format = format; ptr = next_rule_token(&cursor, &token_error); if (token_error != RULE_TOKEN_OK) { report_rule_token_error(token_error, lineno); return RULE_PARSE_ERROR; } if (ptr == NULL) return RULE_PARSE_SKIP; if (ptr[0] == '#') return RULE_PARSE_SKIP; if (ptr[0] == '%') { if (parse_set_line(sets, ptr, lineno)) return RULE_PARSE_ERROR; return RULE_PARSE_SKIP; } // Load decision n->d = dec_name_to_val(ptr); if ((int)n->d == -1) { msg(LOG_ERR, "Invalid decision (%s) in line %d", ptr, lineno); return RULE_PARSE_ERROR; } // Default access permission is open n->a = OPEN_ACC; while ((ptr = next_rule_token(&cursor, &token_error))) { int type; ptr2 = strchr(ptr, '='); if (ptr2) { *ptr2 = 0; ptr2++; if (format == RULE_FMT_COLON) { if (strcmp(ptr, "perm") == 0) { if (strcmp(ptr2, "execute") == 0) n->a = EXEC_ACC; else if (strcmp(ptr2, "any") == 0) n->a = ANY_ACC; else if (strcmp(ptr2, "open")) { msg(LOG_ERR, "Access permission (%s) is unknown in line %d", ptr2, lineno); return RULE_PARSE_ERROR; } } else { type = subj_name_to_val(ptr, RULE_FMT_COLON); if (type == -1) { msg(LOG_ERR, "Field type (%s) is unknown in line %d", ptr, lineno); return RULE_PARSE_ERROR; } if (assign_subject(l, n, type, ptr2, lineno)) return RULE_PARSE_ERROR; } if (parse_new_format(l, n, lineno, &cursor)) return RULE_PARSE_ERROR; goto finish_up; } type = subj_name_to_val(ptr, format); if (type == -1) { type = obj_name_to_val(ptr); if (type == -1) { msg(LOG_ERR, "Field type (%s) is unknown in line %d", ptr, lineno); return RULE_PARSE_ERROR; } else if (assign_object(l, n, type, ptr2, lineno)) return RULE_PARSE_ERROR; } else if (assign_subject(l, n, type, ptr2, lineno)) return RULE_PARSE_ERROR; } else if (strcmp(ptr, "all") == 0) { if (n->s_count == 0) { type = ALL_SUBJ; if (assign_subject(l, n, type, "", lineno)) return RULE_PARSE_ERROR; } else if (n->o_count == 0) { type = ALL_OBJ; if (assign_object(l, n, type, "", lineno)) return RULE_PARSE_ERROR; } else { msg(LOG_ERR, "All can only be used in place of a subject or object"); return RULE_PARSE_ERROR; } } else { msg(LOG_ERR, "'=' is missing for field %s, in line %d", ptr, lineno); return RULE_PARSE_ERROR; } } if (token_error != RULE_TOKEN_OK) { report_rule_token_error(token_error, lineno); return RULE_PARSE_ERROR; } finish_up: // do one last sanity check for missing subj or obj if (n->s_count == 0) { msg(LOG_ERR, "Subject is missing in line %d", lineno); return RULE_PARSE_ERROR; } if (n->o_count == 0) { msg(LOG_ERR, "Object is missing in line %d", lineno); return RULE_PARSE_ERROR; } return RULE_PARSE_OK; } // This function take a whole rule as input and passes it to nv_split. // Returns 0 if success and 1 on rule failure. int rules_append(llist *l, char *buf, unsigned int lineno) { lnode* newnode; sanity_check_list(l, "rules_append - 1"); if (buf && l->sets) { // parse up the rule unsigned int i; newnode = malloc(sizeof(lnode)); if (newnode == NULL) return 1; memset(newnode, 0, sizeof(lnode)); newnode->s_count = 0; newnode->o_count = 0; atomic_init(&newnode->hits, 0); newnode->text = strdup(buf); if (newnode->text == NULL) { free(newnode); return 1; } for (i=0; is[i].type = UNUSED; newnode->o[i].type = UNUSED; } enum rule_parse_result rc = nv_split(l, buf, newnode, lineno); if (rc == RULE_PARSE_SKIP) { free(newnode->text); free(newnode); return 0; } else if (rc == RULE_PARSE_ERROR) { free(newnode->text); free(newnode); return 1; } } else return 1; newnode->next = NULL; rules_last(l); // if we are at top, fix this up if (l->head == NULL) l->head = newnode; else // Otherwise add pointer to newnode l->cur->next = newnode; // make newnode current l->cur = newnode; newnode->num = l->cnt; newnode->lineno = lineno; l->cnt++; sanity_check_list(l, "rules_append - 2"); return 0; } // In this table, the number is string length static const nv_t dirs[] = { { 5, "/etc/"}, { 5, "/usr/"}, { 5, "/bin/"}, { 6, "/sbin/"}, { 5, "/lib/"}, { 7, "/lib64/"}, {13, "/usr/libexec/"} }; #define NUM_DIRS (sizeof(dirs)/sizeof(dirs[0])) // Returns 0 if no match, 1 if a match static int check_dirs(unsigned int i, const char *path) { // Iterate across the lists looking for a match. // If we match, stop iterating and return a decision. for (; i < NUM_DIRS; i++) { // Check to see if we even care about this path if (strncmp(path, dirs[i].name, dirs[i].value) == 0) return 1; } return 0; } /* * path_set_match - match one concrete path against a rule string set * @set: string set assigned to an exe or path rule attribute. * @path: concrete executable or object path from the event. * * Unmarked entries always use exact lookup, including literal filenames that * contain glob metacharacters. Only entries beginning with glob: use fnmatch. * Patterns are pathname-component scoped, case-sensitive, and require leading * periods to be explicit. The event path is never changed, so trust lookup and * logging continue to use the concrete path. * * Returns: 1 when an exact value or glob matches, otherwise 0. */ static int path_set_match(attr_sets_entry_t *set, const char *path) { avl_iterator iterator; avl_str_data_t *entry; if (!set || set->type != STRING || !set->has_glob || !path) return attr_set_check_str(set, path); if (attr_set_check_str(set, path)) return 1; for (entry = (avl_str_data_t *)avl_first(&iterator, &set->tree); entry; entry = (avl_str_data_t *)avl_next(&iterator)) { if (strncmp(entry->str, ATTR_SET_GLOB_PREFIX, ATTR_SET_GLOB_PREFIX_LEN) == 0 && fnmatch(entry->str + ATTR_SET_GLOB_PREFIX_LEN, path, FNM_PATHNAME | FNM_PERIOD) == 0) return 1; } return 0; } /* * Notes about elf program startup * =============================== * The run time linker will do the folowing: * 1) kernel loads executable * 2) kernel attaches ld-2.2x.so to executable memory and turns over execution * 3) rtl loads LD_AUDIT libs * 4) rtl loads LD_PRELOAD libs * 5) rtl next loads /etc/ld.so.preload libs * * Then for each dependency: * Call into LD_AUDIT la_objsearch() to modify path/name and try * 1) RPATH in object * 2) RPATH in executable * 3) LD_LIBRARY_PATH: for each path, iterate permutations of * tls, x86_64, haswell, & plain path * 4) RUNPATH in object * 5) Try the name as found in the object * 6) Consult /etc/ld.so.cache * 7) Try default path (can't find where string table is) * * LD_AUDIT modules can add arbitrary early file system actions because * the may also call open. They can also trigger loading another copy of * libc.so.6. * * Patterns * ======== * Normal: * exe=/usr/bin/bash file=/usr/bin/ls * exe=/usr/bin/bash file=/usr/lib64/ld-2.27.so * exe=/usr/bin/ls file=/etc/ld.so.cache * exe=/usr/bin/ls file=/usr/lib64/libselinux.so.1 * * runtime linker started: * exe=/usr/bin/bash file=/usr/lib64/ld-2.27.so * exe=/usr/bin/bash file=/usr/bin/ls * exe=/usr/lib64/ld-2.27.so file=/etc/ld.so.cache * exe=/usr/lib64/ld-2.27.so file=/usr/lib64/libselinux.so.1 * * LD_PRELOAD=libaudit no LD_LIBRARY_PATH: * exe=/usr/bin/bash file=/usr/bin/ls * exe=/usr/bin/bash file=/usr/lib64/ld-2.27.so * exe=/usr/bin/ls file=/usr/lib64/libaudit.so.1.0.0 * exe=/usr/bin/ls file=/etc/ld.so.cache * exe=/usr/bin/ls file=/usr/lib64/libselinux.so.1 * * LD_PRELOAD=libaudit with LD_LIBRARY_PATH: * exe=/usr/bin/bash file=/usr/bin/ls * exe=/usr/bin/bash file=/usr/lib64/ld-2.28.so * exe=/usr/bin/ls file=/usr/lib64/libaudit.so.1.0.0 * exe=/usr/bin/ls file=/usr/lib64/libselinux.so.1 * * /etc/ld.so.preload: * exe=/usr/bin/bash file=/usr/bin/ls * exe=/usr/bin/bash file=/usr/lib64/ld-2.27.so * exe=/usr/bin/ls file=/etc/ld.so.preload * exe=/usr/bin/ls file=/usr/lib64/libaudit.so.1.0.0 * * This means only first two can be counted on. Looking for ld.so.cache * is no good because its almost the last option. * * kworker: * exe=kworker/u130:6 : path=/usr/bin/cat * exe=kworker/u130:6 : path=/usr/lib64/ld-linux-x86-64.so.2 * exe=/usr/bin/cat : path=/etc/ld.so.cache * exe=/usr/bin/cat : path=/usr/lib64/libc.so.6 * * Springs to life without ever being an object. Becomes STATE_NORMAL. */ // Returns 0 if no match, 1 if a match, -1 on error static int subj_pattern_test(const subject_attr_t *s, event_t *e) { int rc = 0; struct proc_info *pinfo = e->s->info; // At this point, we have only 1 or 2 paths. if (pinfo->state < STATE_FULL) { // if it's not an elf file, we're done if (pinfo->elf_info == 0) { pinfo->state = STATE_NOT_ELF; clear_proc_info(pinfo); } // If its a static, make a decision. EXEC_PERM will cause // a follow up open request. We change state here and will // go all the way to static on the open request. else if ((pinfo->elf_info & IS_ELF) && (pinfo->state == STATE_COLLECTING) && ((pinfo->elf_info & HAS_DYNAMIC) == 0)) { pinfo->state = STATE_STATIC_REOPEN; goto make_decision; } else if (pinfo->state == STATE_STATIC_PARTIAL) goto make_decision; else if ((e->type & FAN_OPEN_EXEC_PERM) && pinfo->path1 && strcmp(pinfo->path1, SYSTEM_LD_SO) == 0) { pinfo->state = STATE_LD_SO; msg(LOG_DEBUG, "pid %d ld.so exec path1=%s path2=%s", pinfo->pid, pinfo->path1 ? pinfo->path1 : "(null)", pinfo->path2 ? pinfo->path2 : "(null)"); goto make_decision; } // otherwise, we don't have enough info for a decision return rc; } // Do the analysis if (pinfo->state == STATE_FULL) { if (pinfo->elf_info & HAS_ERROR) { pinfo->state = STATE_BAD_ELF; clear_proc_info(pinfo); return -1; } // Pattern detection is only static or not, ld.so started or // not. That means everything else is normal. if (strcmp(pinfo->path1, SYSTEM_LD_SO) == 0) { // First thing is ld.so when its used - detected above pinfo->state = STATE_LD_SO; msg(LOG_DEBUG, "pid %d ld.so early path1=%s path2=%s", pinfo->pid, pinfo->path1, pinfo->path2); } else // To get here, pgm matched path1 pinfo->state = STATE_NORMAL; } // Make a decision make_decision: switch (s->uval) { case PATTERN_NORMAL_VAL: if (pinfo->state == STATE_NORMAL) rc = 1; break; case PATTERN_LD_SO_VAL: if (pinfo->state == STATE_LD_SO) rc = 1; break; case PATTERN_STATIC_VAL: if ((pinfo->state == STATE_STATIC_REOPEN) || (pinfo->state == STATE_STATIC_PARTIAL) || (pinfo->state == STATE_STATIC)) rc = 1; break; case PATTERN_LD_PRELOAD_VAL: { int env = check_environ_from_pid(pinfo->pid); if (env == 1) { pinfo->state = STATE_LD_PRELOAD; rc = 1; } } break; } // Done with the paths clear_proc_info(pinfo); return rc; } // Returns 0 if no match, 1 if a match static int check_access(const lnode *r, const event_t *e) { access_t perm; if (r->a == ANY_ACC) return 1; if (e->type & FAN_OPEN_EXEC_PERM) perm = EXEC_ACC; else perm = OPEN_ACC; return r->a == perm; } // Returns 0 if no match, 1 if a match, -1 on error __attribute__((hot)) static int check_subject(lnode *r, event_t *e) { unsigned int cnt = 0; sanity_check_node(r, "check_subject"); while (cnt < r->s_count) { unsigned int type = r->s[cnt].type; subject_attr_t *subj = NULL; // optimize get_subj_attr call if possible if (type == ALL_SUBJ) { cnt++; continue; } else { subj = get_subj_attr(e, type); } if (subj == NULL && type != PATTERN) { cnt++; continue; } switch(type) { // numbers -> multiple value case AUID: case SESSIONID: { if (!attr_set_check_int(r->s[cnt].set, (int64_t)subj->uval)) return 0; break; } case UID: /* * A process can present multiple UID values (real, * effective, saved, filesystem). Require the rule's * UID set to intersect the complete credential set the * subject cached so that any matching identity * authorizes the rule. */ if (!avl_intersection(&(r->s[cnt].set->tree), &(subj->set->tree))) return 0; break; case PID: case PPID: { if (!attr_set_check_int(r->s[cnt].set, (int64_t)subj->pid)) return 0; break; } // case // GID is unique in that process can have multiple and // rules can have multiple case GID: if (!avl_intersection(&(r->s[cnt].set->tree), &(subj->set->tree))) return 0; break; // single value exception case PATTERN: { int rc = subj_pattern_test(&(r->s[cnt]), e); if (rc == 0) return 0; // If there was an error, consider it // a match since deny is likely if (rc == -1) return 1; break; } // case // single value exception case SUBJ_TRUST: { if (subj->uval != r->s[cnt].uval) return 0; break; } // case // regular strings -> multiple value case EXE: { if (!subj->str) { break; } /* * "untrusted" is a macro-style match. If requested, and the * subject is not trusted, this attribute matches immediately. * * Otherwise, fall back to exact string match semantics so * explicit paths in the set continue to work. */ if (attr_set_check_str(r->s[cnt].set, "untrusted") && !is_subj_trusted(e)) break; if (!path_set_match(r->s[cnt].set, subj->str)) return 0; break; } // case case COMM: case EXE_TYPE: { if (!subj->str) break; if (!attr_set_check_str(r->s[cnt].set, subj->str)) return 0; break; } // case case EXE_DIR: { int macro_match = 0; if (!subj->str) { break; } if (attr_set_check_str(r->s[cnt].set, "execdirs")) if (check_dirs(1, subj->str)) macro_match = 1; if (attr_set_check_str(r->s[cnt].set, "systemdirs")) if (check_dirs(0, subj->str)) macro_match = 1; // DEPRECATED if (attr_set_check_str(r->s[cnt].set, "untrusted")) if (!is_subj_trusted(e)) macro_match = 1; /* * Macros are alternatives to literal directory prefixes. * If any macro matched, this attribute is satisfied. */ if (macro_match) break; // check partial match (via strncmp) // subdir test if (!attr_set_check_pstr(r->s[cnt].set, subj->str)) return 0; break; } // case default: return -1; } // switch cnt++; } return 1; } /* * legacy_untrusted_subject_path_blocked - preserve untrusted path exception * @r: rule being evaluated. * @idx: object attribute index currently being evaluated. * @e: event containing already-owned decision attributes. * * Historical exe=untrusted and dir=untrusted subject rules also filtered a * path object at the same rule-field index: trusted objects did not match the * legacy untrusted exception. The positional cross-reference is obsolete, but * remains here for compatibility until dir=untrusted support is removed. * * Returns: 1 when the legacy compatibility rule blocks this object match, * otherwise 0. */ static int legacy_untrusted_subject_path_blocked(lnode *r, unsigned int idx, event_t *e) { if (idx >= r->s_count) return 0; if (r->s[idx].type != EXE && r->s[idx].type != EXE_DIR) return 0; if (!attr_set_check_str(r->s[idx].set, "untrusted")) return 0; return is_obj_trusted(e); } // Returns 0 if no match, 1 if a match, -1 on error __attribute__((hot)) static int check_object(lnode *r, event_t *e) { unsigned int cnt = 0; sanity_check_node(r, "check_object"); while (cnt < r->o_count) { unsigned int type = r->o[cnt].type; object_attr_t *obj = NULL; // optimize get_obj_attr call if possible if (type == ALL_OBJ) { cnt++; continue; } else { obj = get_obj_attr(e, type); } if (obj == NULL) { cnt++; continue; } switch(type) { case OBJ_TRUST: { // obj->val holds (0|1) as int if (obj->val != r->o[cnt].val) return 0; break; } // case case FTYPE: { if (attr_set_check_str(r->o[cnt].set, "any")) break; } // fall through case DEVICE: case FILE_HASH: { if (!obj->o) { // Treat errors as denial for file hash lookups if (type == FILE_HASH) return 0; break; } if (!attr_set_check_str(r->o[cnt].set, obj->o)) return 0; break; } // case case PATH: { if (legacy_untrusted_subject_path_blocked(r, cnt, e)) return 0; if (!obj->o) break; // Globbing changes rule selection only, never object identity. if (!path_set_match(r->o[cnt].set, obj->o)) return 0; break; } // case case ODIR: { int macro_match = 0; if (!obj->o) { break; } if (attr_set_check_str(r->o[cnt].set, "execdirs")) if (check_dirs(1, obj->o)) macro_match = 1; if (attr_set_check_str(r->o[cnt].set, "systemdirs")) if (check_dirs(0, obj->o)) macro_match = 1; // DEPRECATED if (attr_set_check_str(r->o[cnt].set, "untrusted")) if (!is_obj_trusted(e)) macro_match = 1; /* * Keep macro keywords and literal directory prefixes as * ORed alternatives for dir matching. */ if (macro_match) break; // check partial match (via strncmp) // subdir test if (!attr_set_check_pstr(r->o[cnt].set, obj->o)) return 0; break; } // case // should not happen default: { return -1; } // case } // switch cnt++; } return 1; } __attribute__((hot)) decision_t rule_evaluate(lnode *r, event_t *e) { int d; // Check access permission d = check_access(r, e); if (d == 0) // No match return NO_OPINION; // Check the subject d = check_subject(r, e); if (d == 0) // No match return NO_OPINION; // Check the object d = check_object(r, e); if (d == 0) // No match return NO_OPINION; return r->d; } /* * rules_record_hit - count a rule that made the final policy decision. * @r: rule whose allow or deny decision ended evaluation. * * Rule hits are per active ruleset generation; publishing a new generation * replaces the rule nodes and starts their counters at zero. */ void rules_record_hit(lnode *r) { if (r) atomic_fetch_add_explicit(&r->hits, 1, memory_order_relaxed); } /* * rule_hits_snapshot - copy one rule hit counter and optionally reset it. * @r: rule whose hit counter should be copied. * @reset: non-zero resets the counter after copying. * Returns the copied hit count. */ static unsigned long rule_hits_snapshot(lnode *r, int reset) { if (reset) return atomic_exchange_explicit(&r->hits, 0, memory_order_relaxed); return atomic_load_explicit(&r->hits, memory_order_relaxed); } /* * rules_hits_report_reset - write per-rule hit counters in rule order. * @f: output stream. * @l: active rule list to report. * @reset: non-zero resets counters after copying them. * * Returns nothing. */ void rules_hits_report_reset(FILE *f, const llist *l, int reset) { lnode *r; unsigned long max_hits = 0; int hits_width; if (f == NULL || l == NULL) return; for (r = rules_first_node(l); r; r = rules_next_node(r)) { unsigned long hits = atomic_load_explicit(&r->hits, memory_order_relaxed); if (hits > max_hits) max_hits = hits; } if (max_hits < 1000000UL) hits_width = 6; else if (max_hits <= UINT32_MAX) hits_width = 10; else hits_width = 20; for (r = rules_first_node(l); r; r = rules_next_node(r)) fprintf(f, "Hits/rule: %3u %*lu %s\n", r->num + 1, hits_width, rule_hits_snapshot(r, reset), r->text ? r->text : ""); } /* * rules_hits_report - write per-rule hit counters in rule order. * @f: output stream. * @l: active rule list to report. * * Returns nothing. */ void rules_hits_report(FILE *f, const llist *l) { rules_hits_report_reset(f, l, 0); } void rules_unsupport_audit(const llist *l) { #ifdef USE_AUDIT register lnode *current = l->head; int warn = 0; while (current) { if (current->d & AUDIT) warn = 1; current->d &= ~AUDIT; current=current->next; } if (warn) { msg(LOG_WARNING, "Rules with audit events are not supported by the kernel"); msg(LOG_NOTICE, "Converting rules to non-audit rules"); } #endif } void rules_clear(llist *l) { lnode *nextnode; register lnode *current = l->head; while (current) { nextnode=current->next; free(current->text); free(current); current=nextnode; } l->head = NULL; l->cur = NULL; l->cnt = 0; attr_sets_destroy(l->sets); l->sets = NULL; l->proc_status_mask = 0; } /* * rules_get_proc_status_mask - Report /proc status fields needed by rules. * * Return: bitmap of PROC_STAT_* values observed while parsing the current * rule set. The mask guides process attribute collection so we only read * /proc//status once for all requested fields. */ unsigned int rules_get_proc_status_mask(const llist *l) { if (!l) return 0; return l->proc_status_mask; } fapolicyd-2.0/src/library/rules.h000066400000000000000000000061501524071644000170700ustar00rootroot00000000000000/* * rules.h - Header file for rules.c * Copyright (c) 2016-17,2019 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef RULES_HEADER #define RULES_HEADER #include #include #include "policy.h" #include "subject-attr.h" #include "object-attr.h" #include "event.h" #include "gcc-attributes.h" #define MAX_FIELDS 11 // Subject side can have up to 11 attributes. // Object side can have up to 6. 11 covers both. /* This is one node of the linked list. Any data elements that are per * rule goes here. */ typedef struct _lnode{ decision_t d; access_t a; unsigned int num; unsigned int lineno; atomic_ulong hits; char *text; rformat_t format; unsigned int s_count; unsigned int o_count; subject_attr_t s[MAX_FIELDS]; object_attr_t o[MAX_FIELDS]; struct _lnode *next; // Next node pointer } lnode; /* This is the linked list head. Only data elements that are 1 per * event goes here. */ typedef struct { lnode *head; // List head lnode *cur; // Mutable build/compat traversal state unsigned int cnt; // How many items in this list attr_sets_t *sets; // Registry that owns rule attribute sets unsigned int proc_status_mask; // /proc status fields needed by rules } llist; int rules_create(llist *l); /* * rules_first/rules_next/rules_get_cur use llist.cur and are retained for * mutable construction-time traversal. Decision reads use local node cursors. */ void rules_first(llist *l); lnode *rules_next(llist *l); static inline lnode *rules_get_cur(const llist *l) { return l->cur; } /* rules_first_node - get first rule for a local read cursor. * @l: rule list to read. * Return: first rule node, or NULL if the list is empty. */ static inline lnode *rules_first_node(const llist *l) { return l->head; } /* rules_next_node - advance a local read cursor. * @n: current rule node, or NULL. * Return: next rule node, or NULL at the end of the list. */ static inline lnode *rules_next_node(const lnode *n) { return n ? n->next : NULL; } int rules_append(llist *l, char *buf, unsigned int lineno) __wur; __attribute__((hot)) decision_t rule_evaluate(lnode *r, event_t *e); void rules_record_hit(lnode *r); void rules_hits_report(FILE *f, const llist *l); void rules_hits_report_reset(FILE *f, const llist *l, int reset); void rules_unsupport_audit(const llist *l); void rules_clear(llist* l); unsigned int rules_get_proc_status_mask(const llist *l); #endif fapolicyd-2.0/src/library/stack.c000066400000000000000000000040421524071644000170340ustar00rootroot00000000000000/* * stack.c - generic stack impementation * Copyright (c) 2023 Red Hat Inc., Durham, North Carolina. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka */ #include "stack.h" #include // init of the stack struct void stack_init(stack_t *_stack) { if (_stack == NULL) return; list_init(_stack); } // free all the resources from the stack void stack_destroy(stack_t *_stack) { if (_stack == NULL) return; list_empty(_stack); } // push to the top of the stack void stack_push(stack_t *_stack, const void *_data) { if (_stack == NULL) return; list_prepend(_stack, NULL, (const char *)_data); } // pop the the top without returning what was on the top void stack_pop(stack_t *_stack) { if (_stack == NULL || _stack->first == NULL) return; list_item_t *first = _stack->first; _stack->first = first->next; if (_stack->first == NULL) _stack->last = NULL; first->data = NULL; list_destroy_item(&first); if (_stack->count > 0) _stack->count--; return; } // function returns 1 if stack is emtpy 0 if it's not int stack_is_empty(const stack_t *_stack) { if (_stack == NULL) return -1; if (_stack->count == 0) return 1; return 0; } // return top of the stack without popping const void *stack_top(const stack_t *_stack) { if (_stack == NULL) return NULL; return _stack->first ? _stack->first->data : NULL; } fapolicyd-2.0/src/library/stack.h000066400000000000000000000023311524071644000170400ustar00rootroot00000000000000/* * stack.h - header for generic stack implementation * Copyright (c) 2023 Red Hat Inc., Durham, North Carolina. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka */ #ifndef STACK_H_ #define STACK_H_ #include "llist.h" typedef list_t stack_t; void stack_init(stack_t *_stack); void stack_destroy(stack_t *_stack); void stack_push(stack_t *_stack, const void *_data); void stack_pop(stack_t *_stack); int stack_is_empty(const stack_t *_stack); const void *stack_top(const stack_t *_stack); #endif // STACK_H_ fapolicyd-2.0/src/library/string-util.c000066400000000000000000000052261524071644000202150ustar00rootroot00000000000000/* * string-util.c - useful string functions * Copyright (c) 2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka * Zoltan Fridrich */ #include #include #include #include #include "string-util.h" #pragma GCC optimize("O3") char *fapolicyd_strtrim(char *s) { if (!s) return NULL; // skip leading spaces char *start = s; while (*start && isspace((unsigned char)*start)) start++; // shift left (no-op if start == s) size_t len = strlen(start); memmove(s, start, len + 1); // includes the '\0' // all spaces? if (*s == '\0') return s; // trim trailing char *end = s + len - 1; while (end >= s && isspace((unsigned char)*end)) *end-- = '\0'; return s; } char *fapolicyd_strcat(const char *s1, const char *s2) { size_t s1_len = strlen(s1); size_t s2_len = strlen(s2); char *r = malloc(s1_len + s2_len + 1); if (r == NULL) return NULL; memcpy(r, s1, s1_len); memcpy(r + s1_len, s2, s2_len + 1); // includes null terminator return r; } char *fapolicyd_strnchr(const char *s, int c, size_t len) { unsigned char uc = (unsigned char)c; for (; len--; ++s) { if ((unsigned char)*s == uc) return (char *)s; if (*s == '\0') break; } return NULL; } /* * fapolicyd_format_ns - format nanoseconds for compact reports. * @ns: duration or age in nanoseconds. * @buf: destination buffer. * @buf_size: destination size. * * Returns nothing. */ void fapolicyd_format_ns(uint64_t ns, char *buf, size_t buf_size) { if (buf == NULL || buf_size == 0) return; if (ns == 0) snprintf(buf, buf_size, "0 ns"); else if (ns < 1000ULL) snprintf(buf, buf_size, "%llu ns", (unsigned long long)ns); else if (ns < 1000000ULL) snprintf(buf, buf_size, "%.3fus", (double)ns / 1000.0); else if (ns < 1000000000ULL) snprintf(buf, buf_size, "%.3fms", (double)ns / 1000000.0); else snprintf(buf, buf_size, "%.3fs", (double)ns / 1000000000.0); } fapolicyd-2.0/src/library/string-util.h000066400000000000000000000030421524071644000202140ustar00rootroot00000000000000/* * string-util.h - Header file for string-util * Copyright (c) 2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka * Zoltan Fridrich */ #ifndef STRING_UTIL_H #define STRING_UTIL_H #include #include #include "gcc-attributes.h" char *fapolicyd_strtrim(char *s); /** * Concatenates two NULL terminated strings * * @param s1 First NULL terminated string * @param s2 Second NULL terminated string * @return Dynamically allocated NULL terminated string s1||s2 */ char *fapolicyd_strcat(const char *s1, const char *s2) __attr_dealloc_free; char *fapolicyd_strnchr(const char *s, int c, size_t len) __attr_access ((__read_only__, 1, 3)); void fapolicyd_format_ns(uint64_t ns, char *buf, size_t buf_size) __attr_access ((__write_only__, 2, 3)); #endif fapolicyd-2.0/src/library/subject-attr.c000066400000000000000000000051621524071644000203420ustar00rootroot00000000000000/* * subject-attr.c - functions to abstract subject attributes * Copyright (c) 2016,2019,2022 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * Radovan Sroka */ #include "config.h" #include // For NULL #include #include "subject-attr.h" /* * Table1 is used for looking up rule fields written in the old format. * Do not add anything new here. */ static const nv_t table1[] = { { ALL_SUBJ, "all" }, { AUID, "auid" }, { UID, "uid" }, { SESSIONID, "sessionid" }, { PID, "pid" }, { PATTERN, "pattern" }, { COMM, "comm" }, { EXE, "exe" }, { EXE_DIR, "exe_dir" }, { EXE_TYPE, "exe_type" }, }; #define MAX_SUBJECTS1 (sizeof(table1)/sizeof(table1[0])) /* * Table2 is used for looking up rule fields written in the new format */ static const nv_t table2[] = { { ALL_SUBJ, "all" }, { AUID, "auid" }, { UID, "uid" }, { SESSIONID, "sessionid" }, { PID, "pid" }, { PPID, "ppid" }, { PATTERN, "pattern" }, { SUBJ_TRUST, "trust" }, { GID, "gid" }, { COMM, "comm" }, { EXE, "exe" }, { EXE_DIR, "dir" }, { EXE_TYPE, "ftype" }, }; #define MAX_SUBJECTS2 (sizeof(table2)/sizeof(table2[0])) int subj_name_to_val(const char *name, rformat_t format) { unsigned int i = 0; if (format == RULE_FMT_ORIG) { while (i < MAX_SUBJECTS1) { if (strcmp(name, table1[i].name) == 0) return table1[i].value; i++; } } else { while (i < MAX_SUBJECTS2) { if (strcmp(name, table2[i].name) == 0) return table2[i].value; i++; } } return -1; } const char *subj_val_to_name(unsigned int v, rformat_t format) { if (v > SUBJ_END) return NULL; unsigned int index = v - SUBJ_START; if (format == RULE_FMT_ORIG) { if (index < MAX_SUBJECTS1) return table1[index].name; } else { if (index < MAX_SUBJECTS2) return table2[index].name; } return NULL; } fapolicyd-2.0/src/library/subject-attr.h000066400000000000000000000030511524071644000203420ustar00rootroot00000000000000/* * subject-attr.h - Header file for subject-attr.c * Copyright (c) 2016,2019-20,2022 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef SUBJECT_ATTR_HEADER #define SUBJECT_ATTR_HEADER #include #include "nv.h" #include "fapolicyd-defs.h" #include "attr-sets.h" // Top is numbers, bottom is strings typedef enum { ALL_SUBJ = SUBJ_START, AUID, UID, SESSIONID, PID, PPID, PATTERN, SUBJ_TRUST, GID, COMM, EXE, EXE_DIR, EXE_TYPE} subject_type_t; #define SUBJ_END EXE_TYPE #define SUBJ_COUNT (SUBJ_END - SUBJ_START + 1) typedef struct s { subject_type_t type; union { unsigned int uval; pid_t pid; char *str; attr_sets_entry_t * set; }; } subject_attr_t; int subj_name_to_val(const char *name, rformat_t format); const char * subj_val_to_name(unsigned v, rformat_t format); #endif fapolicyd-2.0/src/library/subject.c000066400000000000000000000120701524071644000173660ustar00rootroot00000000000000/* * subject.c - Minimal linked list set of subject attributes * Copyright (c) 2016 Red Hat Inc., Durham, North Carolina. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #include "config.h" #include #include #include #include "policy.h" #include "subject.h" #include "message.h" //#define DEBUG int subject_create(s_array *a) { if (a == NULL) return 1; a->cnt = 0; a->info = NULL; a->subj = calloc(SUBJ_COUNT, sizeof(subject_attr_t *)); if (a->subj == NULL) return 1; return 0; } #ifdef DEBUG static void sanity_check_array(const s_array *a, const char *id) { int i; unsigned int num = 0; if (a == NULL) { msg(LOG_DEBUG, "%s - array is NULL", id); abort(); } for (i = 0; i < SUBJ_COUNT; i++) if (a->subj[i]) num++; if (num != a->cnt) { msg(LOG_DEBUG, "%s - array corruption %u!=%u", id, num, a->cnt); abort(); } } #else #define sanity_check_array(a, b) do {} while(0) #endif subject_attr_t *subject_access(const s_array *a, subject_type_t t) { if (a == NULL || a->subj == NULL) return NULL; sanity_check_array(a, "subject_access"); // These store the same info, see get_subj_attr in event.c if (t == EXE_DIR) t = EXE; if (t >= SUBJ_START && t <= SUBJ_END) return a->subj[t - SUBJ_START]; else return NULL; } // Returns 1 on failure and 0 on success int subject_add(s_array *a, const subject_attr_t *subj) { subject_attr_t* newnode; subject_attr_t **slot; subject_type_t t; if (a == NULL || a->subj == NULL) return 1; sanity_check_array(a, "subject_add 1"); if (subj) { t = subj->type; // These store the same info, see get_subj_attr in event.c if (t == EXE_DIR) t = EXE; if (t >= SUBJ_START && t <= SUBJ_END) { newnode = malloc(sizeof(subject_attr_t)); if (newnode == NULL) return 1; newnode->type = t; if (subj->type >= COMM) newnode->str = subj->str; else if (subj->type == GID || subj->type == UID) /* * Attribute sets are reference-count-less blobs. The * caller allocates a fresh set for us to adopt so we * simply transfer ownership to the cached node. */ newnode->set = subj->set; else if (subj->type == PID || subj->type == PPID) newnode->pid = subj->pid; else newnode->uval = subj->uval; } else return 1; } else return 1; slot = &a->subj[t - SUBJ_START]; if (*slot) { subject_attr_t *old = *slot; if (old->type == GID || old->type == UID) attr_set_destroy(old->set); else if (old->type >= COMM) free(old->str); free(old); } else a->cnt++; *slot = newnode; sanity_check_array(a, "subject_add 2"); return 0; } subject_attr_t *subject_find_exe(const s_array *a) { if (a == NULL || a->subj == NULL) return NULL; sanity_check_array(a, "subject_find_exe"); if (a->subj[EXE - SUBJ_START]) return a->subj[EXE - SUBJ_START]; return NULL; } subject_attr_t *subject_find_comm(const s_array *a) { if (a == NULL || a->subj == NULL) return NULL; sanity_check_array(a, "subject_find_comm"); if (a->subj[COMM - SUBJ_START]) return a->subj[COMM - SUBJ_START]; return NULL; } void subject_clear(s_array* a) { int i; subject_attr_t *current; if (a == NULL) return; if (a->subj) { sanity_check_array(a, "subject_clear"); for (i = 0; i < SUBJ_COUNT; i++) { current = a->subj[i]; if (current == NULL) continue; if (current->type == GID || current->type == UID) { /* * GID/UID attributes own dynamically allocated * sets; attr_set_destroy() releases contents and * container. */ attr_set_destroy(current->set); } else if (current->type >= COMM) free(current->str); free(current); a->subj[i] = NULL; } free(a->subj); a->subj = NULL; } clear_proc_info(a->info); free(a->info); a->info = NULL; a->cnt = 0; } void subject_reset(s_array *a, subject_type_t t) { if (a == NULL || a->subj == NULL) return; sanity_check_array(a, "subject_reset1"); if (t >= SUBJ_START && t <= SUBJ_END) { subject_attr_t *current = a->subj[t - SUBJ_START]; if (current == NULL) return; if (current->type == GID || current->type == UID) { /* * GID/UID attributes own dynamically allocated * sets; attr_set_destroy() releases contents and * container. */ attr_set_destroy(current->set); } else if (current->type >= COMM) free(current->str); free(current); a->subj[t - SUBJ_START] = NULL; a->cnt--; sanity_check_array(a, "subject_reset2"); } } fapolicyd-2.0/src/library/subject.h000066400000000000000000000032221524071644000173720ustar00rootroot00000000000000/* * subject.h - Header file for subject.c * Copyright (c) 2016 Red Hat Inc., Durham, North Carolina. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef SUBJECT_HEADER #define SUBJECT_HEADER #include "subject-attr.h" #include "process.h" /* This is the attribute array. Only data elements that are 1 per * event goes here. */ typedef struct { subject_attr_t **subj; // Subject array unsigned int cnt; // How many items in this list struct proc_info *info; // unique proc fingerprint } s_array; int subject_create(s_array *a); subject_attr_t *subject_access(const s_array *a, subject_type_t t); int subject_add(s_array *a, const subject_attr_t *subj); subject_attr_t *subject_find_exe(const s_array *a); subject_attr_t *subject_find_comm(const s_array *a); void subject_reset(s_array *a, subject_type_t t); void subject_clear(s_array* a); static inline int type_is_subj(int type) {if (type < OBJ_START) return 1; else return 0;} #endif fapolicyd-2.0/src/library/trust-file.c000066400000000000000000000474601524071644000200400ustar00rootroot00000000000000/* * trust-file.c - Functions for working with trust files * Copyright (c) 2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Zoltan Fridrich * Radovan Sroka */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "fapolicyd-backend.h" #include "file.h" #include "llist.h" #include "message.h" #include "trust-file.h" #include "escape.h" #include "paths.h" #include "filter.h" /* * fapolicyd-cli relies on this file to materialize trust entries into * linked lists so they can be inspected, deduplicated, and rewritten. * The daemon also calls into the same helpers when it needs an in-memory * snapshot instead of streaming updates through a memfd. The routines * below therefore serve both the CLI's trust management commands and the * daemon's backend, with the CLI-only helpers called out explicitly in * their documentation. */ /* Path, separators, fixed-width trust DB size, digest, and NUL terminator. */ #define BUFFER_SIZE (PATH_MAX + 1 + 1 + TRUSTDB_SIZE_DECIMAL_DIGITS + \ 1 + FILE_DIGEST_STRING_WIDTH + 1) #define FTW_NOPENFD 1024 #define FTW_FLAGS (FTW_ACTIONRETVAL | FTW_PHYS) #define HEADER_OLD "# AUTOGENERATED FILE VERSION 2\n" #define HEADER0 "# AUTOGENERATED FILE VERSION 3\n" #define HEADER1 "# This file contains a list of trusted files\n" #define HEADER2 "#\n" #define HEADER3 "# FULL PATH SIZE SHA256\n" #define HEADER4 "# /home/user/my-ls 157984 61a9960bf7d255a85811f4afcac51067b8f2e4c75e21cf4f2af95319d4ed1b87\n" static list_t _list; static char *_path; static int _count; static bool _use_filter; static int _memfd = -1; struct trust_seen_entry { const char *path; UT_hash_handle hh; }; /* * make_data_string - Create a trust-file payload for a path. * @path: Absolute path that should be represented in the trust file. * * The resulting buffer contains the "source size hash" triplet used when * rewriting trust fragments. The caller takes ownership of the allocated * string and must free it. Returns NULL if the file cannot be measured. */ static char *make_data_string(const char *path) { int fd = open(path, O_RDONLY); trustdb_size_t size; if (fd < 0) { msg(LOG_ERR, "Cannot open %s", path); return NULL; } // Get the size struct stat sb; if (fstat(fd, &sb)) { msg(LOG_ERR, "Cannot stat %s", path); close(fd); return NULL; } if (trustdb_size_from_signed((intmax_t)sb.st_size, &size)) { msg(LOG_ERR, "Cannot store invalid file size for %s", path); close(fd); return NULL; } /* * Non-RPM (file/DEB) trust fragments have always carried SHA256 digests * only. Keep generating that format even though loading now understands * multiple algorithms for RPM-provided fragments. */ char *hash = get_hash_from_fd2(fd, sb.st_size, FILE_HASH_ALG_SHA256); close(fd); if (!hash) { msg(LOG_ERR, "Cannot hash %s", path); return NULL; } char *line; /* * formated data to be saved * source size sha256 * path is stored as lmdb index */ int count = asprintf(&line, DATA_FORMAT, 0, size, hash); free(hash); if (count < 0) { msg(LOG_ERR, "Cannot format entry for %s", path); return NULL; } return line; } /* * write_out_list - Persist a linked list of trust entries to disk. * @list: List of entries created by trust_file_load or CLI helpers. * @dest: Destination trust file to be rewritten. * * This helper is used exclusively by the CLI trust management commands * after they finish editing an in-memory list. Returns 0 on success and * 1 when the destination file could not be opened. */ static int write_out_list(list_t *list, const char *dest) { FILE *f = fopen(dest, "w"); if (!f) { msg(LOG_ERR, "Cannot delete %s", dest); list_empty(list); return 1; } size_t hlen; hlen = strlen(HEADER0); fwrite(HEADER0, hlen, 1, f); hlen = strlen(HEADER1); fwrite(HEADER1, hlen, 1, f); hlen = strlen(HEADER2); fwrite(HEADER2, hlen, 1, f); hlen = strlen(HEADER3); fwrite(HEADER3, hlen, 1, f); hlen = strlen(HEADER4); fwrite(HEADER4, hlen, 1, f); for (list_item_t *lptr = list->first; lptr; lptr = lptr->next) { const char *data = (char *)(lptr->data); const char *path = (char *)lptr->index; // + 2 because we are omitting source number // "0 12345 ..." // 0 -> filedb source // Skip writing out the source (data + 2) if (fprintf(f, "%s %s\n", path, data + 2) < 0) msg(LOG_ERR, "Cannot write entry for %s", path); } fclose(f); return 0; } /* * trust_file_append - Add entries to a trust file for the CLI. * @fpath: Path to the trust fragment that should be extended. * @list: List of paths prepared by the CLI for insertion. * * The CLI populates @list with path indexes and this helper computes the * hash/size payloads before merging the new entries into @fpath. Returns * 0 when the update succeeds and 1 if the existing file could not be * parsed. */ int trust_file_append(const char *fpath, list_t *list) { list_t content; list_init(&content); int rc = trust_file_load(fpath, &content, -1); // if trust file does not exist, we ignore it as it will be created while writing if (rc > 1) { // exit on load errors, we dont want invalid entries to be autoremoved return 1; } for (list_item_t *lptr = list->first; lptr; lptr = lptr->next) { lptr->data = make_data_string(lptr->index); } list_merge(&content, list); write_out_list(&content, fpath); list_empty(&content); return 0; } #define DELIM ' ' #define MAX_DELIMS 2 // Trustdb has 3 fields - therefore 2 delimiters /* * parse_line_backwards - Split a trust-file line into its components. * @line: Buffer containing the raw line (modified in place). * @path: Output buffer for the stored path. * @size: Output parameter for the recorded size. * @sha: Output buffer for the digest string. * * Trust-file paths may contain spaces, so we cannot parse from the left * with scanf("%s ..."). Instead, scan from the right and split on the * last two spaces, which are the delimiters before size and digest. * * Returns 0 when parsing succeeds or -1 when the line is malformed. */ static int parse_line_backwards(char *line, char *path, trustdb_size_t *size, char *sha, size_t sha_size) { if (line == NULL || path == NULL || size == NULL || sha == NULL || *line == 0) return -1; size_t len = strlen(line); int count = 0; char *delims[MAX_DELIMS] = { 0 }; int stripped = 0; for (int i = len - 1; i >= 0; i--) { if (!stripped) { if (isspace((unsigned char)line[i])) line[i] = '\0'; else stripped = 1; } if (count == MAX_DELIMS) break; if (line[i] == DELIM) delims[count++] = &line[i]; } if (count != MAX_DELIMS) return -1; for (int i = 0; i < count; i++) *(delims[i]) = '\0'; // Save SHA from the last delimiter to the end of the line. size_t sha_width = &line[len-1] - (delims[0] + 1); if (sha_width >= sha_size || sha_width != SHA256_LEN*2) { msg(LOG_INFO, "sha_width %lu", sha_width); return -1; } memcpy(sha, delims[0] + 1, sha_width); sha[sha_width] = '\0'; // save size to arg char *endptr; errno = 0; if (*(delims[1] + 1) == '-') return -1; unsigned long long parsed_size = strtoull(delims[1] + 1, &endptr, 10); if (errno || endptr == delims[1] + 1 || *endptr != '\0' || (uintmax_t)parsed_size > UINT64_MAX) return -1; *size = (trustdb_size_t)parsed_size; // save path to arg size_t path_size = delims[1] - line; if (path_size > PATH_MAX) return -1; memcpy(path, line, path_size); path[path_size] = '\0'; return 0; } /* * trust_file_load - Load a trust fragment into a list or memfd. * @fpath: Full path to the trust fragment. * @list: Destination list when @memfd is negative. * @memfd: File descriptor used for streaming output, or -1 for lists. * * This helper is shared by the daemon and CLI. It returns 0 on success, * 1 when the file cannot be opened, 2 on parse errors, and 3 when memory * could not be allocated while tracking duplicates. */ int trust_file_load(const char *fpath, list_t *list, int memfd) { char buffer[BUFFER_SIZE]; int escaped = 0; long line = 0; int rc = 0; struct trust_seen_entry *seen = NULL; FILE *file = fopen(fpath, "r"); if (!file) return 1; while (fgets(buffer, BUFFER_SIZE, file)) { char name[PATH_MAX+1], sha[FILE_DIGEST_STRING_MAX], *index = NULL, *data = NULL; char data_buf[BUFFER_SIZE]; trustdb_size_t sz; unsigned int tsource = SRC_FILE_DB; line++; if (iscntrl((unsigned char)buffer[0]) || buffer[0] == '#') { if (line == 1 && strncmp(buffer, HEADER_OLD, strlen(HEADER_OLD)) == 0) escaped = 1; continue; } if (parse_line_backwards(buffer, name, &sz, sha, sizeof(sha))) { msg(LOG_WARNING, "Can't parse %s", buffer); rc = 2; goto out; } /* * Infer the algorithm from the digest width instead of trusting * the source. The helpers in file.c keep the mapping between * printable hex length and binary digest sizes in sync with * upstream algorithm support. */ size_t digest_len = strlen(sha); file_hash_alg_t alg = file_hash_alg(digest_len); size_t expected_len = file_hash_length(alg) * 2; if (expected_len == 0 || digest_len != expected_len) { msg(LOG_WARNING, "Cannot infer digest algorithm for %s", name); rc = 2; goto out; } /* * Non-RPM trust fragments historically persisted SHA256 * digests only. RPM database ingestion is the only path * that mirrors multiple upstream algorithms, so seeing * anything but SHA256 here likely means the on-disk format * has changed unexpectedly. */ if (alg != FILE_HASH_ALG_SHA256) { msg(LOG_WARNING,"Unsupported digest algorithm %s in %s", file_hash_alg_name(alg), fpath); rc = 2; goto out; } int len = snprintf(data_buf, sizeof(data_buf), DATA_FORMAT, tsource, sz, sha); if (len < 0 || len >= (int)sizeof(data_buf)) { msg(LOG_ERR, "Entry too large in %s", fpath); continue; } /* If the legacy format was used, unescape the stored path. */ index = escaped ? unescape(name) : strdup(name); if (index == NULL) { msg(LOG_ERR, "Could not unescape %s from %s", name, fpath); continue; } struct trust_seen_entry *entry; HASH_FIND_STR(seen, index, entry); if (entry) { msg(LOG_WARNING, "%s contains a duplicate %s", fpath, index); free(index); continue; } entry = malloc(sizeof(*entry)); if (!entry) { msg(LOG_ERR, "Out of memory tracking %s", index); free(index); rc = 3; goto out; } entry->path = index; if (memfd >= 0) { HASH_ADD_KEYPTR(hh, seen, entry->path, strlen(entry->path), entry); if (dprintf(memfd, "%s %s\n", index, data_buf) < 0) msg(LOG_ERR, "dprintf failed writing %s to memfd (%s)", index, strerror(errno)); } else { data = strdup(data_buf); if (data == NULL) { free(index); free(entry); continue; } if (list_append(list, index, data)) { free(index); free(data); free(entry); } else // Add it after successfully stored on the list HASH_ADD_KEYPTR(hh, seen, entry->path, strlen(entry->path), entry); } } out: fclose(file); struct trust_seen_entry *item; while (seen) { item = seen; HASH_DEL(seen, item); if (memfd >= 0) free((char *)item->path); free(item); } return rc; } /* * trust_file_delete_path - Remove matching entries from a trust file. * @fpath: Path to the trust fragment being edited. * @path: Prefix that identifies entries scheduled for removal. * * Used only by the CLI trust management commands. Returns the number of * entries deleted, 0 when the file could not be opened, * and -1 on parse errors. */ int trust_file_delete_path(const char *fpath, const char *path) { list_t list; list_init(&list); int rc = trust_file_load(fpath, &list, -1); switch (rc) { case 1: msg(LOG_ERR, "Cannot open %s", fpath); return 0; case 2: list_empty(&list); return -1; default: break; } int count = 0; size_t path_len = strlen(path); list_item_t *lptr = list.first, *prev = NULL, *tmp; while (lptr) { if (!strncmp(lptr->index, path, path_len)) { ++count; tmp = lptr->next; if (prev) prev->next = lptr->next; else list.first = lptr->next; if (!lptr->next) list.last = prev; --list.count; list_destroy_item(&lptr); lptr = tmp; continue; } prev = lptr; lptr = lptr->next; } if (count) write_out_list(&list, fpath); list_empty(&list); return count; } /* * trust_file_update_path - Refresh hashes for matching entries. * @fpath: Trust fragment that should be rewritten. * @path: Prefix designating entries that must be re-measured. * * Used only by the CLI trust management commands. Returns the number of * entries updated, 0 when the file could not be opened, * and -1 when the existing file cannot be parsed. */ int trust_file_update_path(const char *fpath, const char *path, bool use_filter) { list_t list; list_init(&list); int rc = trust_file_load(fpath, &list, -1); switch (rc) { case 1: msg(LOG_ERR, "Cannot open %s", fpath); return 0; case 2: list_empty(&list); return -1; default: break; } int count = 0; size_t path_len = strlen(path); for (list_item_t *lptr = list.first; lptr; lptr = lptr->next) { if (!strncmp(lptr->index, path, path_len)) { if (use_filter) { filter_rc_t f_res = filter_check(lptr->index); if (f_res != FILTER_ALLOW) { if (f_res == FILTER_ERR_DEPTH) msg(LOG_WARNING, "filter nesting exceeds MAX_FILTER_DEPTH for %s; excluding", (char *)lptr->index); continue; } } char *new_data = make_data_string(lptr->index); if (!new_data) { msg(LOG_WARNING, "Cannot refresh %s, keeping previous trust entry", (char *)lptr->index); continue; } free((char *)lptr->data); lptr->data = new_data; ++count; } } if (count) write_out_list(&list, fpath); list_empty(&list); return count; } /* * trust_file_rm_duplicates - Prune CLI additions already present on disk. * @fpath: Trust fragment checked for duplicates. * @list: Pending CLI additions to compare against existing entries. * * Used only by the CLI trust management commands before appending new * entries. Returns 0 after pruning, * or -1 when the trust fragment could not be opened or parsed. */ int trust_file_rm_duplicates(const char *fpath, list_t *list) { list_t trust_file; list_init(&trust_file); int rc = trust_file_load(fpath, &trust_file, -1); switch (rc) { case 1: msg(LOG_ERR, "Cannot open %s", fpath); return -1; case 2: list_empty(&trust_file); return -1; default: break; } for (list_item_t *lptr = trust_file.first; lptr; lptr = lptr->next) { list_remove(list, lptr->index); if (list->count == 0) break; } list_empty(&trust_file); return 0; } /* * ftw_load - nftw callback that aggregates trust fragments. * @fpath: Current file discovered by nftw. * @sb: (unused) file metadata supplied by nftw. * @typeflag: nftw entry type. * @ftwbuf: (unused) traversal context from nftw. */ static int ftw_load(const char *fpath, const struct stat *sb __attribute__ ((unused)), int typeflag, struct FTW *ftwbuf __attribute__ ((unused))) { if (typeflag == FTW_F) trust_file_load(fpath, &_list, _memfd); return FTW_CONTINUE; } /* * ftw_delete_path - nftw callback that deletes matching entries. * @fpath: Current trust fragment examined by nftw. * @sb: (unused) file metadata supplied by nftw. * @typeflag: nftw entry type. * @ftwbuf: (unused) traversal context from nftw. */ static int ftw_delete_path(const char *fpath, const struct stat *sb __attribute__ ((unused)), int typeflag, struct FTW *ftwbuf __attribute__ ((unused))) { if (typeflag == FTW_F) _count += trust_file_delete_path(fpath, _path); return FTW_CONTINUE; } /* * ftw_update_path - nftw callback that updates matching entries. * @fpath: Current trust fragment examined by nftw. * @sb: (unused) file metadata supplied by nftw. * @typeflag: nftw entry type. * @ftwbuf: (unused) traversal context from nftw. */ static int ftw_update_path(const char *fpath, const struct stat *sb __attribute__ ((unused)), int typeflag, struct FTW *ftwbuf __attribute__ ((unused))) { if (typeflag == FTW_F) _count += trust_file_update_path(fpath, _path, _use_filter); return FTW_CONTINUE; } /* * ftw_rm_duplicates - nftw callback removing duplicates from CLI lists. * @fpath: Current trust fragment examined by nftw. * @sb: (unused) file metadata supplied by nftw. * @typeflag: nftw entry type. * @ftwbuf: (unused) traversal context from nftw. */ static int ftw_rm_duplicates(const char *fpath, const struct stat *sb __attribute__ ((unused)), int typeflag, struct FTW *ftwbuf __attribute__ ((unused))) { if (_list.count == 0) return FTW_STOP; if (typeflag == FTW_F) trust_file_rm_duplicates(fpath, &_list); return FTW_CONTINUE; } /* * trust_file_load_all - Aggregate every trust fragment. * @list: Destination list when @memfd is negative. * @memfd: File descriptor that receives streamed entries, or -1. * * Used by both the daemon and CLI to populate either an in-memory list or * a memfd-backed snapshot covering the primary trust file plus the tree * of per-package fragments. */ void trust_file_load_all(list_t *list, int memfd) { list_empty(&_list); _memfd = memfd; /* Populate either the in-memory list or the memfd snapshot. */ trust_file_load(TRUST_FILE_PATH, &_list, memfd); nftw(TRUST_DIR_PATH, &ftw_load, FTW_NOPENFD, FTW_FLAGS); if (memfd < 0) { if (list) list_merge(list, &_list); } else list_empty(&_list); _memfd = -1; } /* * trust_file_delete_path_all - Delete matching entries across all files. * @path: Prefix designating entries to remove. * * Used only by the CLI trust management commands to remove a path from * every trust fragment. Returns the number of entries deleted. */ int trust_file_delete_path_all(const char *path) { _path = strdup(path); _count = trust_file_delete_path(TRUST_FILE_PATH, path); nftw(TRUST_DIR_PATH, &ftw_delete_path, FTW_NOPENFD, FTW_FLAGS); free(_path); return _count; } /* * trust_file_update_path_all - Refresh hashes across every trust file. * @path: Prefix designating entries that must be re-measured. * * Used only by the CLI trust management commands. Returns the number of * entries updated. */ int trust_file_update_path_all(const char *path, bool use_filter) { _path = strdup(path); _use_filter = use_filter; _count = trust_file_update_path(TRUST_FILE_PATH, path, _use_filter); nftw(TRUST_DIR_PATH, &ftw_update_path, FTW_NOPENFD, FTW_FLAGS); free(_path); _use_filter = false; return _count; } /* * trust_file_rm_duplicates_all - Remove duplicates across trust files. * @list: Pending CLI additions to prune before appending. * * Used only by the CLI trust management commands prior to calling * trust_file_append(). */ void trust_file_rm_duplicates_all(list_t *list) { list_empty(&_list); list_merge(&_list, list); trust_file_rm_duplicates(TRUST_FILE_PATH, &_list); nftw(TRUST_DIR_PATH, &ftw_rm_duplicates, FTW_NOPENFD, FTW_FLAGS); list_merge(list, &_list); } fapolicyd-2.0/src/library/trust-file.h000066400000000000000000000031501524071644000200310ustar00rootroot00000000000000/* * trust-file.h - Header for managing trust files * Copyright (c) 2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Zoltan Fridrich */ #ifndef TRUST_FILE_H #define TRUST_FILE_H #include #include "llist.h" #define TRUST_FILE_PATH "/etc/fapolicyd/fapolicyd.trust" #define TRUST_DIR_PATH "/etc/fapolicyd/trust.d/" int trust_file_append(const char *fpath, list_t *list); int trust_file_load(const char *fpath, list_t *list, int memfd); int trust_file_update_path(const char *fpath, const char *path, bool use_filter); int trust_file_delete_path(const char *fpath, const char *path); int trust_file_rm_duplicates(const char *fpath, list_t *list); void trust_file_load_all(list_t *list, int memfd); int trust_file_update_path_all(const char *path, bool use_filter); int trust_file_delete_path_all(const char *path); void trust_file_rm_duplicates_all(list_t *list); #endif fapolicyd-2.0/src/perf-test/000077500000000000000000000000001524071644000160305ustar00rootroot00000000000000fapolicyd-2.0/src/perf-test/fapolicyd-perf-test.c000066400000000000000000000076201524071644000220620ustar00rootroot00000000000000/* * fapolicyd-perf-test.c - fapolicyd performance testing tool * Copyright (c) 2025-2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * Ondrej Mosnacek */ #include #include #include #include #include #include #include "config.h" #include "daemon-config.h" #include "message.h" #include "policy.h" #include "database.h" /* These need to be defined for the library */ atomic_bool stop = 0; unsigned int debug_mode = 0; conf_t config = {}; static char *get_line(FILE *f) { char *line = NULL; size_t len = 0; while (getline(&line, &len, f) != -1) { /* remove newline */ char *ptr = strchr(line, 0x0a); if (ptr) *ptr = 0; return line; } free(line); return NULL; } static int do_perf_test(FILE *input) { int rc = 0, resp_fd; pid_t our_pid; struct timeval t0, t1; char *path; set_message_mode(MSG_STDERR, DBG_NO); if (load_daemon_config(&config)) { rc = 1; goto out_reset_config; } if (load_rules(&config)) { rc = 1; goto out_reset_config; } // Setup lru caches if (init_event_system(&config)) { rc = 1; goto out_rules; } if (init_database(&config)) { rc = 1; goto out_event_system; } // Init the file test libraries file_init(); // Don't let it accidently emit audit events policy_no_audit(); resp_fd = open("/dev/null", O_WRONLY|O_CLOEXEC); if (resp_fd < 0) { fprintf(stderr, "Can't open dev null\n"); rc = 1; goto out_file; } our_pid = getpid(); printf("Starting scan...\n"); gettimeofday(&t0, NULL); while ((path = get_line(input))) { int fd = open(path, O_RDONLY|O_CLOEXEC); free(path); if (fd < 0) continue; // Build an "event" to exercise fapolicyd's decision making // decision_event_init copies the complete kernel metadata record. struct fanotify_event_metadata metadata = { 0 }; decision_event_t event; metadata.fd = fd; // listener closes after reply metadata.pid = our_pid; metadata.mask = FAN_OPEN_PERM; decision_event_init(&event, &metadata); make_policy_decision(&event, resp_fd, FAN_OPEN_PERM | FAN_OPEN_EXEC_PERM); } stop = 1; gettimeofday(&t1, NULL); long sec = t1.tv_sec - t0.tv_sec; long usec = t1.tv_usec - t0.tv_usec; if (usec < 0) { usec += 1000000; sec--; } long msec = usec / 1000; printf("Elapsed: %ld seconds, %ld milliseconds\n", sec, msec); close(resp_fd); out_file: file_close(); close_database(); out_event_system: destroy_event_system(); unlink_fifo(); out_rules: destroy_rules(); out_reset_config: free_daemon_config(&config); return rc; } static const char *USAGE = "Fapolicyd Performace Test\n\n" "Usage: %s [INPUT_FILE]\n\n" "Runs a dummy fapolicyd policy decision on each file from newline-separated\n" "list read from INPUT_FILE (or stdin if not specified) and prints the total" "time it took.\n" ; int main(int argc, char * const argv[]) { FILE *input = stdin; int rc; if (argc > 2) { printf(USAGE, argv[0]); return 2; } if (argc > 1) { input = fopen(argv[1], "r"); if (input == NULL) { fprintf(stderr, "Error opening input file\n"); return 1; } } rc = do_perf_test(input); if (input != stdin) fclose(input); return rc; } fapolicyd-2.0/src/tests/000077500000000000000000000000001524071644000152615ustar00rootroot00000000000000fapolicyd-2.0/src/tests/Makefile.am000066400000000000000000000172371524071644000173270ustar00rootroot00000000000000# Copyright 2020 Red Hat Inc. # All Rights Reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Authors: # Steve Grubb # CONFIG_CLEAN_FILES = *.orig *.cur SUBDIRS = if WITH_STRESS SUBDIRS += stress endif check_PROGRAMS = avl_test gid_proc_test uid_proc_test escape_test \ attr_sets_test elf_file_test fd_fgets_test file_type_detect_test \ rules_test policy_reload_test policy_concurrent_test failure_action_test \ queue_test event_test message_test \ trustdb_format_test trustdb_lmdb_test lru_test notify_test \ decision_defer_test decision_config_test \ decision_timing_report_test daemon_config_test ignore_mounts_risk_test check_PROGRAMS += cli_privilege_test AM_CPPFLAGS = -I${top_srcdir}/src/library/ AM_CFLAGS = -std=gnu11 avl_test_SOURCES = avl_test.c ${top_srcdir}/src/library/avl.c gid_proc_test_SOURCES = gid_proc_test.c test-stubs.c gid_proc_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la uid_proc_test_SOURCES = uid_proc_test.c test-stubs.c uid_proc_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la escape_test_SOURCES = escape_test.c test-stubs.c escape_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la attr_sets_test_SOURCES = attr_sets_test.c test-stubs.c attr_sets_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la elf_file_test_SOURCES = elf_file_test.c test-stubs.c elf_file_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la fd_fgets_test_SOURCES = fd_fgets_test.c test-stubs.c fd_fgets_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la file_type_detect_test_SOURCES = file_type_detect_test.c test-stubs.c file_type_detect_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la ignore_mounts_risk_test_SOURCES = ignore_mounts_risk_test.c test-stubs.c ignore_mounts_risk_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la ignore_mounts_risk_test_CPPFLAGS = \ -I${top_builddir} \ -I${top_srcdir}/src/library/ \ -I${top_srcdir}/src/cli/ cli_privilege_test_SOURCES = cli_privilege_test.c cli_privilege_test_CPPFLAGS = \ -I${top_builddir} \ -I${top_srcdir}/src/cli/ rules_test_SOURCES = rules_test.c rules_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la rules_test_CPPFLAGS = -I${top_srcdir}/src/library/ -DTEST_BASE=\"${top_srcdir}\" policy_reload_test_SOURCES = policy_reload_test.c test-stubs.c policy_reload_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la policy_concurrent_test_SOURCES = policy_concurrent_test.c test-stubs.c policy_concurrent_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la policy_concurrent_test_LDFLAGS = -pthread failure_action_test_SOURCES = failure_action_test.c \ ${top_srcdir}/src/library/failure-action.c queue_test_SOURCES = queue_test.c test-stubs.c queue_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la message_test_SOURCES = message_test.c test-stubs.c message_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la message_test_LDFLAGS = -pthread trustdb_format_test_SOURCES = trustdb_format_test.c trustdb_lmdb_test_SOURCES = trustdb_lmdb_test.c test-stubs.c trustdb_lmdb_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la trustdb_lmdb_test_LDFLAGS = -pthread lru_test_SOURCES = lru_test.c test-stubs.c lru_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la notify_test_SOURCES = notify_test.c test-stubs.c \ ${top_srcdir}/src/daemon/decision-worker.c \ ${top_srcdir}/src/daemon/worker-health.c \ ${top_srcdir}/src/daemon/fanotify-fs-error.c \ ${top_srcdir}/src/daemon/notify.c \ ${top_srcdir}/src/daemon/mounts.c \ ${top_srcdir}/src/daemon/state-report.c \ ${top_srcdir}/src/daemon/systemd-notify.c notify_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la notify_test_CPPFLAGS = \ -I${top_srcdir}/src/library/ \ -I${top_srcdir}/src/daemon/ \ -DTEST_SUBJECT_DEFER decision_defer_test_SOURCES = decision_defer_test.c \ ${top_srcdir}/src/library/decision-defer.c \ ${top_srcdir}/src/library/string-util.c decision_defer_test_CPPFLAGS = \ -I${top_builddir} \ -I${top_srcdir}/src/library/ \ -I${top_srcdir}/src/daemon/ decision_defer_test_LDFLAGS = -pthread decision_config_test_SOURCES = decision_config_test.c decision_config_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la daemon_config_test_SOURCES = daemon_config_test.c test-stubs.c daemon_config_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la decision_timing_report_test_SOURCES = decision_timing_report_test.c \ ${top_srcdir}/src/library/decision-timing.c decision_timing_report_test_CPPFLAGS = \ -I${top_builddir} \ -I${top_srcdir}/src/library/ \ -DTEST_DECISION_TIMING_REPORT event_test_SOURCES = event_test.c event_test_LDADD = \ ${top_builddir}/src/library/libfapolicyd_la-attr-lookup-metrics.o \ ${top_builddir}/src/library/libfapolicyd_la-decision-defer.o \ ${top_builddir}/src/library/libfapolicyd_la-event.o \ ${top_builddir}/src/library/libfapolicyd_la-lru.o \ ${top_builddir}/src/library/libfapolicyd_la-subject.o \ ${top_builddir}/src/library/libfapolicyd_la-subject-attr.o \ ${top_builddir}/src/library/libfapolicyd_la-object.o \ ${top_builddir}/src/library/libfapolicyd_la-object-attr.o \ ${top_builddir}/src/library/libfapolicyd_la-attr-sets.o \ ${top_builddir}/src/library/libfapolicyd_la-avl.o \ ${top_builddir}/src/library/libfapolicyd_la-decision-timing.o \ ${top_builddir}/src/library/libfapolicyd_la-string-util.o event_test_DEPENDENCIES = $(event_test_LDADD) if WITH_RPM check_PROGRAMS += filter_test filter_test_SOURCES = filter_test.c filter_test_SOURCES += test-stubs.c filter_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la filter_test_CPPFLAGS = -I${top_srcdir}/src/library/ -DTEST_BASE=\"${top_srcdir}\" filter_test_LDFLAGS = -pthread check_PROGRAMS += file_filter_test file_filter_test_SOURCES = file_filter_test.c file_filter_test_SOURCES += test-stubs.c file_filter_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la file_filter_test_CPPFLAGS = -I${top_srcdir}/src/library/ -DTEST_BASE=\"${top_srcdir}\" check_PROGRAMS += rpm_backend_test rpm_backend_test_SOURCES = rpm_backend_test.c test-stubs.c rpm_backend_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la endif if WITH_DEB check_PROGRAMS += deb_test md5_backend_test deb_filter_test LIBS += -ldpkg -lmd deb_test_CFLAGS = -std=gnu11 -fPIE -DPIE -pthread -g -W -Wall -Wshadow -Wundef -Wno-unused-result -Wno-unused-parameter -D_GNU_SOURCE -DLIBDPKG_VOLATILE_API deb_test_CPPFLAGS = \ -I${top_srcdir}/src/library/ \ -DTEST_BASE=\"${top_srcdir}\" deb_test_LDFLAGS = -pie -Wl,-z,relro -Wl,-z,now deb_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la -ldpkg -lmd deb_test_SOURCES = deb_test.c test-stubs.c md5_backend_test_CFLAGS = $(deb_test_CFLAGS) md5_backend_test_LDFLAGS = $(deb_test_LDFLAGS) md5_backend_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la -ldpkg -lmd md5_backend_test_SOURCES = md5_backend_test.c test-stubs.c deb_filter_test_CFLAGS = $(deb_test_CFLAGS) deb_filter_test_CPPFLAGS = \ -I${top_srcdir}/src/library/ \ -DTEST_BASE=\"${top_srcdir}\" deb_filter_test_LDFLAGS = $(deb_test_LDFLAGS) deb_filter_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la -ldpkg -lmd deb_filter_test_SOURCES = deb_filter_test.c test-stubs.c endif TESTS = $(check_PROGRAMS) fapolicyd-2.0/src/tests/attr_sets_test.c000066400000000000000000000050311524071644000204730ustar00rootroot00000000000000#include #include #include #include "attr-sets.h" /* * main - exercise registry-owned and standalone attr set APIs * Returns 0 on success, exits with error on failure. */ int main(void) { attr_sets_t *sets; attr_sets_entry_t *first; attr_sets_entry_t *set; attr_sets_entry_t *standalone; char name[32]; sets = attr_sets_create(); if (!sets) error(1, 0, "attr_sets_create failed"); if (attr_set_create("bad", 0) != NULL) error(1, 0, "attr_set_create accepted invalid type"); if (attr_sets_find(sets, "bad") != NULL) error(1, 0, "invalid set inserted"); first = attr_set_create("uids", UNSIGNED); if (!first) error(1, 0, "attr_set_create failed"); if (attr_set_append_int(first, 1000)) error(1, 0, "attr_set_append_int failed"); if (attr_set_append_int(first, -1) == 0) error(1, 0, "unsigned set accepted negative value"); if (attr_sets_add(sets, first)) error(1, 0, "attr_sets_add failed"); if (attr_sets_find(sets, "uids") != first) error(1, 0, "attr_sets_find returned wrong set"); for (int i = 0; i < 128; i++) { snprintf(name, sizeof(name), "set%d", i); set = attr_set_create(name, STRING); if (!set) error(1, 0, "attr_set_create resize case failed"); if (attr_set_append_str(set, name)) error(1, 0, "attr_set_append_str resize case failed"); if (attr_sets_add(sets, set)) error(1, 0, "attr_sets_add resize case failed"); } if (!attr_set_check_int(first, 1000)) error(1, 0, "registry resize invalidated set pointer"); standalone = attr_set_create(NULL, STRING); if (!standalone) error(1, 0, "standalone attr_set_create failed"); if (attr_set_append_str(standalone, "/usr/bin/")) error(1, 0, "standalone append failed"); if (standalone->has_glob) error(1, 0, "literal string marked as glob"); if (!attr_set_check_str(standalone, "/usr/bin/")) error(1, 0, "standalone exact check failed"); if (!attr_set_check_pstr(standalone, "/usr/bin/bash")) error(1, 0, "standalone prefix check failed"); if (attr_set_append_str(standalone, "/usr/bin/") == 0) error(1, 0, "duplicate string accepted"); if (attr_set_append_str(standalone, "/opt/app-*/bin/tool")) error(1, 0, "metacharacter string append failed"); if (standalone->has_glob) error(1, 0, "unmarked metacharacter string marked as glob"); if (attr_set_append_str(standalone, "glob:/opt/app-*/bin/tool")) error(1, 0, "marked glob string append failed"); if (!standalone->has_glob) error(1, 0, "marked glob string did not set lookup hint"); attr_set_destroy(standalone); attr_sets_destroy(sets); return 0; } fapolicyd-2.0/src/tests/avl_test.c000066400000000000000000000125271524071644000172550ustar00rootroot00000000000000#include #include #include #include "avl.h" typedef struct _avl_int_data { avl_t avl; int num; } avl_int_data_t; static avl_tree_t tree; static avl_tree_t tree2; static int intcmp_cb(void *a, void *b) { return ((avl_int_data_t *)a)->num - ((avl_int_data_t *)b)->num; } /* * destroy_tree - remove all nodes from an AVL tree * @t: tree to empty */ static void destroy_tree(avl_tree_t *t) { avl_t *cur; while ((cur = t->root) != NULL) { avl_int_data_t *tmp; tmp = (avl_int_data_t *)avl_remove(t, cur); free(tmp); } } int append(int num) { avl_int_data_t *data = malloc(sizeof(avl_int_data_t)); if (data == NULL) return 0; data->num = num; avl_t *ret = avl_insert(&tree, (avl_t *)data); if (ret != (avl_t *)data) { free(data); return 1; } return 0; } int append2(int num) { avl_int_data_t *data = malloc(sizeof(avl_int_data_t)); if (!data) return 0; data->num = num; avl_t *ret = avl_insert(&tree2, (avl_t *)data); if (ret != (avl_t *)data) { free(data); return 1; } return 0; } int node_remove(int num) { avl_int_data_t node, *n; node.num = num; n = (avl_int_data_t *)avl_remove(&tree, (avl_t *)&node); if (n) { if (n->num != num) error(1, 0, "Remove wrong item %d looking for %d", n->num, num); else { free(n); return 0; } } else error(1, 0, "Remove didn't find %d", num); return 0; } static int count_cb(void *entry, void *data) { (void)entry; (void)data; return 1; } static void test_search(void) { avl_int_data_t *res; avl_int_data_t tmp; avl_init(&tree, intcmp_cb); append(10); append(20); append(15); tmp.num = 20; res = (avl_int_data_t *)avl_search(&tree, (avl_t *)&tmp); if (!res || res->num != 20) error(1, 0, "avl_search failed to find 20"); tmp.num = 99; res = (avl_int_data_t *)avl_search(&tree, (avl_t *)&tmp); if (res) error(1, 0, "avl_search incorrectly found 99"); destroy_tree(&tree); } static void test_duplicates(void) { int ret; avl_init(&tree, intcmp_cb); ret = append(5); if (ret != 0) error(1, 0, "append(5) failed"); ret = append(5); if (ret != 1) error(1, 0, "append(5) duplicate not detected"); int count = avl_traverse(&tree, count_cb, NULL); if (count != 1) error(1, 0, "duplicate insert created %d nodes (expected 1)", count); destroy_tree(&tree); } static void test_traverse_count(void) { avl_init(&tree, intcmp_cb); append(1); append(2); append(3); append(4); append(5); int count = avl_traverse(&tree, count_cb, NULL); if (count != 5) error(1, 0, "avl_traverse returned %d (expected 5)", count); destroy_tree(&tree); } static void test_intersection(void) { avl_init(&tree, intcmp_cb); avl_init(&tree2, intcmp_cb); append(1); append(2); append(3); append2(3); append2(4); append2(5); if (!avl_intersection(&tree, &tree2)) error(1, 0, "avl_intersection failed to detect common element"); destroy_tree(&tree2); avl_init(&tree2, intcmp_cb); if (avl_intersection(&tree, &tree2)) error(1, 0, "avl_intersection false positive on empty second tree"); destroy_tree(&tree); if (avl_intersection(&tree, &tree2)) error(1, 0, "avl_intersection false positive on two empty trees"); destroy_tree(&tree2); } static void test_iterator_null(void) { avl_init(&tree, intcmp_cb); if (avl_first(NULL, &tree) != NULL) error(1, 0, "avl_first(NULL,…) should return NULL"); if (avl_next(NULL) != NULL) error(1, 0, "avl_next(NULL) should return NULL"); destroy_tree(&tree); } /* https://stackoverflow.com/questions/3955680/how-to-check-if-my-avl-tree-implementation-is-correct */ int main(void) { avl_int_data_t *node; int i; avl_iterator k; avl_init(&tree, intcmp_cb); append(2); append(1); /* force a 1L rotation */ append(3); node = (avl_int_data_t *)tree.root; if (node->num != 2) error(1, 0, "tree not balanced 1"); /* pop the top off to force a rebalance */ node_remove(2); node = (avl_int_data_t *)tree.root; if (node->num != 3) error(1, 0, "tree not balanced 2"); node_remove(1); append(2); /* tree should be 3-2, then add a 1 to force a 1R rotation */ append(1); node = (avl_int_data_t *)tree.root; if (node->num != 2) error(1, 0, "tree not balanced 3"); node_remove(3); node_remove(2); append(3); /* tree should be 1-3, now force a 2L rotation */ append(2); node = (avl_int_data_t *)tree.root; if (node->num != 2) error(1, 0, "tree not balanced 4"); node_remove(1); node_remove(2); append(1); /* tree should be 3-1, now force a 2R rotation */ append(2); node = (avl_int_data_t *)tree.root; if (node->num != 2) error(1, 0, "tree not balanced 5"); node_remove(1); node_remove(2); node_remove(3); if (tree.root != NULL) error(1, 0, "root not NULL when tree should be empty 1"); /* Now let's test the iterator functions */ append(2); append(5); append(1); append(4); append(3); i = 1; for (node = (avl_int_data_t *)avl_first(&k, &tree); node; node = (avl_int_data_t *)avl_next(&k)) { if (node->num != i) error(1, 0, "Iteration expected %d, got %d", i, node->num); else printf("Iterator %d\n", node->num); i++; } node_remove(1); node_remove(2); node_remove(3); node_remove(4); node_remove(5); if (tree.root != NULL) error(1, 0, "root not NULL when tree should be empty 2"); test_search(); test_duplicates(); test_traverse_count(); test_intersection(); test_iterator_null(); destroy_tree(&tree); destroy_tree(&tree2); return 0; } fapolicyd-2.0/src/tests/cli_privilege_test.c000066400000000000000000000033531524071644000213050ustar00rootroot00000000000000/* * cli_privilege_test.c - verify fapolicyd-cli privilege policies */ #include #include #include #include "../cli/cli-privilege.c" /* * expect_privilege - compare a synthetic identity with the expected result. * @label: test case name used in failures. * @requirement: privilege policy to evaluate. * @credentials: synthetic caller identity. * @expected: expected policy result. * Returns nothing; exits on failure. */ static void expect_privilege(const char *label, enum cli_privilege_requirement requirement, const struct cli_credentials *credentials, bool expected) { bool actual = privilege_satisfied(requirement, credentials); if (actual != expected) error(1, 0, "%s: expected %d got %d", label, expected, actual); } int main(void) { const gid_t supplementary[] = { 10, 20, 30 }; struct cli_credentials credentials = { .euid = 0, .egid = 1000, .allowed_gid = 20, .groups = supplementary, .group_count = 3, }; expect_privilege("root satisfies root-only", CLI_PRIVILEGE_ROOT, &credentials, true); expect_privilege("root satisfies group policy", CLI_PRIVILEGE_ROOT_OR_GROUP, &credentials, true); credentials.euid = 1000; expect_privilege("non-root rejected by root-only", CLI_PRIVILEGE_ROOT, &credentials, false); expect_privilege("supplementary group accepted", CLI_PRIVILEGE_ROOT_OR_GROUP, &credentials, true); credentials.egid = 20; credentials.allowed_gid = 40; expect_privilege("unrelated groups rejected", CLI_PRIVILEGE_ROOT_OR_GROUP, &credentials, false); credentials.allowed_gid = 20; credentials.groups = NULL; credentials.group_count = 0; expect_privilege("effective group accepted", CLI_PRIVILEGE_ROOT_OR_GROUP, &credentials, true); return EXIT_SUCCESS; } fapolicyd-2.0/src/tests/daemon_config_test.c000066400000000000000000000050441524071644000212570ustar00rootroot00000000000000/* * daemon_config_test.c - daemon configuration validation tests */ #include "config.h" #include #include #include #include #include #include "daemon-config.h" #define CHECK(expr, code, msg) \ do { \ if (!(expr)) \ error(1, 0, "%s", msg); \ } while (0) /* * minimal_config - build a small valid config for resource validation. * @cfg: destination config to initialize. * * Returns nothing. */ static void minimal_config(conf_t *cfg) { cfg->q_size = 1; cfg->decision_threads = 1; cfg->subj_cache_size = 16; cfg->obj_cache_size = 16; } /* * main - verify numeric parsing, decision_threads bounds, and reader sizing. * Returns 0 on success. Exits with error() on test failure. */ int main(void) { conf_t cfg = { 0 }; char overflow[64]; unsigned int value = 0; CHECK(daemon_config_parse_unsigned_for_tests(&value, "123") == 0 && value == 123, 8, "[ERROR:8] valid unsigned value was rejected"); CHECK(daemon_config_parse_unsigned_for_tests(&value, "123junk") != 0, 9, "[ERROR:9] partially numeric value was accepted"); CHECK(daemon_config_parse_unsigned_for_tests(&value, "") != 0, 10, "[ERROR:10] empty unsigned value was accepted"); if (UINTMAX_MAX > UINT_MAX) { snprintf(overflow, sizeof(overflow), "%" PRIuMAX, (uintmax_t)UINT_MAX + 1); CHECK(daemon_config_parse_unsigned_for_tests(&value, overflow) != 0, 11, "[ERROR:11] overflowing unsigned value was accepted"); } CHECK(daemon_config_lmdb_reader_limit(NULL) == 1 + DAEMON_CONFIG_LMDB_MAINTENANCE_READERS, 1, "[ERROR:1] default LMDB reader limit is wrong"); CHECK(daemon_config_lmdb_reader_limit(&cfg) == 1 + DAEMON_CONFIG_LMDB_MAINTENANCE_READERS, 2, "[ERROR:2] zeroed config LMDB reader limit is wrong"); minimal_config(&cfg); CHECK(validate_daemon_config(&cfg) == 0, 3, "[ERROR:3] minimal valid config was rejected"); cfg.decision_threads = 0; CHECK(validate_daemon_config(&cfg) != 0, 4, "[ERROR:4] zero decision_threads was accepted"); minimal_config(&cfg); cfg.decision_threads = DAEMON_CONFIG_DECISION_THREADS_MAX + 1; CHECK(validate_daemon_config(&cfg) != 0, 5, "[ERROR:5] oversized decision_threads was accepted"); CHECK(daemon_config_lmdb_reader_limit(&cfg) == 0, 6, "[ERROR:6] oversized LMDB reader limit was accepted"); cfg.decision_threads = DAEMON_CONFIG_DECISION_THREADS_MAX; CHECK(daemon_config_lmdb_reader_limit(&cfg) == DAEMON_CONFIG_LMDB_MAX_READERS, 7, "[ERROR:7] max LMDB reader limit is wrong"); return 0; } fapolicyd-2.0/src/tests/deb_filter_test.c000066400000000000000000000034431524071644000205670ustar00rootroot00000000000000/* * deb_filter_test.c - verify debdb applies the trust-source filter */ #include "config.h" #include #include #include "fapolicyd-backend.h" #include "filter.h" #include "md5-backend.h" #define BAD_MD5 "00000000000000000000000000000000" #define FILTER_CONF TEST_BASE "/src/tests/fixtures/filter-minimal.conf" extern md5_backend_result_t deb_backend_add_file_for_tests(const char *path, const char *hash, struct _hash_record **hashtable); #define CHECK(expr, code, msg) \ do { \ if (!(expr)) \ error(code, 0, "%s", msg); \ } while (0) /* * main - confirm debdb filter decisions happen before MD5 backend work. * * The test leaves deb_backend.memfd invalid. An allowed path must therefore * reach add_file_to_backend_by_md5() and fail fatally, while a denied path * must be skipped before the MD5 helper can validate the backend destination. * Returns 0 on success. Exits with error() on test failure. */ int main(void) { struct _hash_record *hashtable = NULL; md5_backend_result_t rc; CHECK(filter_init() == 0, 1, "[ERROR:1] filter_init failed"); CHECK(filter_load_file(FILTER_CONF) == 0, 2, "[ERROR:2] filter_load_file failed"); deb_backend.memfd = -1; deb_backend.entries = -1; rc = deb_backend_add_file_for_tests("/var/log/messages", BAD_MD5, &hashtable); CHECK(rc == MD5_BACKEND_SKIPPED, 3, "[ERROR:3] denied path was not skipped before hashing"); CHECK(hashtable == NULL, 4, "[ERROR:4] denied path added duplicate tracking state"); rc = deb_backend_add_file_for_tests("/etc/hosts", BAD_MD5, &hashtable); CHECK(rc == MD5_BACKEND_FATAL, 5, "[ERROR:5] allowed path did not reach MD5 backend"); CHECK(hashtable == NULL, 6, "[ERROR:6] fatal allowed path added duplicate tracking state"); filter_destroy(); return 0; } fapolicyd-2.0/src/tests/deb_test.c000066400000000000000000000065701524071644000172260ustar00rootroot00000000000000/* * deb_test.c - verify deb backend loader failure handling */ #include "config.h" #include #include #include #include #include #include #include #include #include #include "backend-manager.h" #include "fapolicyd-backend.h" extern backend deb_backend; #define CHILD_WAIT_MS 2000 #define LOADER_TEST_TIMEOUT_MS 100 #define LOADER_TEST_MAX_WAIT_MS 1000 #define CHECK(expr, code, msg) \ do { \ if (!(expr)) \ error(code, 0, "%s", msg); \ } while (0) /* * monotonic_ms - return monotonic milliseconds for timeout validation. * Returns -1 when the monotonic clock is unavailable. */ static int64_t monotonic_ms(void) { struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now)) return -1; return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000; } /* * wait_for_parent_close - act as a loader that never sends a snapshot. * Returns after the parent closes fd 3 or after a test safety timeout. */ static int wait_for_parent_close(void) { struct pollfd pfd = { .fd = 3, .events = POLLIN }; int rc; do { rc = poll(&pfd, 1, CHILD_WAIT_MS); } while (rc < 0 && errno == EINTR); return rc > 0 ? 0 : 1; } /* * true_path - find a harmless helper that exits without sending an fd. * Returns an executable path, or NULL when none is available. */ static const char *true_path(void) { if (access("/bin/true", X_OK) == 0) return "/bin/true"; if (access("/usr/bin/true", X_OK) == 0) return "/usr/bin/true"; return NULL; } /* * main - run deb loader IPC failure coverage. * Returns 0 on success. Exits with error() on test failure. */ int main(void) { const char *path = true_path(); char self_path[PATH_MAX]; conf_t cfg; backend_entry *entry; int64_t elapsed; int64_t start; ssize_t len; int rc; if (getenv("FAPO_SOCK_FD") != NULL) return wait_for_parent_close(); if (path == NULL) return 77; memset(&cfg, 0, sizeof(cfg)); cfg.trust = "debdb"; CHECK(backend_init(&cfg) == 0, 1, "[ERROR:1] debdb init failed"); entry = backend_get_first(); CHECK(entry != NULL, 2, "[ERROR:2] debdb backend not registered"); CHECK(strcmp(entry->backend->name, cfg.trust) == 0, 3, "[ERROR:3] debdb backend name mismatch"); deb_backend.memfd = -1; deb_backend.entries = -1; rc = deb_backend_load_from_path_for_tests(&cfg, path, LOADER_TEST_MAX_WAIT_MS); CHECK(rc != 0, 4, "[ERROR:4] deb IPC failure returned success"); CHECK(deb_backend.memfd == -1, 5, "[ERROR:5] failed deb load published a memfd"); CHECK(deb_backend.entries == -1, 6, "[ERROR:6] failed deb load published entries"); len = readlink("/proc/self/exe", self_path, sizeof(self_path) - 1); CHECK(len > 0, 7, "[ERROR:7] cannot resolve deb test executable"); self_path[len] = '\0'; start = monotonic_ms(); CHECK(start >= 0, 8, "[ERROR:8] cannot read monotonic clock"); rc = deb_backend_load_from_path_for_tests(&cfg, self_path, LOADER_TEST_TIMEOUT_MS); elapsed = monotonic_ms() - start; CHECK(rc != 0, 9, "[ERROR:9] stalled deb loader returned success"); CHECK(elapsed >= 0 && elapsed < LOADER_TEST_MAX_WAIT_MS, 10, "[ERROR:10] stalled deb loader wait was not bounded"); CHECK(deb_backend.memfd == -1, 11, "[ERROR:11] stalled deb load published a memfd"); CHECK(deb_backend.entries == -1, 12, "[ERROR:12] stalled deb load published entries"); backend_close(); return 0; } fapolicyd-2.0/src/tests/decision_config_test.c000066400000000000000000000052331524071644000216110ustar00rootroot00000000000000/* * decision_config_test.c - decision config generation tests */ #include "config.h" #include #include #include "decision-config.h" #define CHECK(expr, code, msg) \ do { \ if (!(expr)) \ error(1, 0, "%s", msg); \ } while (0) /* * main - verify decision config generations are immutable while pinned. * Returns 0 on success. Exits with error() on test failure. */ int main(void) { conf_t config = { .permissive = 0, .integrity = IN_NONE, }; const struct decision_config *pinned; unsigned int first_generation; unsigned int second_generation; CHECK(decision_config_generation(NULL) == 0, 1, "[ERROR:1] default generation is not zero"); CHECK(decision_config_publish(&config) == 0, 2, "[ERROR:2] first publish failed"); pinned = decision_config_pin(); first_generation = decision_config_generation(pinned); CHECK(first_generation != 0, 3, "[ERROR:3] first published generation is zero"); CHECK(decision_config_permissive(pinned) == 0, 4, "[ERROR:4] pinned permissive mode is wrong"); CHECK(decision_config_integrity(pinned) == IN_NONE, 5, "[ERROR:5] pinned integrity mode is wrong"); CHECK(decision_config_rpm_sha256_only(pinned) == 0, 15, "[ERROR:15] pinned rpm_sha256_only flag is wrong"); config.permissive = 1; config.integrity = IN_SHA256; config.rpm_sha256_only = 1; CHECK(decision_config_publish(&config) == 0, 6, "[ERROR:6] second publish failed"); second_generation = decision_config_active_generation(); CHECK(second_generation > first_generation, 7, "[ERROR:7] generation did not advance"); CHECK(decision_config_generation(NULL) == first_generation, 8, "[ERROR:8] current generation ignored thread pin"); CHECK(decision_config_permissive(NULL) == 0, 9, "[ERROR:9] pinned permissive mode was mutated"); CHECK(decision_config_integrity(NULL) == IN_NONE, 10, "[ERROR:10] pinned integrity mode was mutated"); CHECK(decision_config_rpm_sha256_only(NULL) == 0, 16, "[ERROR:16] pinned rpm_sha256_only flag was mutated"); decision_config_unpin(pinned); CHECK(decision_config_generation(NULL) == second_generation, 11, "[ERROR:11] current generation did not unpin"); CHECK(decision_config_permissive(NULL) == 1, 12, "[ERROR:12] active permissive mode is wrong"); CHECK(decision_config_integrity(NULL) == IN_SHA256, 13, "[ERROR:13] active integrity mode is wrong"); CHECK(decision_config_rpm_sha256_only(NULL) == 1, 17, "[ERROR:17] active rpm_sha256_only flag is wrong"); decision_config_destroy(); CHECK(decision_config_generation(NULL) == 0, 14, "[ERROR:14] destroy did not restore default generation"); return 0; } fapolicyd-2.0/src/tests/decision_defer_test.c000066400000000000000000000305511524071644000214320ustar00rootroot00000000000000/* * decision_defer_test.c - unit tests for subject-slot decision deferral */ #include #include #include #include #include #include "decision-defer.h" #define METRICS_SNAPSHOT_ITERATIONS 10000 #define CHECK(cond, code, msg) \ do { \ if (!(cond)) { \ fprintf(stderr, "%s\n", msg); \ return code; \ } \ } while (0) /* * make_event - build a synthetic decision event for queue tests. * @pid: event subject pid. * @fd: synthetic fanotify permission fd. * @slot: subject cache slot that blocks the event. * * Returns an initialized decision event. completed_subject_slot is set to a * non-default value so tests can prove deferral clears stale release state. */ static decision_event_t make_event(pid_t pid, int fd, unsigned int slot) { decision_event_t event; memset(&event, 0, sizeof(event)); event.metadata.pid = pid; event.metadata.fd = fd; event.metadata.mask = FAN_OPEN_PERM; event.subject_slot = slot; event.completed_subject_slot = slot + 100; return event; } struct metrics_snapshot_mutator { struct decision_defer_queue *defer; int failed; }; /* * metrics_snapshot_mutator_run - mutate a defer queue during reporting. * @data: struct metrics_snapshot_mutator state. * * Returns NULL after repeatedly adding and removing one deferred event. */ static void *metrics_snapshot_mutator_run(void *data) { struct metrics_snapshot_mutator *mutator = data; decision_event_t event, out; unsigned int i; for (i = 0; i < METRICS_SNAPSHOT_ITERATIONS; i++) { event = make_event(600 + i, 100 + i, i % 3); if (decision_defer_push(mutator->defer, &event) || decision_defer_pop_any(mutator->defer, &out) != 1) { mutator->failed = 1; break; } } return NULL; } /* * test_slot_release_chain - pop deferred events for one released slot. * * Multiple events can wait on the same subject slot. Releasing that slot must * return the oldest matching event first, then the next matching event, while * leaving unrelated slots parked until their own release. * * Returns 0 on success, or a distinct failure code. */ static int test_slot_release_chain(void) { struct decision_defer_queue defer; decision_event_t event, out; CHECK(decision_defer_init(&defer, 1) == 0, 1, "[ERROR:1] decision_defer_init failed"); event = make_event(100, 10, 3); CHECK(decision_defer_push(&defer, &event) == 0, 2, "[ERROR:2] first defer push failed"); event = make_event(101, 11, 4); CHECK(decision_defer_push(&defer, &event) == 0, 3, "[ERROR:3] unrelated defer push failed"); event = make_event(102, 12, 3); CHECK(decision_defer_push(&defer, &event) == 0, 4, "[ERROR:4] chained defer push failed"); CHECK(defer.current == 3 && defer.max_depth == 3, 5, "[ERROR:5] defer depth not tracked"); CHECK(decision_defer_pop_slot(&defer, 3, &out) == 1, 6, "[ERROR:6] released slot did not pop"); CHECK(out.metadata.pid == 100 && out.metadata.fd == 10, 7, "[ERROR:7] released slot did not pop oldest event"); CHECK(out.completed_subject_slot == DECISION_EVENT_NO_SLOT, 8, "[ERROR:8] deferred event kept stale completion slot"); CHECK(decision_defer_pop_slot(&defer, 3, &out) == 1, 9, "[ERROR:9] chained release did not pop second event"); CHECK(out.metadata.pid == 102 && out.metadata.fd == 12, 10, "[ERROR:10] chained release popped wrong event"); CHECK(decision_defer_pop_slot(&defer, 3, &out) == 0, 11, "[ERROR:11] released slot popped unrelated event"); CHECK(decision_defer_pop_slot(&defer, 4, &out) == 1, 12, "[ERROR:12] unrelated slot did not remain deferred"); CHECK(out.metadata.pid == 101 && defer.current == 0, 13, "[ERROR:13] unrelated release or depth mismatch"); decision_defer_destroy(&defer); return 0; } /* * report_contains - test whether defer metrics report contains expected text. * @metrics: metrics snapshot to render. * @needle: expected substring. * * Returns 1 when the rendered report contains @needle, 0 otherwise. */ static int report_contains(const struct decision_defer_metrics *metrics, const char *needle) { FILE *report; char buf[512]; size_t used; int found; report = tmpfile(); if (report == NULL) return 0; decision_defer_metrics_report(report, metrics); fflush(report); rewind(report); used = fread(buf, 1, sizeof(buf) - 1, report); buf[used] = 0; found = strstr(buf, needle) != NULL; fclose(report); return found; } /* * test_full_array_fallback_metrics - verify bounded capacity accounting. * * A full defer array must reject another event so the caller can fall back to * historical eviction behavior. The fallback counter and reset semantics must * remain observable in the metrics snapshot and report. * * Returns 0 on success, or a distinct failure code. */ static int test_full_array_fallback_metrics(void) { struct decision_defer_queue defer; struct decision_defer_metrics metrics; decision_event_t event; char expected[64]; unsigned int i; CHECK(decision_defer_init(&defer, 1) == 0, 20, "[ERROR:20] decision_defer_init failed"); CHECK(defer.capacity == DECISION_DEFER_MIN, 21, "[ERROR:21] tiny cache did not use defer floor"); for (i = 0; i < defer.capacity; i++) { event = make_event(200 + i, 20 + i, i % 2); CHECK(decision_defer_push(&defer, &event) == 0, 22, "[ERROR:22] filling defer array failed"); } event = make_event(999, 99, 1); errno = 0; CHECK(decision_defer_push(&defer, &event) == -1, 23, "[ERROR:23] full defer array accepted an event"); CHECK(errno == ENOSPC, 24, "[ERROR:24] full defer array did not set ENOSPC"); decision_defer_count_fallback(&defer); decision_defer_metrics_snapshot_reset(&defer, &metrics, 0); CHECK(metrics.capacity == defer.capacity, 25, "[ERROR:25] metrics capacity mismatch"); CHECK(metrics.current_depth == defer.capacity, 26, "[ERROR:26] metrics current depth mismatch"); CHECK(metrics.deferred_events == defer.capacity, 27, "[ERROR:27] metrics deferred events mismatch"); CHECK(metrics.max_depth == defer.capacity, 28, "[ERROR:28] metrics max depth mismatch"); CHECK(metrics.fallbacks == 1, 29, "[ERROR:29] metrics fallback count mismatch"); snprintf(expected, sizeof(expected), "Subject deferred events: %u", defer.capacity); CHECK(report_contains(&metrics, expected), 30, "[ERROR:30] metrics report missing deferred event count"); CHECK(report_contains(&metrics, "Subject defer fallbacks: 1"), 31, "[ERROR:31] metrics report missing fallback count"); decision_defer_metrics_snapshot_reset(&defer, &metrics, 1); CHECK(metrics.deferred_events == defer.capacity, 32, "[ERROR:32] reset snapshot lost deferred event count"); CHECK(metrics.fallbacks == 1, 33, "[ERROR:33] reset snapshot lost fallback count"); CHECK(defer.deferred_events == 0, 34, "[ERROR:34] reset did not clear deferred event counter"); CHECK(defer.fallbacks == 0, 35, "[ERROR:35] reset did not clear fallback counter"); CHECK(defer.max_depth == defer.current, 36, "[ERROR:36] reset did not leave max depth at live depth"); while (decision_defer_pop_any(&defer, &event)) ; CHECK(defer.current == 0, 37, "[ERROR:37] defer array did not drain"); decision_defer_destroy(&defer); return 0; } /* * test_shutdown_pop_any_cleanup - drain deferred fds in shutdown order. * * Shutdown cleanup uses pop_any() to take ownership of every still-deferred * event. This test closes each popped permission fd and verifies each one is * released exactly once. * * Returns 0 on success, or a distinct failure code. */ static int test_shutdown_pop_any_cleanup(void) { struct decision_defer_queue defer; decision_event_t event; int pipes[3][2]; unsigned int i; CHECK(decision_defer_init(&defer, 1) == 0, 40, "[ERROR:40] decision_defer_init failed"); for (i = 0; i < 3; i++) { CHECK(pipe(pipes[i]) == 0, 41, "[ERROR:41] pipe setup failed"); event = make_event(300 + i, pipes[i][0], 7 - i); CHECK(decision_defer_push(&defer, &event) == 0, 42, "[ERROR:42] shutdown defer push failed"); } for (i = 0; i < 3; i++) { CHECK(decision_defer_pop_any(&defer, &event) == 1, 43, "[ERROR:43] shutdown pop_any missed event"); CHECK(event.metadata.pid == 300 + i, 44, "[ERROR:44] shutdown pop_any order mismatch"); CHECK(close(event.metadata.fd) == 0, 45, "[ERROR:45] shutdown fd close failed"); } CHECK(decision_defer_pop_any(&defer, &event) == 0, 46, "[ERROR:46] shutdown pop_any returned extra event"); for (i = 0; i < 3; i++) { errno = 0; CHECK(close(pipes[i][0]) == -1 && errno == EBADF, 47, "[ERROR:47] deferred fd was not closed exactly once"); close(pipes[i][1]); } decision_defer_destroy(&defer); return 0; } /* * match_slot - predicate used by pop_if tests. * @event: deferred event candidate. * @ctx: pointer to the slot number that should match. * * Returns 1 when @event uses the requested subject slot, 0 otherwise. */ static int match_slot(const decision_event_t *event, void *ctx) { unsigned int slot = *(unsigned int *)ctx; return event->subject_slot == slot; } /* * test_pop_if_keeps_matching_order - pop oldest matching deferred event. * * Periodic defer rechecks need to release the oldest event that is now ready * without disturbing older events for slots that are still blocked. * * Returns 0 on success, or a distinct failure code. */ static int test_pop_if_keeps_matching_order(void) { struct decision_defer_queue defer; decision_event_t event, out; unsigned int slot = 5; CHECK(decision_defer_init(&defer, 1) == 0, 60, "[ERROR:60] decision_defer_init failed"); event = make_event(400, 40, 4); CHECK(decision_defer_push(&defer, &event) == 0, 61, "[ERROR:61] first defer push failed"); event = make_event(401, 41, 5); CHECK(decision_defer_push(&defer, &event) == 0, 62, "[ERROR:62] matching defer push failed"); event = make_event(402, 42, 5); CHECK(decision_defer_push(&defer, &event) == 0, 63, "[ERROR:63] second matching defer push failed"); CHECK(decision_defer_pop_if(&defer, match_slot, &slot, &out) == 1, 64, "[ERROR:64] pop_if did not pop a matching event"); CHECK(out.metadata.pid == 401 && defer.current == 2, 65, "[ERROR:65] pop_if did not pop oldest matching event"); CHECK(decision_defer_pop_any(&defer, &out) == 1, 66, "[ERROR:66] pop_any missed oldest remaining event"); CHECK(out.metadata.pid == 400, 67, "[ERROR:67] pop_if disturbed unrelated older event"); decision_defer_destroy(&defer); return 0; } /* * test_concurrent_metrics_snapshot - report while a worker mutates defers. * * Reporting runs on a different decision worker, so snapshot/reset must see * one complete queue state even while the owner adds and removes events. * * Returns 0 on success, or a distinct failure code. */ static int test_concurrent_metrics_snapshot(void) { struct decision_defer_queue defer; struct decision_defer_metrics metrics; struct metrics_snapshot_mutator mutator; pthread_t thread; unsigned int i; int snapshot_valid = 1; int rc; CHECK(decision_defer_init(&defer, 1) == 0, 70, "[ERROR:70] concurrent defer init failed"); mutator.defer = &defer; mutator.failed = 0; rc = pthread_create(&thread, NULL, metrics_snapshot_mutator_run, &mutator); if (rc) { decision_defer_destroy(&defer); fprintf(stderr, "[ERROR:71] concurrent mutator create failed\n"); return 71; } for (i = 0; i < METRICS_SNAPSHOT_ITERATIONS; i++) { decision_defer_metrics_snapshot_reset(&defer, &metrics, 1); if (metrics.capacity != defer.capacity || metrics.current_depth > metrics.capacity || metrics.max_depth < metrics.current_depth) { snapshot_valid = 0; break; } } rc = pthread_join(thread, NULL); if (rc) { decision_defer_destroy(&defer); fprintf(stderr, "[ERROR:72] concurrent mutator join failed\n"); return 72; } decision_defer_destroy(&defer); CHECK(mutator.failed == 0, 73, "[ERROR:73] concurrent defer mutation failed"); CHECK(snapshot_valid, 74, "[ERROR:74] concurrent defer snapshot was inconsistent"); return 0; } /* main - run defer queue unit tests. */ int main(void) { int rc; rc = test_slot_release_chain(); if (rc) return rc; rc = test_full_array_fallback_metrics(); if (rc) return rc; rc = test_shutdown_pop_any_cleanup(); if (rc) return rc; rc = test_pop_if_keeps_matching_order(); if (rc) return rc; rc = test_concurrent_metrics_snapshot(); if (rc) return rc; return 0; } fapolicyd-2.0/src/tests/decision_timing_report_test.c000066400000000000000000000241621524071644000232300ustar00rootroot00000000000000/* * decision_timing_report_test.c - timing report formatting tests */ #include #include #include #include #include #include "decision-timing.h" #define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) #define CHECK(expr, code, msg) \ do { \ if (!(expr)) \ error(1, 0, "%s", msg); \ } while (0) struct report_case { const struct decision_timing_test_stage_sample *samples; unsigned int sample_count; const struct decision_timing_test_report_input *input; }; void msg(int priority, const char *fmt, ...) { (void)priority; (void)fmt; } /* * read_test_report - capture synthetic timing report output. * @test: report inputs. * @buf: destination buffer. * @size: size of @buf. * Returns nothing. Exits if the temporary stream cannot be used. */ static void read_test_report(const struct report_case *test, char *buf, size_t size) { FILE *f = tmpfile(); size_t used; if (f == NULL) error(1, 0, "tmpfile failed"); decision_timing_test_write_report(f, test->samples, test->sample_count, test->input); fflush(f); rewind(f); used = fread(buf, 1, size - 1, f); buf[used] = 0; fclose(f); } /* * require_text - find required report text. * @report: report buffer. * @needle: required text. * @code: failure code. * Returns the first occurrence of @needle. */ static const char *require_text(const char *report, const char *needle, int code) { const char *found = strstr(report, needle); CHECK(found != NULL, code, needle); return found; } /* * reject_text - assert that report text is absent. * @report: report buffer. * @needle: text that must be absent. * @code: failure code. * Returns nothing. */ static void reject_text(const char *report, const char *needle, int code) { CHECK(strstr(report, needle) == NULL, code, needle); } /* * require_trailing_blank_line - verify reports separate repeated output. * @report: report buffer. * @code: failure code. * Returns nothing. */ static void require_trailing_blank_line(const char *report, int code) { size_t len = strlen(report); CHECK(len >= 2 && report[len - 1] == '\n' && report[len - 2] == '\n', code, "report missing trailing blank line"); } static const struct decision_timing_test_stage_sample full_samples[] = { { DECISION_TIMING_STAGE_TOTAL, 99, 99000000ULL, 5000000ULL, 5 }, { DECISION_TIMING_STAGE_TOTAL, 1, 260000000ULL, 260000000ULL, 13 }, { DECISION_TIMING_STAGE_QUEUE_WAIT, 100, 17400000ULL, 314000000ULL, 4 }, { DECISION_TIMING_STAGE_EVENT_BUILD, 100, 20000000ULL, 1000000ULL, 3 }, { DECISION_TIMING_STAGE_CACHE_FLUSH, 1, 1000000ULL, 1000000ULL, 5 }, { DECISION_TIMING_STAGE_PROC_FINGERPRINT, 100, 5000000ULL, 100000ULL, 3 }, { DECISION_TIMING_STAGE_PROC_STATUS_EXE_LOOKUP, 3, 1200000ULL, 500000ULL, 5 }, { DECISION_TIMING_STAGE_FD_STAT, 100, 3000000ULL, 100000ULL, 3 }, { DECISION_TIMING_STAGE_FD_PATH_RESOLUTION, 50, 4000000ULL, 200000ULL, 4 }, { DECISION_TIMING_STAGE_EVAL_MIME_DETECTION, 10, 10000000ULL, 1000000ULL, 6 }, { DECISION_TIMING_STAGE_RESPONSE_MIME_DETECTION, 10, 12000000ULL, 5000000ULL, 7 }, { DECISION_TIMING_STAGE_EVAL_MIME_FAST_CLASSIFICATION, 10, 4000000ULL, 1000000ULL, 5 }, { DECISION_TIMING_STAGE_RESPONSE_MIME_FAST_CLASSIFICATION, 10, 3690000ULL, 1000000ULL, 5 }, { DECISION_TIMING_STAGE_EVAL_MIME_GATHER_ELF, 5, 5000000ULL, 1200000ULL, 5 }, { DECISION_TIMING_STAGE_RESPONSE_MIME_GATHER_ELF, 5, 4220000ULL, 1200000ULL, 5 }, { DECISION_TIMING_STAGE_EVAL_MIME_LIBMAGIC_FALLBACK, 5, 4300000ULL, 2000000ULL, 6 }, { DECISION_TIMING_STAGE_RESPONSE_MIME_LIBMAGIC_FALLBACK, 10, 12000000ULL, 4000000ULL, 7 }, { DECISION_TIMING_STAGE_HASH_IMA, 2, 7500000ULL, 3000000ULL, 7 }, { DECISION_TIMING_STAGE_HASH_SHA, 3, 4500000ULL, 2000000ULL, 6 }, { DECISION_TIMING_STAGE_EVAL_TRUST_DB_LOOKUP, 10, 4000000ULL, 2000000ULL, 5 }, { DECISION_TIMING_STAGE_RESPONSE_TRUST_DB_LOOKUP, 5, 2000000ULL, 1000000ULL, 5 }, { DECISION_TIMING_STAGE_EVAL_TRUST_DB_LOCK_WAIT, 10, 1000ULL, 500ULL, 0 }, { DECISION_TIMING_STAGE_RESPONSE_TRUST_DB_LOCK_WAIT, 5, 500ULL, 200ULL, 0 }, { DECISION_TIMING_STAGE_EVAL_TRUST_DB_READ, 10, 3999000ULL, 1999000ULL, 5 }, { DECISION_TIMING_STAGE_RESPONSE_TRUST_DB_READ, 5, 1999500ULL, 1000000ULL, 5 }, { DECISION_TIMING_STAGE_RULE_LOCK_WAIT, 100, 100000ULL, 5000ULL, 1 }, { DECISION_TIMING_STAGE_RULE_EVALUATION, 100, 30000000ULL, 2000000ULL, 5 }, { DECISION_TIMING_STAGE_RESPONSE_TOTAL, 99, 140000000ULL, 3000000ULL, 5 }, { DECISION_TIMING_STAGE_RESPONSE_TOTAL, 1, 260000000ULL, 260000000ULL, 13 }, { DECISION_TIMING_STAGE_SYSLOG_DEBUG_FORMAT, 100, 300000000ULL, 260000000ULL, 13 }, { DECISION_TIMING_STAGE_AUDIT_RESPONSE_PREP, 100, 500000ULL, 10000ULL, 2 }, { DECISION_TIMING_STAGE_FANOTIFY_RESPONSE_WRITE, 100, 1000000ULL, 50000ULL, 3 }, }; static const struct decision_timing_test_stage_sample sparse_samples[] = { { DECISION_TIMING_STAGE_TOTAL, 4, 4000000ULL, 1000000ULL, 6 }, { DECISION_TIMING_STAGE_EVENT_BUILD, 4, 1000000ULL, 250000ULL, 4 }, { DECISION_TIMING_STAGE_RULE_EVALUATION, 4, 2000000ULL, 500000ULL, 5 }, { DECISION_TIMING_STAGE_RESPONSE_TOTAL, 4, 1000000ULL, 250000ULL, 4 }, }; static const struct decision_timing_test_report_input full_input = { .duration_ns = 10000000000ULL, .max_queue_depth = 7, .q_size = 40, }; static const struct decision_timing_test_report_input sparse_input = { .duration_ns = 1000000000ULL, .max_queue_depth = 0, .q_size = 40, }; /* * test_full_report - verify the insight sections with all major stages. * Returns nothing. */ static void test_full_report(void) { const struct report_case test = { .samples = full_samples, .sample_count = ARRAY_SIZE(full_samples), .input = &full_input, }; const char *tldr, *overall, *queueing, *phases, *helper_intro, *helpers; const char *observations, *drivers, *detailed, *tail; const char *not_observed, *notes; char report[16384]; read_test_report(&test, report, sizeof(report)); tldr = require_text(report, "\nTL;DR:", 43); overall = require_text(report, "\nOverall decision latency:", 1); queueing = require_text(report, "\nQueueing:", 2); phases = require_text(report, "\nDecision phase timing:", 3); helper_intro = require_text(report, "\nLazy helper attribution:", 4); drivers = require_text(report, "\nLazy helper attribution by driver:", 5); helpers = require_text(report, "\nCombined lazy helper attribution:", 6); observations = require_text(report, "\nDerived observations:", 35); detailed = require_text(report, "\nDetailed stage timing, sorted by total time:", 7); tail = require_text(report, "\nStage tail summary:", 8); not_observed = require_text(report, "\nNot observed:", 9); notes = require_text(report, "\nNotes:", 10); CHECK(tldr < overall && overall < queueing && queueing < phases && phases < helper_intro && helper_intro < drivers && drivers < helpers && helpers < observations && observations < detailed && detailed < tail && tail < not_observed && not_observed < notes, 11, "[ERROR:11] report sections are out of order"); require_text(report, "max queue depth: 7", 12); require_text(tldr, "MIME detection dominates helper time", 44); require_text(tldr, "Manual/debug response formatting accounts", 45); require_text(tldr, "Queueing is healthy", 46); require_text(detailed, "decision:total", 30); require_text(report, "event_build", 13); require_text(report, "evaluation", 14); require_text(report, "response", 15); require_text(report, "syslog/debug-heavy", 16); require_text(report, "mime_detection:libmagic_fallback", 17); require_text(report, "mime_detection:fast_classification 4.00 ms", 40); require_text(report, "trust_db_lookup:lock_wait", 18); reject_text(report, "metrics:", 19); require_text(report, "tail: >10ms", 20); require_text(report, "hash_ima is rare but expensive", 21); require_text(report, "hash_sha is rare but expensive", 41); require_text(report, "evaluation:hash_sha:total", 42); require_text(report, "trust DB lock wait is negligible", 22); require_text(report, "active logical driver: evaluation or response", 23); require_text(report, "Queueing was low with small bursts: max queue " "depth 7 of 40 (17.5%), p95 wait <=100us, max wait " "314 ms.", 36); require_text(report, "Largest manual/debug phase contributor: response", 37); require_text(report, "Largest daemon-relevant decision phase contributor: evaluation", 38); require_trailing_blank_line(report, 48); reject_text(report, "other:", 31); reject_text(report, "Other total", 32); reject_text(report, ">100ms 0/", 33); reject_text(report, ">250ms 0/", 34); reject_text(report, "Stage timings may be nested", 39); } /* * test_sparse_report - verify missing queue and helper rows are stable. * Returns nothing. */ static void test_sparse_report(void) { const struct report_case test = { .samples = sparse_samples, .sample_count = ARRAY_SIZE(sparse_samples), .input = &sparse_input, }; char report[8192]; read_test_report(&test, report, sizeof(report)); require_text(report, "\nTL;DR:\n - No dominant timing findings observed.\n", 47); require_text(report, "\nQueueing:\n not observed\n max queue depth: 0", 24); require_text(report, "\nLazy helper attribution by driver:", 25); require_text(report, "\nCombined lazy helper attribution:", 26); require_text(report, " not observed", 27); require_trailing_blank_line(report, 49); reject_text(report, "Response note:", 28); reject_text(report, "syslog/debug-heavy", 29); } /* * test_missing_input - verify unavailable run-level input is deterministic. * Returns nothing. */ static void test_missing_input(void) { const struct report_case test = { .samples = sparse_samples, .sample_count = ARRAY_SIZE(sparse_samples), .input = NULL, }; char report[8192]; read_test_report(&test, report, sizeof(report)); require_text(report, "max queue depth: 0", 30); require_trailing_blank_line(report, 50); } /* * main - run timing report formatting tests. * Returns 0 on success. Exits with error() on test failure. */ int main(void) { test_full_report(); test_sparse_report(); test_missing_input(); return 0; } fapolicyd-2.0/src/tests/elf_file_test.c000066400000000000000000000117161524071644000202370ustar00rootroot00000000000000/* * elf_file_test.c - verify gather_elf flag classification * * Each case writes a synthetic object into an anonymous descriptor created * by memfd_create (or an unlinked temporary file when memfd is unavailable), * then checks the returned flag bitmap. Coverage includes 32-bit/64-bit * executables, text and shebang scripts, truncated ELF headers, and an * oversized program header table. The expectations cover IS_ELF, HAS_LOAD, * HAS_ERROR, TEXT_SCRIPT, HAS_SHEBANG, and HAS_RWE_LOAD. */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include "file.h" #include "process.h" #ifndef MFD_CLOEXEC #define MFD_CLOEXEC 0 #endif static void build_ident(unsigned char ident[EI_NIDENT], unsigned char elf_class) { memset(ident, 0, EI_NIDENT); ident[EI_MAG0] = ELFMAG0; ident[EI_MAG1] = ELFMAG1; ident[EI_MAG2] = ELFMAG2; ident[EI_MAG3] = ELFMAG3; ident[EI_CLASS] = elf_class; ident[EI_DATA] = ELFDATA2LSB; ident[EI_VERSION] = EV_CURRENT; } static size_t make_elf32(unsigned char *buf, int with_load) { Elf32_Ehdr *eh = (Elf32_Ehdr *)buf; Elf32_Phdr *ph = (Elf32_Phdr *)(buf + sizeof(Elf32_Ehdr)); memset(buf, 0, sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr)); build_ident(eh->e_ident, ELFCLASS32); eh->e_type = ET_EXEC; eh->e_machine = EM_386; eh->e_version = EV_CURRENT; eh->e_entry = 0x8048000; eh->e_phoff = sizeof(Elf32_Ehdr); eh->e_ehsize = sizeof(Elf32_Ehdr); eh->e_phentsize = sizeof(Elf32_Phdr); eh->e_phnum = with_load ? 1 : 0; if (!with_load) return sizeof(Elf32_Ehdr); ph->p_type = PT_LOAD; ph->p_offset = 0; ph->p_vaddr = 0x8048000; ph->p_paddr = 0x8048000; ph->p_filesz = 0x1000; ph->p_memsz = 0x1000; ph->p_flags = PF_R | PF_X; ph->p_align = 0x1000; return sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr); } static size_t make_elf64(unsigned char *buf, unsigned int flags) { Elf64_Ehdr *eh = (Elf64_Ehdr *)buf; Elf64_Phdr *ph = (Elf64_Phdr *)(buf + sizeof(Elf64_Ehdr)); memset(buf, 0, sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr)); build_ident(eh->e_ident, ELFCLASS64); eh->e_type = ET_EXEC; eh->e_machine = EM_X86_64; eh->e_version = EV_CURRENT; eh->e_entry = 0x400000; eh->e_phoff = sizeof(Elf64_Ehdr); eh->e_ehsize = sizeof(Elf64_Ehdr); eh->e_phentsize = sizeof(Elf64_Phdr); eh->e_phnum = 1; ph->p_type = PT_LOAD; ph->p_offset = 0; ph->p_vaddr = 0x400000; ph->p_paddr = 0x400000; ph->p_filesz = 0x2000; ph->p_memsz = 0x2000; ph->p_flags = flags; ph->p_align = 0x200000; return sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr); } static size_t make_elf64_header_only(unsigned char *buf, unsigned short phnum) { Elf64_Ehdr *eh = (Elf64_Ehdr *)buf; memset(buf, 0, sizeof(Elf64_Ehdr)); build_ident(eh->e_ident, ELFCLASS64); eh->e_type = ET_EXEC; eh->e_machine = EM_X86_64; eh->e_version = EV_CURRENT; eh->e_entry = 0x400000; eh->e_phoff = sizeof(Elf64_Ehdr); eh->e_ehsize = sizeof(Elf64_Ehdr); eh->e_phentsize = sizeof(Elf64_Phdr); eh->e_phnum = phnum; return sizeof(Elf64_Ehdr); } static size_t make_truncated32(unsigned char *buf) { memset(buf, 0, EI_NIDENT + 4); build_ident(buf, ELFCLASS32); return EI_NIDENT + 4; } static int fd_from_buffer(const char *name, const void *buf, size_t len) { int fd = memfd_create(name, MFD_CLOEXEC); if (fd < 0) { char path[] = "/tmp/fapolicyd-elftest-XXXXXX"; fd = mkstemp(path); if (fd < 0) return -1; unlink(path); } if (write(fd, buf, len) != (ssize_t)len) { int saved = errno; close(fd); errno = saved; return -1; } if (lseek(fd, 0, SEEK_SET) < 0) { int saved = errno; close(fd); errno = saved; return -1; } return fd; } static void expect_flags(const char *label, const void *buf, size_t len, uint32_t expect) { int fd = fd_from_buffer(label, buf, len); if (fd < 0) error(1, errno, "%s: unable to obtain descriptor", label); uint32_t got = gather_elf(fd, (off_t)len); close(fd); if (got != expect) error(1, 0, "%s: expected 0x%x got 0x%x", label, expect, got); } int main(void) { unsigned char buf[sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr)]; unsigned char shebang[] = "#!/bin/sh\nexit 0\n"; unsigned char text_script[] = "echo hello world\n"; unsigned char trunc_buf[EI_NIDENT + 4]; size_t sz32 = make_elf32(buf, 1); expect_flags("elf32-load", buf, sz32, IS_ELF | HAS_EXEC | HAS_LOAD); size_t sz64 = make_elf64(buf, PF_R | PF_W | PF_X); expect_flags("elf64-rwe", buf, sz64, IS_ELF | HAS_EXEC | HAS_LOAD | HAS_RWE_LOAD); size_t shebang_len = sizeof(shebang) - 1; expect_flags("shebang", shebang, shebang_len, HAS_SHEBANG); size_t text_len = sizeof(text_script) - 1; expect_flags("text-script", text_script, text_len, TEXT_SCRIPT); size_t bad32 = make_truncated32(trunc_buf); expect_flags("truncated32", trunc_buf, bad32, IS_ELF | HAS_ERROR); size_t head64 = make_elf64_header_only(buf, 4); expect_flags("oversized-ph", buf, head64, IS_ELF | HAS_EXEC | HAS_ERROR); return 0; } fapolicyd-2.0/src/tests/escape_test.c000066400000000000000000000106351524071644000177310ustar00rootroot00000000000000/* * escape_test.c - tests for shell escaping helpers */ #include "escape.h" #include #include #include #include int main(void) { char *tmp; size_t sz; /* check_escape_shell */ sz = check_escape_shell("plain"); if (sz != 0) { fprintf(stderr, "[ERROR:1] plain input %zu\n", sz); return 1; } sz = check_escape_shell("a b"); if (sz != 4) { fprintf(stderr, "[ERROR:1] space %zu\n", sz); return 1; } sz = check_escape_shell("a$b"); if (sz != 4) { fprintf(stderr, "[ERROR:1] metachar %zu\n", sz); return 1; } sz = check_escape_shell("a\nb"); if (sz != 6) { fprintf(stderr, "[ERROR:1] control %zu\n", sz); return 1; } const char high_bit[] = { (char)0x80, ' ', '\0' }; sz = check_escape_shell(high_bit); if (sz != 3) { fprintf(stderr, "[ERROR:1] high-bit byte %zu\n", sz); return 1; } /* escape_shell */ tmp = escape_shell(NULL, 0); if (tmp) { fprintf(stderr, "[ERROR:2] NULL input\n"); free(tmp); return 2; } char big_in[8192]; strcpy(big_in, "abc"); tmp = escape_shell(big_in, 8192); if (tmp) { fprintf(stderr, "[ERROR:2] size check\n"); free(tmp); return 2; } sz = check_escape_shell("a b"); tmp = escape_shell("a b", sz); if (!tmp) { fprintf(stderr, "[ERROR:2] escape_shell failed\n"); return 2; } if (strcmp(tmp, "a\\ b")) { fprintf(stderr, "[ERROR:2] escaped '%s'\n", tmp); free(tmp); return 2; } free(tmp); tmp = escape_shell("a b", sz - 1); if (tmp) { fprintf(stderr, "[ERROR:2] accepted undersized output\n"); free(tmp); return 2; } tmp = escape_shell("a b", sz + 1); if (tmp) { fprintf(stderr, "[ERROR:2] accepted oversized output\n"); free(tmp); return 2; } const char high_bit_escaped[] = { (char)0x80, '\\', ' ', '\0' }; sz = check_escape_shell(high_bit); tmp = escape_shell(high_bit, sz); if (!tmp || memcmp(tmp, high_bit_escaped, sizeof(high_bit_escaped))) { fprintf(stderr, "[ERROR:2] high-bit escape failed\n"); free(tmp); return 2; } free(tmp); char max_in[8191]; memset(max_in, 'a', sizeof(max_in)); max_in[sizeof(max_in) - 2] = ' '; max_in[sizeof(max_in) - 1] = '\0'; sz = check_escape_shell(max_in); if (sz != 8191) { fprintf(stderr, "[ERROR:2] maximum size %zu\n", sz); return 2; } tmp = escape_shell(max_in, sz); if (!tmp || strlen(tmp) != sz) { fprintf(stderr, "[ERROR:2] maximum escape failed\n"); free(tmp); return 2; } free(tmp); char over_in[8192]; memset(over_in, 'a', sizeof(over_in)); over_in[sizeof(over_in) - 2] = ' '; over_in[sizeof(over_in) - 1] = '\0'; sz = check_escape_shell(over_in); tmp = escape_shell(over_in, sz); if (sz != 8192 || tmp) { fprintf(stderr, "[ERROR:2] accepted maximum size\n"); free(tmp); return 2; } /* unescape_shell */ char buf1[] = "\\040\\$"; if (unescape_shell(buf1, sizeof(buf1))) { fprintf(stderr, "[ERROR:3] unescape_shell failed\n"); return 3; } if (strcmp(buf1, " $")) { fprintf(stderr, "[ERROR:3] unescape_shell octal '%s'\n", buf1); return 3; } char buf2[] = "abc\\"; if (unescape_shell(buf2, sizeof(buf2))) { fprintf(stderr, "[ERROR:3] trailing escape failed\n"); return 3; } if (strcmp(buf2, "abc\\")) { fprintf(stderr, "[ERROR:3] trailing '%s'\n", buf2); return 3; } char buf3[] = "abc\\0"; if (unescape_shell(buf3, sizeof(buf3))) { fprintf(stderr, "[ERROR:3] malformed escape failed\n"); return 3; } if (strcmp(buf3, "abc\\0")) { fprintf(stderr, "[ERROR:3] malformed '%s'\n", buf3); return 3; } char buf4[] = { 'a', 'b', 'c' }; if (unescape_shell(buf4, sizeof(buf4)) == 0 || memcmp(buf4, "abc", sizeof(buf4))) { fprintf(stderr, "[ERROR:3] accepted unterminated input\n"); return 3; } char buf5 = 'x'; if (unescape_shell(&buf5, 0) == 0 || buf5 != 'x') { fprintf(stderr, "[ERROR:3] accepted zero capacity\n"); return 3; } /* unescape */ tmp = unescape("%41%42"); if (!tmp || strcmp(tmp, "AB")) { fprintf(stderr, "[ERROR:4] unescape valid\n"); free(tmp); return 4; } free(tmp); tmp = unescape("%4"); if (!tmp || strcmp(tmp, "%4")) { fprintf(stderr, "[ERROR:4] unescape short\n"); free(tmp); return 4; } free(tmp); tmp = unescape("%GG"); if (!tmp || strcmp(tmp, "%GG")) { fprintf(stderr, "[ERROR:4] unescape invalid\n"); free(tmp); return 4; } free(tmp); char big[4097 + 1]; memset(big, 'A', sizeof(big)); big[sizeof(big) - 1] = '\0'; tmp = unescape(big); if (tmp) { fprintf(stderr, "[ERROR:4] unescape big\n"); free(tmp); return 4; } return 0; } fapolicyd-2.0/src/tests/event_test.c000066400000000000000000001064521524071644000176150ustar00rootroot00000000000000/* * event_test.c - unit tests for new_event subject/object cache behavior */ #include #include #include #include #include #include "attr-lookup-metrics.h" #include "event.h" #include "decision-context.h" #include "conf.h" #include "process.h" #include "object.h" #include "subject.h" #include "fapolicyd-backend.h" /* * Test doubles * ------------ * The tests below replace the process and file fingerprint helpers used by * new_event(). Each stub returns deterministic data so we can control cache * reuse and eviction without touching /proc or real file descriptors. */ /* * Test strategy * ------------- * The fixtures configure small, deterministic caches so that each test can * exercise a specific branch inside new_event(). The helpers below provide * stable process and file identities which lets us trigger subject cache * reuse, deliberate evictions, and skip-path behavior without relying on * kernel state. Each scenario asserts the resulting event_t contents as well * as the cache side effects (state transitions and cache pointer reuse). * * Extending the suite is straightforward: add new rows to the stub tables or * new helper routines that model additional metadata, then write another test * that seeds fanotify_event_metadata with the desired pid/fd pair. Tests can * reuse init_caches() to size caches appropriately and the CHECK macro to * report deterministic failures. Future scenarios to consider include * multi-object fanotify events, cross-context cache flush generations, or * validating that trust-database results propagate into the event fields. */ struct proc_info *stat_proc_entry(pid_t pid); void clear_proc_info(struct proc_info *info); int compare_proc_infos(const struct proc_info *p1, const struct proc_info *p2); struct file_info *stat_file_entry(int fd); int compare_file_infos(const struct file_info *p1, const struct file_info *p2); char *get_file_from_fd(int fd, pid_t pid, size_t blen, char *buf); uint32_t gather_elf(int fd, off_t size); void msg(int priority, const char *fmt, ...); unsigned int policy_get_rules_proc_status_mask(void); unsigned int policy_get_syslog_proc_status_mask(void); int read_proc_status(pid_t pid, unsigned int fields, struct proc_status_info *info); char *get_program_from_pid(pid_t pid, size_t blen, char *buf); char *get_type_from_pid(pid_t pid, size_t blen, char *buf); uid_t get_program_auid_from_pid(pid_t pid); int get_program_sessionid_from_pid(pid_t pid); int check_trust_database(const char *exe, const char *digest, int mode); char *get_device_from_stat(unsigned int device, size_t blen, char *buf); char *get_file_type_from_fd(int fd, const struct file_info *i, const char *path, size_t blen, char *buf); char *get_hash_from_fd2(int fd, size_t size, file_hash_alg_t alg); struct stub_proc_record { pid_t pid; dev_t device; ino_t inode; long nsec; }; static const struct stub_proc_record proc_table[] = { { 100, 1, 111, 100 }, { 200, 2, 222, 200 }, { 201, 3, 333, 300 }, { 202, 4, 444, 400 }, { 300, 5, 555, 500 }, { 301, 6, 666, 600 }, { 400, 7, 777, 700 }, }; struct stub_file_record { int fd; dev_t device; ino_t inode; off_t size; long nsec; const char *path; }; static const struct stub_file_record file_table[] = { { 10, 11, 1010, 4096, 101, "/stub/bin/first" }, { 11, 12, 1111, 4096, 111, "/stub/bin/first-open" }, { 20, 21, 2020, 2048, 202, "/stub/bin/second" }, { 21, 22, 2121, 1024, 212, "/stub/bin/third" }, { 30, 31, 3030, 512, 303, "/stub/bin/fourth" }, { 31, 32, 3131, 512, 313, "/stub/bin/fifth" }, { 40, 41, 4040, 256, 404, "/stub/bin/sixth" }, }; static pid_t traced_proc_pid = -1; /* --- Stub implementations ------------------------------------------------ */ /* * Locate the stubbed proc entry for the given pid or NULL when missing. */ static const struct stub_proc_record *find_proc(pid_t pid) { size_t i; for (i = 0; i < sizeof(proc_table)/sizeof(proc_table[0]); i++) if (proc_table[i].pid == pid) return &proc_table[i]; return NULL; } /* * Locate the stubbed file entry for the given descriptor or NULL when absent. */ static const struct stub_file_record *find_file(int fd) { size_t i; for (i = 0; i < sizeof(file_table)/sizeof(file_table[0]); i++) if (file_table[i].fd == fd) return &file_table[i]; return NULL; } /* * file_hash_length - return digest lengths for the unit test stubs. * @alg: digest algorithm requested by the event code under test. * Returns a constant binary length for the supported algorithms. */ size_t file_hash_length(file_hash_alg_t alg) { switch (alg) { case FILE_HASH_ALG_SHA1: return SHA1_LEN; case FILE_HASH_ALG_SHA256: return SHA256_LEN; case FILE_HASH_ALG_SHA512: return SHA512_LEN; case FILE_HASH_ALG_MD5: return 16; default: return 0; } } file_hash_alg_t file_hash_alg(unsigned len) { switch (len) { case MD5_LEN * 2: return FILE_HASH_ALG_MD5; case SHA1_LEN * 2: return FILE_HASH_ALG_SHA1; case SHA256_LEN * 2: return FILE_HASH_ALG_SHA256; case SHA512_LEN * 2: return FILE_HASH_ALG_SHA512; default: return FILE_HASH_ALG_NONE; } } /* * file_info_reset_digest - clear cached digest metadata for unit tests. * @info: cached file entry supplied by the test harness. */ void file_info_reset_digest(struct file_info *info) { if (info == NULL) return; info->digest_alg = FILE_HASH_ALG_NONE; info->digest[0] = 0; } /* * file_info_cache_digest - store digest metadata for unit test file entries. * @info: cached file entry supplied by the test harness. * @alg: algorithm associated with the cached digest string. * Tests derive digest length with file_hash_length(@alg) when necessary. */ void file_info_cache_digest(struct file_info *info, file_hash_alg_t alg) { if (info == NULL) return; info->digest_alg = alg; } /* * Allocate a proc_info populated from the stub table, emulating /proc stats. */ struct proc_info *stat_proc_entry(pid_t pid) { const struct stub_proc_record *rec = find_proc(pid); struct proc_info *info; if (rec == NULL) return NULL; info = malloc(sizeof(*info)); if (info == NULL) return NULL; info->pid = rec->pid; info->device = rec->device; info->inode = rec->inode; info->time.tv_sec = 0; info->time.tv_nsec = rec->nsec; info->state = STATE_COLLECTING; info->path1 = NULL; info->path2 = NULL; info->building_started_ns = 0; info->building_event_count = 0; info->elf_info = 0; return info; } /* * Release any heap-allocated strings contained inside the stub proc_info. */ void clear_proc_info(struct proc_info *info) { if (info == NULL) return; free(info->path1); free(info->path2); info->path1 = NULL; info->path2 = NULL; } /* * Provide the equality predicate required by the subject cache machinery. */ int compare_proc_infos(const struct proc_info *p1, const struct proc_info *p2) { if (p1 == NULL || p2 == NULL) return 1; if (p1->pid != p2->pid) return 1; if (p1->device != p2->device) return 1; if (p1->inode != p2->inode) return 1; if (p1->time.tv_sec != p2->time.tv_sec) return 1; if (p1->time.tv_nsec != p2->time.tv_nsec) return 1; return 0; } /* * Allocate a file_info populated from the stub table for the supplied fd. */ struct file_info *stat_file_entry(int fd) { const struct stub_file_record *rec = find_file(fd); struct file_info *info; if (rec == NULL) return NULL; info = malloc(sizeof(*info)); if (info == NULL) return NULL; info->device = rec->device; info->inode = rec->inode; info->mode = 0; info->size = rec->size; info->time.tv_sec = 0; info->time.tv_nsec = rec->nsec; file_info_reset_digest(info); return info; } /* * Implement the object cache equality predicate using stub file metadata. */ int compare_file_infos(const struct file_info *p1, const struct file_info *p2) { if (p1 == NULL || p2 == NULL) return 1; if (p1->device != p2->device) return 1; if (p1->inode != p2->inode) return 1; if (p1->size != p2->size) return 1; if (p1->time.tv_sec != p2->time.tv_sec) return 1; if (p1->time.tv_nsec != p2->time.tv_nsec) return 1; return 0; } /* * Return a synthetic path for the provided fd so path collection can succeed. */ char *get_file_from_fd(int fd, pid_t pid, size_t blen, char *buf) { const struct stub_file_record *rec = find_file(fd); (void)pid; if (rec == NULL) return NULL; if (strlen(rec->path) + 1 > blen) return NULL; strcpy(buf, rec->path); return buf; } /* * Produce a deterministic ELF signature based on the stubbed fd and size. */ uint32_t gather_elf(int fd, off_t size) { return ((uint32_t)fd << 8) ^ (uint32_t)size; } /* * Stub out the logging hook invoked by new_event(); nothing to record here. */ void msg(int priority, const char *fmt, ...) { (void)priority; (void)fmt; } /* Return zero to disable reading of /proc status fields during tests. */ unsigned int policy_get_rules_proc_status_mask(void) { return 0; } /* Avoid requesting additional /proc status fields in this isolated harness. */ unsigned int policy_get_syslog_proc_status_mask(void) { return 0; } /* Return zero for the combined policy proc-status field request. */ unsigned int policy_get_proc_status_mask(void) { return 0; } /* * Provide an inert implementation for read_proc_status() that always succeeds. */ int read_proc_status(pid_t pid, unsigned int fields, struct proc_status_info *info) { (void)pid; if (info == NULL) return -1; info->ppid = -1; if (fields & PROC_STAT_TRACER) info->tracer_state = pid == traced_proc_pid ? PROC_TRACER_TRACED : PROC_TRACER_NOT_TRACED; else info->tracer_state = PROC_TRACER_UNKNOWN; info->uid = NULL; info->groups = NULL; info->comm = NULL; return 0; } /* * Fabricate a program path based on pid so subject attributes remain stable. */ char *get_program_from_pid(pid_t pid, size_t blen, char *buf) { if (snprintf(buf, blen, "/proc/%d/exe", pid) < 0) return NULL; return buf; } /* Fabricate a subject type string that is unique per pid. */ char *get_type_from_pid(pid_t pid, size_t blen, char *buf) { if (snprintf(buf, blen, "type-%d", pid) < 0) return NULL; return buf; } /* Return a deterministic audit uid derived from the pid. */ uid_t get_program_auid_from_pid(pid_t pid) { return (uid_t)pid; } /* Return a deterministic session id derived from the pid. */ int get_program_sessionid_from_pid(pid_t pid) { return (int)pid; } /* Bypass trust database lookups while keeping the signature intact. */ int check_trust_database(const char *exe, const char *digest, int mode) { (void)exe; (void)digest; (void)mode; return 0; } /* * Report a stringified device identifier to satisfy object attribute updates. */ char *get_device_from_stat(unsigned int device, size_t blen, char *buf) { if (snprintf(buf, blen, "dev-%u", device) < 0) return NULL; return buf; } /* * Provide a deterministic object type string incorporating the fd and path. */ char *get_file_type_from_fd(int fd, const struct file_info *i, const char *path, size_t blen, char *buf) { (void)i; if (snprintf(buf, blen, "ftype-%d-%s", fd, path ? path : "?") < 0) return NULL; return buf; } /* * Produce a fake digest string so new_event() can populate hash attributes. */ char *get_hash_from_fd2(int fd, size_t size, file_hash_alg_t alg) { char *out = malloc(64); if (out == NULL) return NULL; snprintf(out, 64, "hash-%d-%zu-%d", fd, (size_t)size, (int)alg); return out; } /* --- Test helpers -------------------------------------------------------- */ #define CHECK(cond, code, msg) \ do { \ if (!(cond)) { \ fprintf(stderr, "%s\n", msg); \ return code; \ } \ } while (0) struct metric_expectation { unsigned long long requests; unsigned long long lookups; }; /* * reset_attr_lookup_metrics - clear all attribute lookup counters. * Returns nothing. */ static void reset_attr_lookup_metrics(void) { struct attr_lookup_metric_snapshot snapshot; unsigned int type; for (type = SUBJ_START; type <= SUBJ_END; type++) attr_lookup_metrics_subject_snapshot(type, &snapshot, 1); for (type = OBJ_START; type <= OBJ_END; type++) attr_lookup_metrics_object_snapshot(type, &snapshot, 1); } /* * check_subject_metric - assert one subject attribute metric snapshot. * @type: subject attribute to inspect. * @expected: expected request and lookup counts. * @code: test failure code. * Return codes: * 0 - counters match. * @code - counters differ or snapshot failed. */ static int check_subject_metric(subject_type_t type, const struct metric_expectation *expected, int code) { struct attr_lookup_metric_snapshot snapshot; const char *name = subj_val_to_name(type, RULE_FMT_COLON); if (attr_lookup_metrics_subject_snapshot(type, &snapshot, 0)) { fprintf(stderr, "[ERROR:%d] subject metric snapshot failed\n", code); return code; } if (snapshot.requests != expected->requests || snapshot.lookups != expected->lookups) { fprintf(stderr, "[ERROR:%d] subject %s metrics %llu/%llu != %llu/%llu\n", code, name ? name : "unknown", snapshot.requests, snapshot.lookups, expected->requests, expected->lookups); return code; } return 0; } /* * check_object_metric - assert one object attribute metric snapshot. * @type: object attribute to inspect. * @expected: expected request and lookup counts. * @code: test failure code. * Return codes: * 0 - counters match. * @code - counters differ or snapshot failed. */ static int check_object_metric(object_type_t type, const struct metric_expectation *expected, int code) { struct attr_lookup_metric_snapshot snapshot; const char *name = obj_val_to_name(type); if (attr_lookup_metrics_object_snapshot(type, &snapshot, 0)) { fprintf(stderr, "[ERROR:%d] object metric snapshot failed\n", code); return code; } if (snapshot.requests != expected->requests || snapshot.lookups != expected->lookups) { fprintf(stderr, "[ERROR:%d] object %s metrics %llu/%llu != %llu/%llu\n", code, name ? name : "unknown", snapshot.requests, snapshot.lookups, expected->requests, expected->lookups); return code; } return 0; } /* * check_attr_metric_report_line - verify aligned attribute metric output. * @needle: expected report line. * @code: test failure code. * Return codes: * 0 - report contains @needle. * @code - report generation failed or @needle is absent. */ static int check_attr_metric_report_line(const char *needle, int code) { char report_buf[2048]; FILE *report; size_t used; report = tmpfile(); if (report == NULL) { fprintf(stderr, "[ERROR:%d] tmpfile failed\n", code); return code; } attr_lookup_metrics_report(report, 0); fflush(report); rewind(report); used = fread(report_buf, 1, sizeof(report_buf) - 1, report); report_buf[used] = 0; fclose(report); if (strstr(report_buf, needle) == NULL) { fprintf(stderr, "[ERROR:%d] missing metric line: %s\n", code, needle); return code; } return 0; } struct lmdb_record { unsigned int tsource; off_t size; char digest[FILE_DIGEST_STRING_MAX]; size_t digest_len; file_hash_alg_t alg; }; static int parse_record(const char *record, struct lmdb_record *parsed) { size_t expected_len; trustdb_size_t stored_size; if (sscanf(record, DATA_FORMAT_IN, &parsed->tsource, &stored_size, parsed->digest) != 3 || trustdb_size_to_off_t(stored_size, &parsed->size)) return 1; parsed->digest_len = strlen(parsed->digest); parsed->alg = file_hash_alg(parsed->digest_len); expected_len = file_hash_length(parsed->alg) * 2; if (expected_len == 0 || parsed->digest_len != expected_len) return 2; if (parsed->tsource != SRC_RPM && parsed->alg != FILE_HASH_ALG_SHA256) return 2; return 0; } static int test_rpm_accepts_sha512(void) { struct lmdb_record parsed; char record[256]; const char *sha512 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; snprintf(record, sizeof(record), DATA_FORMAT, (unsigned int)SRC_RPM, UINT64_C(8192), sha512); CHECK(parse_record(record, &parsed) == 0, 40, "[ERROR:40] parse failed for RPM SHA512 digest"); CHECK(parsed.alg == FILE_HASH_ALG_SHA512, 41, "[ERROR:41] RPM digest algorithm not inferred as SHA512"); CHECK(parsed.digest_len == strlen(sha512), 42, "[ERROR:42] RPM digest length not preserved"); return 0; } static int test_filedb_rejects_sha512(void) { struct lmdb_record parsed; char record[256]; const char *sha512 = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; snprintf(record, sizeof(record), DATA_FORMAT, (unsigned int)SRC_FILE_DB, UINT64_C(4096), sha512); CHECK(parse_record(record, &parsed) != 0, 50, "[ERROR:50] filedb SHA512 digest unexpectedly accepted"); return 0; } static int test_filedb_accepts_sha256(void) { struct lmdb_record parsed; char record[256]; const char *sha256 = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; snprintf(record, sizeof(record), DATA_FORMAT, (unsigned int)SRC_FILE_DB, UINT64_C(1024), sha256); CHECK(parse_record(record, &parsed) == 0, 60, "[ERROR:60] filedb SHA256 digest rejected"); CHECK(parsed.alg == FILE_HASH_ALG_SHA256, 61, "[ERROR:61] filedb digest algorithm not forced to SHA256"); return 0; } static int init_caches(unsigned int subj_size, unsigned int obj_size) { conf_t cfg = (conf_t){ 0 }; cfg.subj_cache_size = subj_size; cfg.obj_cache_size = obj_size; return init_event_system(&cfg); } /* * test_attribute_replacement_preserves_count - verify subject/object array * replacement keeps cnt as an occupied-slot count. * Returns 0 on success, test error code otherwise. */ static int test_attribute_replacement_preserves_count(void) { s_array subjects; o_array objects; subject_attr_t old_exe = { .type = EXE, .str = strdup("/old-exe") }; subject_attr_t new_exe = { .type = EXE, .str = strdup("/new-exe") }; object_attr_t old_path = { .type = PATH, .o = strdup("/old-path") }; object_attr_t new_path = { .type = PATH, .o = strdup("/new-path") }; subject_attr_t *cached_subject; object_attr_t *cached_object; CHECK(old_exe.str && new_exe.str && old_path.o && new_path.o, 130, "[ERROR:130] failed to allocate replacement attributes"); CHECK(subject_create(&subjects) == 0, 131, "[ERROR:131] failed to allocate subject array"); CHECK(object_create(&objects) == 0, 132, "[ERROR:132] failed to allocate object array"); CHECK(subject_add(&subjects, &old_exe) == 0, 133, "[ERROR:133] failed to add original subject attribute"); CHECK(subject_add(&subjects, &new_exe) == 0, 134, "[ERROR:134] failed to replace subject attribute"); CHECK(subjects.cnt == 1, 135, "[ERROR:135] subject replacement changed slot count"); cached_subject = subject_access(&subjects, EXE); CHECK(cached_subject != NULL, 136, "[ERROR:136] replacement subject attribute missing"); CHECK(strcmp(cached_subject->str, "/new-exe") == 0, 137, "[ERROR:137] replacement subject attribute not cached"); CHECK(object_add(&objects, &old_path) == 0, 138, "[ERROR:138] failed to add original object attribute"); CHECK(object_add(&objects, &new_path) == 0, 139, "[ERROR:139] failed to replace object attribute"); CHECK(objects.cnt == 1, 140, "[ERROR:140] object replacement changed slot count"); cached_object = object_access(&objects, PATH); CHECK(cached_object != NULL, 141, "[ERROR:141] replacement object attribute missing"); CHECK(strcmp(cached_object->o, "/new-path") == 0, 142, "[ERROR:142] replacement object attribute not cached"); subject_clear(&subjects); object_clear(&objects); return 0; } /* * test_attr_lookup_metrics - verify request, miss, dependency, and reset data. * Returns 0 on success, test error code otherwise. */ static int test_attr_lookup_metrics(void) { event_t e = { 0 }; s_array subjects; o_array objects; struct attr_lookup_metric_snapshot snapshot; subject_attr_t pid_attr = { .type = PID, .pid = 123 }; struct metric_expectation expected; int rc; reset_attr_lookup_metrics(); CHECK(attr_lookup_metrics_subject_snapshot(PATTERN, &snapshot, 0), 99, "[ERROR:99] pattern should not have attr metrics"); CHECK(subject_create(&subjects) == 0, 100, "[ERROR:100] failed to allocate subject array"); CHECK(object_create(&objects) == 0, 101, "[ERROR:101] failed to allocate object array"); e.pid = 123; e.fd = 10; e.s = &subjects; e.o = &objects; e.o->info = stat_file_entry(e.fd); CHECK(e.o->info != NULL, 102, "[ERROR:102] failed to allocate test file info"); CHECK(subject_add(e.s, &pid_attr) == 0, 103, "[ERROR:103] failed to add cached pid"); CHECK(get_subj_attr(&e, PID) != NULL, 104, "[ERROR:104] cached pid lookup failed"); CHECK(get_subj_attr(&e, EXE) != NULL, 105, "[ERROR:105] exe lookup failed"); CHECK(get_subj_attr(&e, EXE) != NULL, 106, "[ERROR:106] cached exe lookup failed"); CHECK(get_subj_attr(&e, SUBJ_TRUST) != NULL, 107, "[ERROR:107] subject trust lookup failed"); CHECK(get_obj_attr(&e, PATH) != NULL, 108, "[ERROR:108] path lookup failed"); CHECK(get_obj_attr(&e, PATH) != NULL, 109, "[ERROR:109] cached path lookup failed"); CHECK(get_obj_attr(&e, FTYPE) != NULL, 110, "[ERROR:110] ftype lookup failed"); expected = (struct metric_expectation){ 1, 0 }; rc = check_subject_metric(PID, &expected, 111); if (rc) return rc; expected = (struct metric_expectation){ 3, 1 }; rc = check_subject_metric(EXE, &expected, 112); if (rc) return rc; expected = (struct metric_expectation){ 1, 1 }; rc = check_subject_metric(SUBJ_TRUST, &expected, 113); if (rc) return rc; expected = (struct metric_expectation){ 3, 1 }; rc = check_object_metric(PATH, &expected, 114); if (rc) return rc; expected = (struct metric_expectation){ 1, 1 }; rc = check_object_metric(FTYPE, &expected, 115); if (rc) return rc; rc = check_attr_metric_report_line( "Subject attr: pid requests=1" " lookups=0\n", 118); if (rc) return rc; rc = check_attr_metric_report_line( "Object attr: path requests=3" " lookups=1\n", 119); if (rc) return rc; reset_attr_lookup_metrics(); expected = (struct metric_expectation){ 0, 0 }; rc = check_subject_metric(EXE, &expected, 116); if (rc) return rc; rc = check_object_metric(PATH, &expected, 117); if (rc) return rc; subject_clear(&subjects); object_clear(&objects); return 0; } /* * read_cache_report_line - report one cache counter from event.c. * @label: printable counter label from do_cache_reports(). * @value: output location for the parsed counter. * * Returns 0 when @value is populated and 1 on parse or I/O failure. */ static int read_cache_report_line(const char *label, unsigned long *value) { FILE *report; char line[256]; char pattern[96]; unsigned long parsed; if (label == NULL || value == NULL) return 1; report = tmpfile(); if (report == NULL) return 1; do_cache_reports(report); rewind(report); snprintf(pattern, sizeof(pattern), "%s: %%lu", label); while (fgets(line, sizeof(line), report)) { if (sscanf(line, pattern, &parsed) == 1) { fclose(report); *value = parsed; return 0; } } fclose(report); return 1; } /* * reset_cache_report_counters - clear interval counters in event.c reports. * Returns 0 on success and 1 on I/O failure. */ static int reset_cache_report_counters(void) { FILE *report = tmpfile(); if (report == NULL) return 1; do_cache_reports_reset(report, 1); fclose(report); return 0; } /* * cache_report_contains - check whether do_cache_reports() emits text. * @needle: text expected in the cache report. * Returns 1 when found, 0 otherwise. */ static int cache_report_contains(const char *needle) { FILE *report; char buf[4096]; size_t used; if (needle == NULL) return 0; report = tmpfile(); if (report == NULL) return 0; do_cache_reports(report); fflush(report); rewind(report); used = fread(buf, 1, sizeof(buf) - 1, report); buf[used] = 0; fclose(report); return strstr(buf, needle) != NULL; } /* * read_object_cache_metric - report an object-cache counter from event.c. * @metric: printable counter label from do_cache_reports(), e.g. "hits". * @value: output location for the parsed counter. * * The helper captures do_cache_reports() into a temporary stream and extracts * a single "Object : " line for assertions. * * Returns 0 when @value is populated and 1 on parse or I/O failure. */ static int read_object_cache_metric(const char *metric, unsigned long *value) { char label[64]; if (metric == NULL || value == NULL) return 1; snprintf(label, sizeof(label), "Object %s", metric); return read_cache_report_line(label, value); } /* * Verify that a second FAN_OPEN_PERM event for the same pid reuses the cached * subject, transitions STATE_COLLECTING -> STATE_REOPEN, and skips path * collection. */ static int test_reopen_skip_path(void) { struct fanotify_event_metadata meta = { 0 }; event_t first = { 0 }; event_t reopen = { 0 }; object_attr_t *path; object_attr_t *digest; CHECK(init_caches(4, 4) == 0, 1, "[ERROR:1] init_event_system failed"); meta.mask = FAN_OPEN_EXEC_PERM; meta.fd = 10; meta.pid = 100; CHECK(new_event(&meta, &first) == 0, 2, "[ERROR:2] first new_event failed"); CHECK(first.pid == 100, 3, "[ERROR:3] pid not copied"); CHECK(first.fd == 10, 4, "[ERROR:4] fd not copied"); CHECK((first.type & FAN_OPEN_EXEC_PERM) != 0, 5, "[ERROR:5] mask missing FAN_OPEN_EXEC_PERM"); CHECK(first.s && first.s->info, 6, "[ERROR:6] missing subject info"); CHECK(first.o && first.o->info, 7, "[ERROR:7] missing object info"); path = object_access(first.o, PATH); CHECK(path != NULL, 8, "[ERROR:8] path attribute missing"); CHECK(strcmp(path->o, "/stub/bin/first") == 0, 9, "[ERROR:9] unexpected path1"); CHECK(first.s->info->path1 && strcmp(first.s->info->path1, "/stub/bin/first") == 0, 10, "[ERROR:10] subject path1 not captured"); CHECK(first.s->info->state == STATE_COLLECTING, 11, "[ERROR:11] initial state mutated"); digest = get_obj_attr(&first, FILE_HASH); CHECK(digest != NULL, 17, "[ERROR:17] missing digest attribute"); CHECK(first.o->info->digest_alg == FILE_HASH_ALG_SHA256, 18, "[ERROR:18] digest algorithm not cached as SHA256"); meta.mask = FAN_OPEN_PERM; CHECK(new_event(&meta, &reopen) == 0, 12, "[ERROR:12] reopen new_event failed"); CHECK(reopen.s == first.s, 13, "[ERROR:13] subject cache miss"); CHECK(reopen.o == first.o, 14, "[ERROR:14] object cache miss"); CHECK(reopen.s->info->state == STATE_REOPEN, 15, "[ERROR:15] state did not transition to STATE_REOPEN"); CHECK(reopen.s->info->path2 == NULL, 16, "[ERROR:16] path2 collected despite skip_path"); destroy_event_system(); return 0; } /* * Ensure that a tiny subject cache evicts the previous entry when a different * pid hashes to the same slot. */ static int test_subject_eviction(void) { struct fanotify_event_metadata meta = { 0 }; event_t first = { 0 }; event_t second = { 0 }; int first_pid; CHECK(init_caches(1, 2) == 0, 20, "[ERROR:20] init_event_system failed"); meta.mask = FAN_OPEN_EXEC_PERM; meta.fd = 30; meta.pid = 300; CHECK(new_event(&meta, &first) == 0, 21, "[ERROR:21] first new_event failed"); CHECK(first.s && first.s->info, 22, "[ERROR:22] subject missing"); first_pid = first.s->info->pid; meta.fd = 31; meta.pid = 301; CHECK(new_event(&meta, &second) == 0, 23, "[ERROR:23] second new_event failed"); CHECK(second.s && second.s->info, 24, "[ERROR:24] subject info missing after eviction"); CHECK(second.s->info->pid != first_pid, 25, "[ERROR:25] subject cache did not evict prior entry"); destroy_event_system(); return 0; } /* * Verify that a different pid colliding with a pre-STATE_FULL occupant blocks * instead of evicting the occupant, while same-pid and terminal occupants do * not block. */ static int test_subject_slot_blocks_pre_full_collision(void) { struct fanotify_event_metadata meta = { 0 }; event_t first = { 0 }; unsigned int slot; CHECK(init_caches(1, 2) == 0, 70, "[ERROR:70] init_event_system failed"); meta.mask = FAN_OPEN_EXEC_PERM; meta.fd = 30; meta.pid = 300; CHECK(new_event(&meta, &first) == 0, 71, "[ERROR:71] first new_event failed"); CHECK(first.s && first.s->info, 72, "[ERROR:72] subject missing"); CHECK(first.s->info->state < STATE_FULL, 73, "[ERROR:73] subject unexpectedly reached terminal state"); slot = event_subject_slot(301); CHECK(slot == event_subject_slot(300), 74, "[ERROR:74] tiny cache did not force a slot collision"); CHECK(event_subject_slot_is_blocked(slot, 301) == 1, 75, "[ERROR:75] colliding BUILDING subject did not block"); CHECK(first.s->info->pid == 300, 76, "[ERROR:76] blocked collision evicted the occupant"); CHECK(event_subject_slot_is_blocked(slot, 300) == 0, 77, "[ERROR:77] same pid should not block itself"); CHECK(event_subject_slot_is_unblocked(slot) == 0, 78, "[ERROR:78] pre-FULL subject reported unblocked"); first.s->info->state = STATE_FULL; CHECK(event_subject_slot_is_blocked(slot, 301) == 0, 79, "[ERROR:79] STATE_FULL subject still blocked collision"); CHECK(event_subject_slot_is_unblocked(slot) == 1, 80, "[ERROR:80] STATE_FULL subject did not release slot"); destroy_event_system(); return 0; } /* * Verify that a traced BUILDING occupant is evicted instead of deferring * incoming work indefinitely, and that the eviction is counted. */ static int test_traced_building_occupant_eviction(void) { struct fanotify_event_metadata meta = { 0 }; event_t first = { 0 }; unsigned long tracer_evictions = 0; unsigned int slot; CHECK(init_caches(1, 2) == 0, 90, "[ERROR:90] init_event_system failed"); CHECK(reset_cache_report_counters() == 0, 91, "[ERROR:91] failed resetting cache report counters"); meta.mask = FAN_OPEN_EXEC_PERM; meta.fd = 30; meta.pid = 300; CHECK(new_event(&meta, &first) == 0, 92, "[ERROR:92] first new_event failed"); CHECK(first.s && first.s->info, 93, "[ERROR:93] subject missing"); slot = event_subject_slot(301); traced_proc_pid = 300; CHECK(event_subject_slot_is_blocked(slot, 301) == 0, 94, "[ERROR:94] traced BUILDING occupant blocked collision"); traced_proc_pid = -1; CHECK(event_subject_slot_is_unblocked(slot) == 1, 95, "[ERROR:95] traced BUILDING occupant was not evicted"); CHECK(read_cache_report_line("Subject BUILDING tracer evictions", &tracer_evictions) == 0, 96, "[ERROR:96] failed reading tracer eviction counter"); CHECK(tracer_evictions == 1, 97, "[ERROR:97] tracer eviction counter mismatch"); destroy_event_system(); return 0; } /* * Verify that a flush generation resets the object cache so the next lookup * allocates a fresh entry. */ static int test_flush_generation_resets_object_cache(void) { struct fanotify_event_metadata meta = { 0 }; event_t first = { 0 }; event_t second = { 0 }; unsigned long object_hits = 0, object_misses = 0; unsigned long generation; CHECK(init_caches(4, 1) == 0, 30, "[ERROR:30] init_event_system failed"); meta.mask = FAN_OPEN_EXEC_PERM; meta.fd = 40; meta.pid = 400; CHECK(new_event(&meta, &first) == 0, 31, "[ERROR:31] first new_event failed"); CHECK(first.o != NULL, 32, "[ERROR:32] object missing"); generation = object_cache_flush_generation_snapshot(); CHECK(request_object_cache_flush() == generation + 1, 34, "[ERROR:34] flush generation did not advance"); meta.mask = FAN_OPEN_PERM; CHECK(new_event(&meta, &second) == 0, 33, "[ERROR:33] second new_event failed"); CHECK(second.s == first.s, 35, "[ERROR:35] subject cache should reuse same entry"); CHECK(read_object_cache_metric("hits", &object_hits) == 0, 36, "[ERROR:36] failed reading object cache hits"); CHECK(read_object_cache_metric("misses", &object_misses) == 0, 37, "[ERROR:37] failed reading object cache misses"); CHECK(object_hits == 0, 38, "[ERROR:38] object cache flush should reset hit counter"); CHECK(object_misses == 1, 39, "[ERROR:39] object cache flush should force a cache miss"); destroy_event_system(); return 0; } /* * Verify that one trust DB update flushes every registered worker cache. */ static int test_flush_generation_reaches_all_contexts(void) { conf_t cfg = { 0 }; struct decision_context *ctx0, *ctx1; struct fanotify_event_metadata meta = { 0 }; event_t event = { 0 }; unsigned long object_hits = 0, object_misses = 0; cfg.subj_cache_size = 4; cfg.obj_cache_size = 1; CHECK(init_event_system(&cfg) == 0, 150, "[ERROR:150] init_event_system failed"); ctx0 = decision_context_current(); decision_context_set_worker_id(ctx0, 0); ctx1 = decision_context_create(&cfg); CHECK(ctx1 != NULL, 151, "[ERROR:151] second decision context allocation failed"); decision_context_set_worker_id(ctx1, 1); meta.mask = FAN_OPEN_EXEC_PERM; meta.fd = 40; meta.pid = 400; decision_context_set_current(ctx0); CHECK(new_event(&meta, &event) == 0, 152, "[ERROR:152] first context prime failed"); decision_context_set_current(ctx1); CHECK(new_event(&meta, &event) == 0, 153, "[ERROR:153] second context prime failed"); CHECK(reset_cache_report_counters() == 0, 154, "[ERROR:154] failed resetting cache counters"); request_object_cache_flush(); meta.mask = FAN_OPEN_PERM; decision_context_set_current(ctx0); CHECK(new_event(&meta, &event) == 0, 155, "[ERROR:155] first context post-flush event failed"); decision_context_set_current(ctx1); CHECK(new_event(&meta, &event) == 0, 156, "[ERROR:156] second context post-flush event failed"); CHECK(read_object_cache_metric("hits", &object_hits) == 0, 157, "[ERROR:157] failed reading object cache hits"); CHECK(read_object_cache_metric("misses", &object_misses) == 0, 158, "[ERROR:158] failed reading object cache misses"); CHECK(object_hits == 0, 159, "[ERROR:159] one worker reused stale object cache"); CHECK(object_misses == 2, 160, "[ERROR:160] both worker caches were not refreshed"); destroy_event_system(); return 0; } /* * Verify that cache reports retain aggregate counters and add worker detail. */ static int test_cache_worker_report(void) { CHECK(init_caches(2, 2) == 0, 120, "[ERROR:120] init_event_system failed"); CHECK(cache_report_contains("Subject hits:"), 121, "[ERROR:121] aggregate subject counters missing"); CHECK(cache_report_contains("Per-worker cache effectiveness:"), 122, "[ERROR:122] per-worker cache section missing"); CHECK(cache_report_contains(" Decision worker 0 Subject hits:"), 123, "[ERROR:123] worker subject counters missing"); CHECK(cache_report_contains(" Decision worker 0 Object hits:"), 124, "[ERROR:124] worker object counters missing"); destroy_event_system(); return 0; } /* Run each scenario in sequence, propagating the first non-zero error code. */ int main(void) { int rc; rc = test_rpm_accepts_sha512(); if (rc) return rc; rc = test_filedb_rejects_sha512(); if (rc) return rc; rc = test_filedb_accepts_sha256(); if (rc) return rc; rc = test_attribute_replacement_preserves_count(); if (rc) return rc; rc = test_attr_lookup_metrics(); if (rc) return rc; rc = test_reopen_skip_path(); if (rc) return rc; rc = test_subject_eviction(); if (rc) return rc; rc = test_subject_slot_blocks_pre_full_collision(); if (rc) return rc; rc = test_traced_building_occupant_eviction(); if (rc) return rc; rc = test_flush_generation_resets_object_cache(); if (rc) return rc; rc = test_flush_generation_reaches_all_contexts(); if (rc) return rc; rc = test_cache_worker_report(); if (rc) return rc; return 0; } fapolicyd-2.0/src/tests/failure_action_test.c000066400000000000000000000064161524071644000214570ustar00rootroot00000000000000/* * failure_action_test.c - verify internal failure action accounting */ #include #include #include #include "failure-action.h" #define CHECK(expr, code, msg) \ do { \ if (!(expr)) \ error(1, 0, "%s", msg); \ } while (0) /* * read_failure_report - capture failure action report output. * @buf: destination buffer. * @size: size of @buf. * Returns nothing. Exits if the temporary stream cannot be used. */ static void read_failure_report(char *buf, size_t size) { FILE *f = tmpfile(); size_t used; if (f == NULL) error(1, 0, "tmpfile failed"); failure_action_report(f); fflush(f); rewind(f); used = fread(buf, 1, size - 1, f); buf[used] = 0; fclose(f); } /* * main - exercise failure action names, actions, counters, and reporting. * Returns 0 on success. Exits with error() on test failure. */ int main(void) { failure_action_metrics_t metrics; char report[1024]; char expected[128]; unsigned long before, after; CHECK(strcmp(failure_reason_name(FAILURE_REASON_QUEUE_FULL), "queue_full") == 0, 1, "[ERROR:1] queue_full reason name changed"); CHECK(failure_reason_action(FAILURE_REASON_QUEUE_FULL) == FAILURE_ACTION_OBSERVE, 2, "[ERROR:2] queue_full default action changed"); CHECK(strcmp(failure_action_name(FAILURE_ACTION_OBSERVE), "observe") == 0, 3, "[ERROR:3] observe action name changed"); CHECK(strcmp(failure_action_name(FAILURE_ACTION_TERMINATE), "terminate") == 0, 13, "[ERROR:13] terminate action name changed"); CHECK(failure_reason_action(FAILURE_REASON_WORKER_STALL) == FAILURE_ACTION_TERMINATE, 14, "[ERROR:14] worker_stall default action changed"); CHECK(strcmp(failure_reason_name((failure_reason_t)-1), "unknown") == 0, 4, "[ERROR:4] invalid reason name not unknown"); CHECK(failure_action_record((failure_reason_t)-1) == 0, 5, "[ERROR:5] invalid reason changed counters"); before = failure_action_count(FAILURE_REASON_QUEUE_FULL); after = failure_action_record(FAILURE_REASON_QUEUE_FULL); CHECK(after == before + 1, 6, "[ERROR:6] queue_full counter did not increment"); CHECK(failure_action_count(FAILURE_REASON_QUEUE_FULL) == after, 7, "[ERROR:7] queue_full counter read mismatch"); read_failure_report(report, sizeof(report)); snprintf(expected, sizeof(expected), "Failure action queue_full (observe): %lu", after); CHECK(strstr(report, expected) != NULL, 8, "[ERROR:8] report missing queue_full counter"); CHECK(strstr(report, "Failure action trust_reload_failure (observe): ") != NULL, 9, "[ERROR:9] report missing trust reload counter"); CHECK(strstr(report, "Failure action fanotify_filesystem_error (observe): ") != NULL, 12, "[ERROR:12] report missing FAN_FS_ERROR counter"); CHECK(strstr(report, "Failure action worker_stall (terminate): ") != NULL, 15, "[ERROR:15] report missing worker stall action"); failure_action_snapshot(&metrics, 1); CHECK(failure_action_metrics_count(&metrics, FAILURE_REASON_QUEUE_FULL) == after, 10, "[ERROR:10] reset snapshot lost queue_full count"); CHECK(failure_action_count(FAILURE_REASON_QUEUE_FULL) == 0, 11, "[ERROR:11] reset snapshot did not clear queue_full count"); return 0; } fapolicyd-2.0/src/tests/fd_fgets_test.c000066400000000000000000000226411524071644000202520ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include /* * Exercises the fd_fgets_r family of APIs with multiple backing buffers and * input patterns. The goal is to cover the behaviours that fapolicyd relies * on: incremental reads from pipes, truncated lines, anonymous mmap buffers * and pre-populated mmap()'d files. */ #ifndef MAP_ANONYMOUS #define MAP_ANONYMOUS MAP_ANON #endif static void write_all(int fd, const char *data) { size_t done = 0, len = strlen(data); while (done < len) { ssize_t rc = write(fd, data + done, len - done); assert(rc > 0); done += (size_t)rc; } } /* * Verify the default "self managed" buffer path. This covers the most * common usage in fapolicyd where lines arrive from a pipe incrementally. */ static void test_pipe_self_managed(void) { int fds[2]; char buf[16]; fd_fgets_state_t *st; assert(pipe(fds) == 0); st = fd_fgets_init(); assert(st); /* Nothing buffered yet. */ assert(fd_fgets_more_r(st, sizeof(buf)) == 0); assert(fd_fgets_eof_r(st) == 0); write_all(fds[1], "hello\nworld\n"); close(fds[1]); /* Read first line and ensure the buffer reports more data. */ int len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == 6); assert(strcmp(buf, "hello\n") == 0); assert(fd_fgets_more_r(st, sizeof(buf)) == 1); len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == 6); assert(strcmp(buf, "world\n") == 0); /* EOF is detected on the next call. */ len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == 0); assert(fd_fgets_eof_r(st) == 1); fd_fgets_clear_r(st); assert(fd_fgets_eof_r(st) == 0); close(fds[0]); fd_fgets_destroy(st); } /* * A long line must be returned in multiple chunks when the destination buffer * is too small. The second call should resume from where the first one * stopped and deliver the trailing newline. */ static void test_truncation_resume(void) { int fds[2]; char buf[6]; fd_fgets_state_t *st; assert(pipe(fds) == 0); st = fd_fgets_init(); assert(st); write_all(fds[1], "123456789\n"); close(fds[1]); int len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == 5); assert(strcmp(buf, "12345") == 0); assert(fd_fgets_more_r(st, sizeof(buf)) == 1); len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == 5); assert(strcmp(buf, "6789\n") == 0); len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == 0); assert(fd_fgets_eof_r(st) == 1); close(fds[0]); fd_fgets_destroy(st); } /* * Allocate the working buffer with malloc() so that the destroy path frees it * for us. Exercise blank lines, the clear helper, and the ability to process * additional data after clearing. */ static void test_malloc_buffer(void) { int fds[2]; char buf[32]; char *custom; fd_fgets_state_t *st; assert(pipe(fds) == 0); st = fd_fgets_init(); assert(st); custom = malloc(128); assert(custom); assert(fd_setvbuf_r(st, custom, 128, MEM_MALLOC) == 0); write_all(fds[1], "first\n\n"); int len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == 6); assert(strcmp(buf, "first\n") == 0); len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == 1); assert(strcmp(buf, "\n") == 0); fd_fgets_clear_r(st); assert(fd_fgets_eof_r(st) == 0); write_all(fds[1], "third\n"); close(fds[1]); len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == 6); assert(strcmp(buf, "third\n") == 0); len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == 0); assert(fd_fgets_eof_r(st) == 1); close(fds[0]); fd_fgets_destroy(st); } /* * Use an anonymous mmap() backed buffer. Start with a partial line so that * the first call returns 0 while the writer is still open, then complete the * line and ensure it becomes available without losing data. */ static void test_mmap_buffer(void) { int fds[2]; char buf[64]; void *region; fd_fgets_state_t *st; assert(pipe(fds) == 0); st = fd_fgets_init(); assert(st); region = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); assert(region != MAP_FAILED); assert(fd_setvbuf_r(st, region, 4096, MEM_MMAP) == 0); write_all(fds[1], "hello"); int len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == 0); assert(fd_fgets_more_r(st, sizeof(buf)) == 0); assert(fd_fgets_eof_r(st) == 0); write_all(fds[1], " world\n"); close(fds[1]); len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == 12); assert(strcmp(buf, "hello world\n") == 0); len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == 0); assert(fd_fgets_eof_r(st) == 1); close(fds[0]); fd_fgets_destroy(st); } /* * Keep unread data in place until the working buffer runs out of space. * The first read consumes the entire buffer without seeing a newline, so the * second read must trigger a deferred compaction before pulling in the tail of * the line. */ static void test_deferred_compaction(void) { int fds[2]; char buf[64]; char *custom; const char *line = "0123456789abcdef0123456789abcdefQRSTUVWX\n"; fd_fgets_state_t *st; size_t line_len = strlen(line); size_t capacity = 33; assert(pipe(fds) == 0); st = fd_fgets_init(); assert(st); custom = malloc(capacity); assert(custom); assert(fd_setvbuf_r(st, custom, capacity, MEM_MALLOC) == 0); write_all(fds[1], line); close(fds[1]); int len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == (int)(capacity - 1)); assert(strncmp(buf, line, (size_t)len) == 0); assert(fd_fgets_eof_r(st) == 0); len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == (int)(line_len - (capacity - 1))); assert(strcmp(buf, line + (capacity - 1)) == 0); len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == 0); assert(fd_fgets_eof_r(st) == 1); close(fds[0]); fd_fgets_destroy(st); } /* * MEM_SELF_MANAGED is reserved for the internal default buffer created by * fd_fgets_init(). Supplying external memory with this mode is rejected. */ static void test_reject_self_managed_override(void) { char custom[32]; fd_fgets_state_t *st; st = fd_fgets_init(); assert(st); assert(fd_setvbuf_r(st, custom, sizeof(custom), MEM_SELF_MANAGED) == 1); fd_fgets_destroy(st); } /* * An I/O error is distinct from EOF: callers must not consume the output * buffer after fd_fgets_r() returns -1 because it leaves both buffer and * state untouched. */ static void test_read_error_preserves_caller_state(void) { char buf[32] = "unchanged"; fd_fgets_state_t *st; st = fd_fgets_init(); assert(st); assert(fd_fgets_r(st, buf, sizeof(buf), -1) == -1); assert(strcmp(buf, "unchanged") == 0); assert(fd_fgets_eof_r(st) == 0); fd_fgets_destroy(st); } /* * Map README.md directly and parse it without issuing read() calls. This is * the MEM_MMAP_FILE path that the daemon relies on for audit log replay. */ /* * Verify MEM_MMAP_FILE parsing on an mmap()ed read-only file that has no * trailing newline. This ensures we never try to compact by writing into the * mapped region after advancing st->buffer. */ static void test_mmap_file_no_trailing_newline(void) { char template[] = "/tmp/fd_fgets_mmap_no_nlXXXXXX"; char buf[64]; const char *line = "0123456789abcdef0123456789abcdefQRSTUVWX"; size_t line_len = strlen(line); int fd; void *base; struct stat sb; fd_fgets_state_t *st; fd = mkstemp(template); assert(fd >= 0); assert(unlink(template) == 0); write_all(fd, line); assert(lseek(fd, 0, SEEK_SET) == 0); assert(fstat(fd, &sb) == 0); base = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0); assert(base != MAP_FAILED); st = fd_fgets_init(); assert(st); assert(fd_setvbuf_r(st, base, sb.st_size, MEM_MMAP_FILE) == 0); int len = fd_fgets_r(st, buf, 33, fd); assert(len == 32); assert(strncmp(buf, line, (size_t)len) == 0); len = fd_fgets_r(st, buf, sizeof(buf), fd); assert(len == (int)(line_len - 32)); assert(strcmp(buf, line + 32) == 0); len = fd_fgets_r(st, buf, sizeof(buf), fd); assert(len == 0); assert(fd_fgets_eof_r(st) == 1); fd_fgets_destroy(st); close(fd); } static void test_mmap_file_readme(void) { const char *srcdir = getenv("srcdir"); char path[512]; int fd; fd_fgets_state_t *st; char buf[256]; int lines = 0; struct stat sb; void *base; if (!srcdir) srcdir = "src/tests"; snprintf(path, sizeof(path), "%s/../../README.md", srcdir); fd = open(path, O_RDONLY); assert(fd >= 0); assert(fstat(fd, &sb) == 0); base = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0); assert(base != MAP_FAILED); st = fd_fgets_init(); assert(st); assert(fd_setvbuf_r(st, base, sb.st_size, MEM_MMAP_FILE) == 0); int len = fd_fgets_r(st, buf, sizeof(buf), fd); assert(len > 0); assert(strncmp(buf, "File Access Policy Daemon", 25) == 0); lines++; /* Clearing should rewind the file mapping to the start. */ fd_fgets_clear_r(st); len = fd_fgets_r(st, buf, sizeof(buf), fd); assert(len > 0); assert(strncmp(buf, "File Access Policy Daemon", 25) == 0); lines++; do { len = fd_fgets_r(st, buf, sizeof(buf), fd); if (len > 0) lines++; } while (!fd_fgets_eof_r(st)); assert(lines > 50); fd_fgets_destroy(st); close(fd); } int main(void) { test_pipe_self_managed(); test_truncation_resume(); test_malloc_buffer(); test_mmap_buffer(); test_deferred_compaction(); test_reject_self_managed_override(); test_read_error_preserves_caller_state(); test_mmap_file_no_trailing_newline(); test_mmap_file_readme(); printf("fd-fgets_r tests: all passed\n"); return 0; } fapolicyd-2.0/src/tests/file_filter_test.c000066400000000000000000000035671524071644000207630ustar00rootroot00000000000000/* * file_filter_test.c - ensure filter_prune_list handles basic lists */ #include "filter.h" #include "llist.h" #include #include #include #define FILTER_CONF TEST_BASE "/src/tests/fixtures/filter-minimal.conf" static int has_path(list_t *list, const char *path) { for (list_item_t *lptr = list->first; lptr; lptr = lptr->next) { if (strcmp(lptr->index, path) == 0) return 1; } return 0; } int main(void) { list_t list; list_init(&list); if (list_append(&list, strdup("/usr/bin/allowed"), NULL) || list_append(&list, strdup("/usr/bin/skipped"), NULL) || list_append(&list, strdup("/var/log/public/info"), NULL) || list_append(&list, strdup("/var/log/blocked.log"), NULL) || list_append(&list, strdup("/usr/share/example.tmp"), NULL)) { fprintf(stderr, "[ERROR:1] unable to build list\n"); list_empty(&list); return 1; } if (filter_prune_list(&list, FILTER_CONF)) { fprintf(stderr, "[ERROR:2] filter_prune_list failed\n"); list_empty(&list); return 2; } if (list.count != 2) { fprintf(stderr, "[ERROR:3] expected 2 entries, got %ld\n", list.count); list_empty(&list); return 3; } if (!has_path(&list, "/usr/bin/allowed")) { fprintf(stderr, "[ERROR:4] allowed binary missing\n"); list_empty(&list); return 4; } if (!has_path(&list, "/var/log/public/info")) { fprintf(stderr, "[ERROR:5] allowed log entry missing\n"); list_empty(&list); return 5; } list_empty(&list); return 0; } fapolicyd-2.0/src/tests/file_type_detect_test.c000066400000000000000000000211151524071644000217740ustar00rootroot00000000000000/* * file_type_detect_test.c - verify quick file type helpers */ #define _GNU_SOURCE #include "config.h" #include #include #include #include #include #include #include #include #include #include "decision-context.h" #include "file.h" #ifndef TEST_BASE #define TEST_BASE "." #endif // RHEL8 does not have this. Remove when RHEL 8 goes EOL. #ifndef MAGIC_NO_CHECK_JSON #define MAGIC_NO_CHECK_JSON 0 #endif static void expect_extract(const char *label, const char *script, const char *expected) { char buf[64]; size_t len = strlen(script); const char *got = extract_shebang_interpreter(script, len, buf, sizeof(buf)); if (expected == NULL) { if (got != NULL) error(1, 0, "%s: expected NULL, got %s", label, got); return; } if (!got || strcmp(got, expected) != 0) error(1, 0, "%s: expected %s got %s", label, expected, got ? got : "(null)"); } static void expect_mime(const char *label, const char *interp, const char *expected) { const char *got = mime_from_shebang(interp); if (expected == NULL) { if (got != NULL) error(1, 0, "%s: expected NULL, got %s", label, got); return; } if (!got || strcmp(got, expected) != 0) error(1, 0, "%s: expected %s got %s", label, expected, got ? got : "(null)"); } static void expect_magic(const char *label, const unsigned char *hdr, size_t len, const char *expected) { const char *got = detect_by_magic_number(hdr, len); if (expected == NULL) { if (got != NULL) error(1, 0, "%s: expected NULL, got %s", label, got); return; } if (!got || strcmp(got, expected) != 0) error(1, 0, "%s: expected %s got %s", label, expected, got ? got : "(null)"); } static void expect_text(const char *label, const char *buf, size_t len, const char *expected) { char local[513]; if (len >= sizeof(local)) error(1, 0, "%s: test buffer too large", label); memcpy(local, buf, len); local[len] = '\0'; const char *got = detect_text_format(local, len); if (expected == NULL) { if (got != NULL) error(1, 0, "%s: expected NULL, got %s", label, got); return; } if (!got || strcmp(got, expected) != 0) error(1, 0, "%s: expected %s got %s", label, expected, got ? got : "(null)"); } /* * init_magic_handles - initialize libmagic handles in the default context. * Returns 0 on success, -1 on failure. */ static int init_magic_handles(void) { struct decision_context *ctx = decision_context_current(); char path[512]; const char *fast_db[] = { TEST_BASE "/init/fapolicyd-magic", "./init/fapolicyd-magic", "../init/fapolicyd-magic", "../../init/fapolicyd-magic", NULL, }; int i; unsetenv("MAGIC"); ctx->magic_fast = magic_open( MAGIC_MIME | MAGIC_ERROR | MAGIC_NO_CHECK_CDF | MAGIC_NO_CHECK_ELF | MAGIC_NO_CHECK_COMPRESS | MAGIC_NO_CHECK_TAR | MAGIC_NO_CHECK_APPTYPE | MAGIC_NO_CHECK_TOKENS | MAGIC_NO_CHECK_JSON ); if (!ctx->magic_fast) return -1; for (i = 0; fast_db[i]; i++) { (void)snprintf(path, sizeof(path), "%s", fast_db[i]); if (magic_load(ctx->magic_fast, path) == 0) break; } if (!fast_db[i]) { file_close(); return -1; } ctx->magic_full = magic_open(MAGIC_MIME | MAGIC_ERROR | MAGIC_NO_CHECK_CDF | MAGIC_NO_CHECK_ELF); if (!ctx->magic_full) { file_close(); return -1; } if (magic_load(ctx->magic_full, NULL) != 0) { file_close(); return -1; } return 0; } /* * close_magic_handles - release libmagic handles used in direct tests. */ static void close_magic_handles(void) { file_close(); } /* * expect_file_init_magic_failure - verify file_init() reports libmagic errors. */ static void expect_file_init_magic_failure(void) { struct decision_context *ctx; int rc; file_init_failure_for_tests(FILE_INIT_MAGIC_FAST_OPEN_FAILED); rc = file_init(); file_init_failure_for_tests(FILE_INIT_OK); if (rc != FILE_INIT_MAGIC_FAST_OPEN_FAILED) error(1, 0, "file_init: expected fast magic failure got %d", rc); ctx = decision_context_current(); if (ctx->magic_fast != NULL || ctx->magic_full != NULL) error(1, 0, "file_init: failed init left magic handles open"); file_close(); } /* * create_tmp_file - create a temporary file populated with text content. * Returns a readable descriptor on success. */ static int create_tmp_file(const char *content) { char path[] = "/tmp/file-type-test-XXXXXX"; int fd = mkstemp(path); size_t len = strlen(content); if (fd < 0) error(1, errno, "mkstemp failed"); if (unlink(path) != 0) error(1, errno, "unlink failed"); if (write(fd, content, len) != (ssize_t)len) error(1, errno, "write failed"); if (lseek(fd, 0, SEEK_SET) != 0) error(1, errno, "lseek failed"); return fd; } /* * expect_magic_descriptor - call magic_descriptor directly and compare mime. */ static void expect_magic_descriptor(const char *label, magic_t cookie, int fd, const char *expected) { const char *got; char type[128]; const char *semi; size_t len; if (lseek(fd, 0, SEEK_SET) != 0) error(1, errno, "%s: lseek failed", label); got = magic_descriptor(cookie, fd); if (!got) error(1, 0, "%s: expected %s got (null)", label, expected); semi = strchr(got, ';'); len = semi ? (size_t)(semi - got) : strlen(got); if (len >= sizeof(type)) error(1, 0, "%s: mime too large", label); memcpy(type, got, len); type[len] = '\0'; if (strcmp(type, expected) != 0) error(1, 0, "%s: expected %s got %s", label, expected, type); } int main(void) { struct decision_context *ctx; int fd; const unsigned char png_hdr[] = { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' }; const unsigned char jpg_hdr[] = { 0xFF, 0xD8, 0xFF, 0xE0 }; const unsigned char gzip_hdr[] = { 0x1F, 0x8B, 0x08, 0x00 }; expect_file_init_magic_failure(); if (init_magic_handles() != 0) error(1, 0, "failed to initialize test libmagic handles"); ctx = decision_context_current(); expect_extract("bash", "#!/bin/bash\n", "bash"); expect_extract("env-python", "#! /usr/bin/env -S python3 -u\n", "python3"); expect_extract("env-assignment-shell", "#!/usr/bin/env -S FOO=1 BAR=2 bash -eux\n", "bash"); expect_extract("env-assignment-path", "#!/usr/bin/env -S HOME=/tmp /usr/bin/dash -e\n", "dash"); expect_extract("env-double-dash-assignment", "#!/usr/bin/env -- FOO=1 /bin/bash\n", "bash"); expect_extract("env-path", "#!/usr/bin/env /opt/perl5.32/bin/perl5.32\n", "perl5"); expect_extract("no-shebang", "echo hello\n", NULL); expect_mime("shell", "bash", "text/x-shellscript"); expect_mime("python", "python3", "text/x-python"); expect_mime("php", "php", "text/x-php"); expect_mime("unknown", "ruby", NULL); expect_magic("png", png_hdr, sizeof(png_hdr), "image/png"); expect_magic("jpeg", jpg_hdr, sizeof(jpg_hdr), "image/jpeg"); expect_magic("gzip", gzip_hdr, sizeof(gzip_hdr), "application/gzip"); expect_magic("unknown", (const unsigned char *)"abc", 3, NULL); expect_text("html", " \n", strlen(" \n"), "text/html"); expect_text("plain", "just some text\n", strlen("just some text\n"), NULL); if (strcmp(classify_device(S_IFIFO), "inode/fifo") != 0) error(1, 0, "classify_device: expected inode/fifo"); fd = create_tmp_file("#!/bin/awk\nBEGIN { print 1 }\n"); expect_magic_descriptor("full-awk-bin", ctx->magic_full, fd, "text/x-awk"); close(fd); fd = create_tmp_file("#!/usr/bin/gawk\nBEGIN { print 1 }\n"); expect_magic_descriptor("full-gawk-usr-bin", ctx->magic_full, fd, "text/x-gawk"); close(fd); fd = create_tmp_file("#!/usr/bin/perl\nprint qq(hi);\n"); expect_magic_descriptor("full-perl-usr-bin", ctx->magic_full, fd, "text/x-perl"); close(fd); #ifndef FAPOLICYD_RHEL8 /* * RHEL 8 libmagic reports this as text/x-python. Keep the current * expectation for newer systems and drop this guard when RHEL 8 * support is no longer needed. */ fd = create_tmp_file("#!/usr/bin/python3\nprint(1)\n"); expect_magic_descriptor("full-python-usr-bin", ctx->magic_full, fd, "text/x-script.python"); close(fd); #endif fd = create_tmp_file("#!/usr/bin/R\nprint(1)\n"); expect_magic_descriptor("full-r-usr-bin", ctx->magic_full, fd, "text/plain"); close(fd); fd = create_tmp_file("#!/usr/bin/guile\n(display 1)\n"); expect_magic_descriptor("fast-guile-usr-bin", ctx->magic_fast, fd, "text/x-script.guile"); close(fd); fd = create_tmp_file("#!/usr/bin/gjs\nprint(1);\n"); expect_magic_descriptor("fast-gjs-usr-bin", ctx->magic_fast, fd, "application/javascript"); close(fd); fd = create_tmp_file("#!/usr/sbin/nft\nadd table inet t\n"); expect_magic_descriptor("fast-nft-usr-sbin", ctx->magic_fast, fd, "text/x-nftables"); close(fd); close_magic_handles(); return 0; } fapolicyd-2.0/src/tests/filter_test.c000066400000000000000000000262601524071644000177570ustar00rootroot00000000000000/* * filter_test.c - comprehensive tests for filter configuration */ #include #include #include #include #include #include #include #include #include #include "filter.h" /* * Test strategy summary * --------------------- * This harness validates filter.c against both a minimal example * configuration and the full production filter. Path/verdict pairs * are defined in src/tests/fixtures/filter-cases.txt; for each entry the * test: * 1. re‑initializes the filter, * 2. loads the designated filter file, * 3. checks that filter_check() returns the expected allow/deny * result. * Coverage includes wildcard patterns, nested overrides, directory * versus file semantics, duplicate slashes, “..” traversal, UTF‑8 * path segments, and other edge cases. * * Negative parsing: src/tests/fixtures/broken-filter.conf contains mixed * whitespace indentation, a missing leading ‘+’/‘-’, and an unescaped * ‘#’ to ensure filter_load_file() fails on malformed syntax. * * Performance guardrail: the production filter is parsed 1000 times, * measuring mean parse time via clock_gettime(). A warning is issued if * the average exceeds twice BASE_NS, allowing detection of significant * regressions. * * Additional safeguards: explicit checks ensure all fixture files are * present, filter_init() succeeds, and error messages provide unique * exit codes for CI triage. */ #define BASE_NS 7400 #ifndef TEST_BASE #define TEST_BASE "." #endif #define CASES_FILE TEST_BASE "/src/tests/fixtures/filter-cases.txt" #define MIN_CONF TEST_BASE "/src/tests/fixtures/filter-minimal.conf" #define BROKEN_CONF TEST_BASE "/src/tests/fixtures/broken-filter.conf" #define PROD_CONF TEST_BASE "/init/fapolicyd-filter.conf" #define CONCURRENT_FILTER_RULES 2048 #define CONCURRENT_FILTER_WORKERS 4 #define CONCURRENT_FILTER_ITERATIONS 200 extern filter_t *global_filter; /* check_tree_reset - ensure processed and matched flags are cleared */ static int check_tree_reset(filter_t *f) { if (!f) return 1; if (f->processed || f->matched) return 0; list_item_t *item = list_get_first(&f->list); for (; item; item = item->next) { if (!check_tree_reset((filter_t *)item->data)) return 0; } return 1; } static int file_exists(const char *path) { struct stat st; return stat(path, &st) == 0; } /* replace escape sequences like '\ ' */ static void unescape(char *s) { char *src = s, *dst = s; while (*src) { if (*src == '\\' && src[1]) { ++src; *dst++ = *src++; } else { *dst++ = *src++; } } *dst = '\0'; } static int run_cases(const char *cfg, const char *path) { FILE *f = fopen(CASES_FILE, "r"); char col[32]; char p[1024]; int exp; int rc = 0; if (f == NULL) { fprintf(stderr, "[ERROR:6] missing %s\n", CASES_FILE); return 6; } while (fscanf(f, "%31s %1023s %d", col, p, &exp) == 3) { if (strcmp(col, cfg) != 0) continue; unescape(p); if (filter_init()) { fprintf(stderr, "[ERROR:2] filter_init failed\n"); rc = 2; break; } if (filter_load_file(path)) { fprintf(stderr, "[ERROR:3] loading a valid fixture failed\n"); filter_destroy(); rc = 3; break; } int res = filter_check(p); if (!check_tree_reset(global_filter)) { fprintf(stderr, "[ERROR:7] filter flags not reset after filter_check\n"); rc = 7; filter_destroy(); break; } if (res != exp) { fprintf(stderr, "[ERROR:4] %s:%s expected %s got %s\n", cfg, p, exp ? "ALLOW" : "DENY", res ? "ALLOW" : "DENY"); rc = 4; filter_destroy(); break; } filter_destroy(); } fclose(f); return rc; } /* * run_wide_tree_case - verify wide root trees do not hit depth errors * Returns 0 on success and a unique non-zero test code on failure. */ static int run_wide_tree_case(void) { char tmpl[] = "/tmp/fapolicyd-filter-wide-XXXXXX"; int fd = mkstemp(tmpl); if (fd < 0) { fprintf(stderr, "[ERROR:8] cannot create temp file\n"); return 8; } FILE *f = fdopen(fd, "w"); if (!f) { close(fd); unlink(tmpl); fprintf(stderr, "[ERROR:9] cannot open temp file stream\n"); return 9; } /* * Create more than MAX_FILTER_DEPTH sibling rules at the root level. * The checker pushes root descendants before matching, so this used to * fail with FILTER_ERR_DEPTH when backed by a fixed-size stack. */ for (int i = 0; i < 80; i++) { if (fprintf(f, "+ /wide-%d\n", i) < 0) { fclose(f); unlink(tmpl); fprintf(stderr, "[ERROR:10] cannot write temp config\n"); return 10; } } if (fprintf(f, "+ /target\n") < 0) { fclose(f); unlink(tmpl); fprintf(stderr, "[ERROR:10] cannot write temp config\n"); return 10; } if (fclose(f) != 0) { unlink(tmpl); fprintf(stderr, "[ERROR:11] cannot close temp config\n"); return 11; } if (filter_init()) { unlink(tmpl); fprintf(stderr, "[ERROR:2] filter_init failed\n"); return 2; } if (filter_load_file(tmpl)) { filter_destroy(); unlink(tmpl); fprintf(stderr, "[ERROR:3] loading wide fixture failed\n"); return 3; } filter_rc_t res = filter_check("/target"); if (!check_tree_reset(global_filter)) { filter_destroy(); unlink(tmpl); fprintf(stderr, "[ERROR:7] filter flags not reset after filter_check\n"); return 7; } filter_destroy(); unlink(tmpl); if (res != FILTER_ALLOW) { fprintf(stderr, "[ERROR:12] wide tree expected ALLOW got %d\n", res); return 12; } return 0; } struct concurrent_filter_worker { const char *path; filter_rc_t expected; int failed; }; struct concurrent_filter_observer { atomic_bool done; atomic_bool saw_mutation; }; /* * concurrent_filter_worker - repeatedly check one path from a shared tree. * @arg: concurrent_filter_worker pointer describing the expected verdict. * Returns NULL. */ static void *concurrent_filter_worker(void *arg) { struct concurrent_filter_worker *worker = arg; for (int i = 0; i < CONCURRENT_FILTER_ITERATIONS; i++) { if (filter_check(worker->path) != worker->expected) { worker->failed = 1; break; } } return NULL; } /* * concurrent_filter_observer - detect check-time mutations in the filter tree. * @arg: concurrent_filter_observer pointer. * Returns NULL. */ static void *concurrent_filter_observer(void *arg) { struct concurrent_filter_observer *observer = arg; while (!atomic_load_explicit(&observer->done, memory_order_relaxed)) { if (!check_tree_reset(global_filter)) { atomic_store_explicit(&observer->saw_mutation, true, memory_order_relaxed); break; } } return NULL; } /* * run_concurrent_check_case - verify filter checks are read-only tree walks. * * Trust-source imports are normally serialized today, but the compiled filter * is shared library state. Future import backends can safely share one loaded * filter generation only if checking a path does not write traversal state * into the tree itself. * * Returns 0 on success and a unique non-zero test code on failure. */ static int run_concurrent_check_case(void) { struct concurrent_filter_worker worker[CONCURRENT_FILTER_WORKERS] = { { "/target", FILTER_ALLOW, 0 }, { "/wide-9999", FILTER_DENY, 0 }, { "/wide-0100", FILTER_ALLOW, 0 }, { "/wide-2047", FILTER_ALLOW, 0 }, }; struct concurrent_filter_observer observer = { 0 }; pthread_t workers[CONCURRENT_FILTER_WORKERS]; pthread_t observer_thread; char tmpl[] = "/tmp/fapolicyd-filter-concurrent-XXXXXX"; int fd = mkstemp(tmpl); int rc = 0; if (fd < 0) { fprintf(stderr, "[ERROR:13] cannot create temp file\n"); return 13; } FILE *f = fdopen(fd, "w"); if (!f) { close(fd); unlink(tmpl); fprintf(stderr, "[ERROR:14] cannot open temp file stream\n"); return 14; } for (int i = 0; i < CONCURRENT_FILTER_RULES; i++) { if (fprintf(f, "+ /wide-%04d\n", i) < 0) { fclose(f); unlink(tmpl); fprintf(stderr, "[ERROR:15] cannot write temp config\n"); return 15; } } if (fprintf(f, "+ /target\n") < 0) { fclose(f); unlink(tmpl); fprintf(stderr, "[ERROR:15] cannot write temp config\n"); return 15; } if (fclose(f) != 0) { unlink(tmpl); fprintf(stderr, "[ERROR:16] cannot close temp config\n"); return 16; } if (filter_init()) { unlink(tmpl); fprintf(stderr, "[ERROR:2] filter_init failed\n"); return 2; } if (filter_load_file(tmpl)) { filter_destroy(); unlink(tmpl); fprintf(stderr, "[ERROR:3] loading concurrent fixture failed\n"); return 3; } if (pthread_create(&observer_thread, NULL, concurrent_filter_observer, &observer)) { filter_destroy(); unlink(tmpl); fprintf(stderr, "[ERROR:17] cannot create observer thread\n"); return 17; } for (int i = 0; i < CONCURRENT_FILTER_WORKERS; i++) { if (pthread_create(&workers[i], NULL, concurrent_filter_worker, &worker[i])) { atomic_store_explicit(&observer.done, true, memory_order_relaxed); pthread_join(observer_thread, NULL); filter_destroy(); unlink(tmpl); fprintf(stderr, "[ERROR:18] cannot create worker\n"); return 18; } } for (int i = 0; i < CONCURRENT_FILTER_WORKERS; i++) pthread_join(workers[i], NULL); atomic_store_explicit(&observer.done, true, memory_order_relaxed); pthread_join(observer_thread, NULL); for (int i = 0; i < CONCURRENT_FILTER_WORKERS; i++) { if (worker[i].failed) { fprintf(stderr, "[ERROR:19] concurrent filter verdict changed\n"); rc = 19; break; } } if (rc == 0 && atomic_load_explicit(&observer.saw_mutation, memory_order_relaxed)) { fprintf(stderr, "[ERROR:20] filter_check mutated shared tree state\n"); rc = 20; } filter_destroy(); unlink(tmpl); return rc; } int main(void) { if (!file_exists(MIN_CONF)) { fprintf(stderr, "[ERROR:6] missing %s\n", MIN_CONF); return 6; } if (!file_exists(PROD_CONF)) { fprintf(stderr, "[ERROR:6] missing %s\n", PROD_CONF); return 6; } if (!file_exists(CASES_FILE)) { fprintf(stderr, "[ERROR:6] missing %s\n", CASES_FILE); return 6; } if (!file_exists(BROKEN_CONF)) { fprintf(stderr, "[ERROR:6] missing %s\n", BROKEN_CONF); return 6; } if (filter_init()) { fprintf(stderr, "[ERROR:2] filter_init failed\n"); return 2; } if (!filter_load_file(BROKEN_CONF)) { fprintf(stderr, "[ERROR:5] malformed filter did not fail as expected\n"); filter_destroy(); return 5; } filter_destroy(); int rc = run_cases("minimal", MIN_CONF); if (rc) return rc; rc = run_cases("prod", PROD_CONF); if (rc) return rc; rc = run_wide_tree_case(); if (rc) return rc; rc = run_concurrent_check_case(); if (rc) return rc; struct timespec s, e; clock_gettime(CLOCK_MONOTONIC, &s); for (int i = 0; i < 1000; i++) { if (filter_init()) { fprintf(stderr, "[ERROR:2] filter_init failed\n"); return 2; } if (filter_load_file(PROD_CONF)) { fprintf(stderr, "[ERROR:3] loading a valid fixture failed\n"); filter_destroy(); return 3; } filter_destroy(); } clock_gettime(CLOCK_MONOTONIC, &e); long avg = ((e.tv_sec - s.tv_sec) * 1000000000L + (e.tv_nsec - s.tv_nsec)) / 1000; // The point of this test is to spot something wrong in the // parser that might loop way too long. Calling it a warning // since build systems vary in speed. if (avg > 2 * BASE_NS) { fprintf(stderr, "[WARNING:4] prod parse %ldns exceeds %dns\n", avg, 2 * BASE_NS); } return 0; } fapolicyd-2.0/src/tests/fixtures/000077500000000000000000000000001524071644000171325ustar00rootroot00000000000000fapolicyd-2.0/src/tests/fixtures/broken-filter.conf000066400000000000000000000000721524071644000225430ustar00rootroot00000000000000# malformed filter + / - usr/ usr/share/ + usr/dir#name fapolicyd-2.0/src/tests/fixtures/filter-cases.txt000066400000000000000000000035231524071644000222570ustar00rootroot00000000000000minimal /usr/include/stdio.h 0 minimal /usr/share/doc.txt 1 minimal /usr/share/cache.tmp 0 minimal /usr/share/script.py 1 minimal /usr/src/kernel123/driver.c 0 minimal /usr/src/kernel123/scripts/build 0 minimal /etc/hosts 1 minimal /var/log/messages 0 minimal /var/log/public/info.log 1 minimal ../foo 0 minimal foo/.. 0 prod ../foo 0 prod foo/.. 0 prod /usr/share/../include/stdio.h 0 prod /usr/includee/stdio.h 1 prod /usr/share/doc.txt 0 prod /usr/share/script.py 1 prod /usr/share/byte.pyc 1 prod /usr/share/byte.pyzz 0 prod /usr/share/space\ file.py 1 prod /usr/share/space\ file.txt 0 prod /usr/share/app/libexec/ 1 prod /usr/share/app/libexecx/tool 0 prod /usr/share/test.rb 1 prod /usr/share/test.rbx 0 prod /usr/share/test.pl 1 prod /usr/share/test.plx 0 prod /usr/share/test.stp 1 prod /usr/share/test.stpx 0 prod /usr//share/test.js 1 prod /usr//share/test.jsx 0 prod /usr/share/test.jar 1 prod /usr/share/test.jarx 0 prod /usr/share/test.m4 1 prod /usr/share/test.m4x 0 prod /usr/share/test.php 1 prod /usr/share/test.phpx 0 prod /usr/share/test.pm 1 prod /usr/share/test.pmx 0 prod /usr/share/手稿.lua 1 prod /usr/share/手稿.luax 0 prod /usr/share/Test.class 1 prod /usr/share/Test.classx 0 prod /usr/share/test.ts 1 prod /usr/share/test.tss 0 prod /usr/share/test.tsx 1 prod /usr/share/test.tsxx 0 prod /usr/share/test.el 1 prod /usr/share/test.elx 0 prod /usr/share/test.elc 1 prod /usr/share/test.elcx 0 prod /usr/src/kernel123/driver.c 0 prod /usr/src/kernel123/scripts/build 1 prod /usr/src/kernel123/scriptz/build 0 prod /usr/src/kernel123/tools/objtool/run 1 prod /usr/src/kernel123/tools/objtool2/run 0 prod /usr/src/kernels/6.17.8-200.fc42.x86_64/arch/x86/kernel/ptrace.c 0 prod /usr/src/kernels/6.17.8-200.fc42.x86_64/scripts/gendwarfksyms/examples/kabi.h 1 prod /usr/src/kernels/6.17.6-200.fc42.x86_64/include/linux/memory.h 0 prod /usr//bin/ls 1 fapolicyd-2.0/src/tests/fixtures/filter-minimal.conf000066400000000000000000000001601524071644000227070ustar00rootroot00000000000000# test filter configuration + / - usr/ + bin/allowed + share/ - *.tmp + *.py - var/log/ + public/ fapolicyd-2.0/src/tests/fixtures/rules-valid.rules000066400000000000000000000002441524071644000224350ustar00rootroot00000000000000%good=1000,1001 %paths=/bin/ls,/usr/bin/id allow perm=any auid=1000 : path=/bin/ls allow perm=any auid=%good : path=/bin/ls allow perm=any auid=%good : path=%paths fapolicyd-2.0/src/tests/gid_proc_test.c000066400000000000000000000165621524071644000202640ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include "attr-sets.h" #include "process.h" /* * require_group - ensure a collected attribute set contains a specific GID * @set: attribute set generated by read_proc_status() * @gid: numeric group identifier expected in the set * @label: description for error reporting when the lookup fails */ static void require_group(attr_sets_entry_t *set, unsigned int gid, const char *label) { if (!attr_set_check_int(set, (int64_t)gid)) error(1, 0, "%s group %u not found", label, gid); } /* * check_split_groups_status - verify parser consumes fragmented Groups lines * * Return: none; exits through error() if parsing misses expected gids. */ static void check_split_groups_status(void) { char status_template[] = "/tmp/fapolicyd-status-XXXXXX"; char long_groups[512]; char content[1024]; int fd; int len; unsigned int gid_values[] = { 1000, 1001, 1002, 1003, 1004, 1234567890, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, }; struct proc_status_info info = { .ppid = -1, .uid = NULL, .groups = NULL, .comm = NULL, }; attr_sets_entry_t *groups; size_t i; len = snprintf(long_groups, sizeof(long_groups), "Groups: 1000 1001 1002 1003 1004 1234567890 " "1006 1007 1008 1009 1010 1011 1012 1013 " "1014 1015 1016 1017 1018 1019\n"); if (len < 0 || (size_t)len >= sizeof(long_groups)) error(1, 0, "Unable to format Groups line"); len = snprintf(content, sizeof(content), "Name: testproc\n" "Gid: 1000 1001 1002 1003\n" "%s", long_groups); if (len < 0 || (size_t)len >= sizeof(content)) error(1, 0, "Unable to format synthetic status content"); fd = mkstemp(status_template); if (fd < 0) error(1, errno, "mkstemp failed"); if (write(fd, content, (size_t)len) != len) error(1, errno, "write synthetic status failed"); if (lseek(fd, 0, SEEK_SET) == (off_t)-1) error(1, errno, "lseek synthetic status failed"); if (read_proc_status_fd(fd, PROC_STAT_GID, &info) != 0) error(1, 0, "Unable to parse synthetic status file"); close(fd); unlink(status_template); groups = info.groups; info.groups = NULL; if (!groups) error(1, 0, "Synthetic gid set not available"); for (i = 0; i < sizeof(gid_values) / sizeof(gid_values[0]); i++) { if (!attr_set_check_int(groups, (int64_t)gid_values[i])) { error(1, 0, "Synthetic group %u not found", gid_values[i]); } } attr_set_destroy(groups); } /* * check_truncated_status_cleanup - reject a partial Groups line. * * Return: none; exits through error() if incomplete status data succeeds or * leaves a partially populated result behind. */ static void check_truncated_status_cleanup(void) { char status_template[] = "/tmp/fapolicyd-status-XXXXXX"; const char content[] = "Name:\ttestproc\n" "Uid:\t1000 1000 1000 1000\n" "Gid:\t1000 1000 1000 1000\n" "Groups:\t1000 1001"; struct proc_status_info info = { .ppid = -1, .uid = NULL, .groups = NULL, .comm = NULL, }; int fd; fd = mkstemp(status_template); if (fd < 0) error(1, errno, "mkstemp failed"); if (write(fd, content, sizeof(content) - 1) != (ssize_t)(sizeof(content) - 1)) error(1, errno, "write truncated status failed"); if (lseek(fd, 0, SEEK_SET) == (off_t)-1) error(1, errno, "lseek truncated status failed"); if (read_proc_status_fd(fd, PROC_STAT_UID | PROC_STAT_GID | PROC_STAT_COMM, &info) == 0) error(1, 0, "Truncated status unexpectedly succeeded"); close(fd); unlink(status_template); if (info.uid || info.groups || info.comm) error(1, 0, "Truncated status left partial data behind"); } /* * check_status_reader_error_cleanup - reject a status descriptor read error. * * Return: none; exits through error() if a reader error succeeds or leaves * partial result storage allocated. */ static void check_status_reader_error_cleanup(void) { struct proc_status_info info = { .ppid = -1, .uid = NULL, .groups = NULL, .comm = NULL, }; int fds[2]; if (pipe(fds)) error(1, errno, "pipe failed"); if (fcntl(fds[0], F_SETFL, O_NONBLOCK) == -1) error(1, errno, "fcntl O_NONBLOCK failed"); if (read_proc_status_fd(fds[0], PROC_STAT_UID | PROC_STAT_GID | PROC_STAT_COMM, &info) == 0) error(1, 0, "Status reader error unexpectedly succeeded"); close(fds[0]); close(fds[1]); if (info.uid || info.groups || info.comm) error(1, 0, "Status reader error left partial data behind"); } /* * main - validate GID collection helper captures all credential facets * * Return: 0 when all expected group IDs are reported, or terminate via error() * if any lookup or syscall fails during the exercise. */ int main(void) { int res, num, i, check_intersect = 0; gid_t gid, gids[NGROUPS_MAX]; struct proc_status_info info = { .ppid = -1, .uid = NULL, .groups = NULL, .comm = NULL }; attr_sets_entry_t *groups; FILE *status; char buf[4096]; int saw_gid_line = 0; unsigned int missing_gid; check_split_groups_status(); check_truncated_status_cleanup(); check_status_reader_error_cleanup(); if (read_proc_status(getpid(), PROC_STAT_GID, &info) != 0) error(1, 0, "Unable to obtain gid set"); groups = info.groups; info.groups = NULL; if (!groups) error(1, 0, "Unable to obtain gid set"); status = fopen("/proc/self/status", "rt"); if (!status) error(1, errno, "fopen /proc/self/status"); while (fgets(buf, sizeof(buf), status)) { if (memcmp(buf, "Gid:", 4) == 0) { unsigned int real_gid = 0, eff_gid = 0; unsigned int saved_gid = 0, fs_gid = 0; int fields = sscanf(buf, "Gid: %u %u %u %u", &real_gid, &eff_gid, &saved_gid, &fs_gid); if (fields >= 1) require_group(groups, real_gid, "Real"); if (fields >= 2) require_group(groups, eff_gid, "Effective"); // if (fields >= 3) // require_group(groups, saved_gid, "Saved"); if (fields >= 4) require_group(groups, fs_gid, "Filesystem"); saw_gid_line = 1; break; } } fclose(status); if (!saw_gid_line) error(1, 0, "Gid line not found in /proc/self/status"); gid = getgid(); num = getgroups(NGROUPS_MAX, gids); if (num < 0) error(1, 0, "Too many groups"); for (i = 0; i < num; i++) { if (gids[i] == gid) check_intersect = 1; printf("Checking for %u...", (unsigned int)gids[i]); res = attr_set_check_int(groups, (int64_t)gids[i]); if (!res) error(1, 0, "Group %u not found", (unsigned int)gids[i]); printf("found\n"); } missing_gid = 0; for (; missing_gid < UINT_MAX; missing_gid++) { if (!attr_set_check_int(groups, (int64_t)missing_gid)) break; } if (missing_gid == UINT_MAX) error(1, 0, "Unable to determine missing group for test"); res = attr_set_check_int(groups, (int64_t)missing_gid); if (res) error(1, 0, "Found unexpected group"); if (check_intersect) { printf("Doing Negative AVL intersection\n"); attr_sets_entry_t *g = attr_set_create(NULL, UNSIGNED); attr_set_append_int(g, (int64_t)missing_gid); attr_set_append_int(g, (int64_t)(missing_gid + 1)); res = avl_intersection(&(g->tree), &(groups->tree)); if (res) error(1, 0, "Negative AVL intersection failed"); printf("Doing Positive AVL intersection\n"); attr_set_append_int(g, (int64_t)gid); res = avl_intersection(&(g->tree), &(groups->tree)); if (!res) error(1, 0, "Positive AVL intersection failed"); attr_set_destroy(g); } attr_set_destroy(groups); return 0; } fapolicyd-2.0/src/tests/ignore_mounts_risk_test.c000066400000000000000000000171511524071644000224110ustar00rootroot00000000000000/* * ignore_mounts_risk_test.c - verify ignored-mount risk categories */ #define _GNU_SOURCE #include #include #include #include #include #include bool verbose; #include "../cli/ignore-mounts.c" struct risk_case { const char *label; const char *path; mode_t mode; const char *mime; const avl_tree_t *languages; unsigned int expected; }; struct dir_case { const char *label; const char *path; unsigned int expected; }; /* * expect_risk - verify that a file path and MIME produce expected risks. * @test: test case describing the path, MIME, language set, and expectation. * Returns nothing. */ static void expect_risk(const struct risk_case *test) { struct stat sb; unsigned int got; memset(&sb, 0, sizeof(sb)); sb.st_mode = S_IFREG | test->mode; got = classify_file_risks(test->path, &sb, test->mime, test->languages); if (got != test->expected) error(1, 0, "%s: expected 0x%x got 0x%x", test->label, test->expected, got); } /* * expect_dir_risk - verify that a directory path produces expected risks. * @test: test case describing the directory path and expectation. * Returns nothing. */ static void expect_dir_risk(const struct dir_case *test) { unsigned int got = classify_dir_risks(test->path); if (got != test->expected) error(1, 0, "%s: expected 0x%x got 0x%x", test->label, test->expected, got); } /* * test_language_mime_loading - load a base language set and package addition. * * The ignored-mount scanner must use the same effective %languages values as * the daemon policy, including duplicate-safe += MIME additions. * Returns nothing. Exits on test failure. */ static void test_language_mime_loading(void) { char path[] = "/tmp/fapolicyd-language-rules.XXXXXX"; avl_tree_t languages; FILE *fp; int fd; fd = mkstemp(path); if (fd == -1) error(1, errno, "mkstemp failed"); fp = fdopen(fd, "w"); if (fp == NULL) { close(fd); unlink(path); error(1, errno, "fdopen failed"); } if (fputs("%languages=text/x-python\n" "%languages+=application/x-vendor-script,text/x-python\n", fp) == EOF) { fclose(fp); unlink(path); error(1, errno, "write language rules failed"); } if (fclose(fp) == EOF) { unlink(path); error(1, errno, "write language rules failed"); } avl_init(&languages, compare_language_entry); if (load_language_mimes_from_file(&languages, path)) { unlink(path); error(1, 0, "language rule extensions did not load"); } if (!mime_is_language(&languages, "text/x-python") || !mime_is_language(&languages, "application/x-vendor-script")) { free_language_mimes(&languages); unlink(path); error(1, 0, "loaded language MIME set was incomplete"); } free_language_mimes(&languages); if (unlink(path)) error(1, errno, "unlink failed"); } /* * nftw() supplies no valid stat buffer when it reports FTW_NS. The callback * must report the incomplete scan without dereferencing that buffer. */ static void test_unstatable_walk_entry(void) { if (inspect_mount_entry("/unstatable", NULL, FTW_NS, NULL) != FTW_CONTINUE) error(1, 0, "FTW_NS callback stopped the walk"); if (scan_state.had_error == 0) error(1, 0, "FTW_NS callback did not report a scan error"); scan_state.had_error = 0; } /* * A pathname can change after nftw() stats it. The reopen helper must retain * only the exact regular object seen by the walk, without following a link or * blocking on a replacement FIFO. */ static void test_walked_open_rejects_replacements(void) { char path[] = "/tmp/fapolicyd-walked-open.XXXXXX"; char replacement_path[] = "/tmp/fapolicyd-walked-replace.XXXXXX"; char link_path[sizeof(path) + 8]; char pipe_path[sizeof(path) + 8]; struct stat walked, opened; int fd, reopened; fd = mkstemp(path); if (fd == -1) error(1, errno, "mkstemp failed"); if (fstat(fd, &walked)) error(1, errno, "fstat failed"); if (close(fd)) error(1, errno, "close failed"); reopened = open_verified_regular_file(path, &walked, &opened); if (reopened == -1) error(1, errno, "regular file reopen failed"); if (opened.st_dev != walked.st_dev || opened.st_ino != walked.st_ino) error(1, 0, "regular file reopen changed identity"); if (close(reopened)) error(1, errno, "reopened file close failed"); fd = mkstemp(replacement_path); if (fd == -1) error(1, errno, "replacement mkstemp failed"); if (close(fd)) error(1, errno, "replacement close failed"); if (rename(replacement_path, path)) error(1, errno, "replacement rename failed"); errno = 0; reopened = open_verified_regular_file(path, &walked, &opened); if (reopened != -1 || errno != ESTALE) error(1, errno, "replacement regular file was accepted"); if (lstat(path, &walked)) error(1, errno, "lstat replacement failed"); snprintf(link_path, sizeof(link_path), "%s.link", path); if (symlink(path, link_path)) error(1, errno, "symlink failed"); reopened = open_verified_regular_file(link_path, &walked, &opened); if (reopened != -1) error(1, 0, "replacement symlink was accepted"); if (unlink(link_path)) error(1, errno, "unlink symlink failed"); snprintf(pipe_path, sizeof(pipe_path), "%s.fifo", path); if (mkfifo(pipe_path, 0600)) error(1, errno, "mkfifo failed"); if (lstat(pipe_path, &walked)) error(1, errno, "lstat fifo failed"); alarm(2); reopened = open_verified_regular_file(pipe_path, &walked, &opened); alarm(0); if (reopened != -1 || errno != ESTALE) error(1, errno, "replacement FIFO was accepted"); if (unlink(pipe_path)) error(1, errno, "unlink fifo failed"); if (unlink(path)) error(1, errno, "unlink regular file failed"); } int main(void) { avl_tree_t languages; struct risk_case risk_cases[] = { { "executable mode", "/mnt/bin/tool", 0755, "text/plain", NULL, RISK_BIT(RISK_EXECUTABLE_REGULAR) }, { "elf shared", "/mnt/lib/libdemo.so", 0644, "application/x-sharedlib", NULL, RISK_BIT(RISK_ELF_SHARED) }, { "archive extension", "/mnt/cache/app.JAR", 0644, "application/octet-stream", NULL, RISK_BIT(RISK_ARCHIVE) }, { "archive mime", "/mnt/cache/blob", 0644, "application/zip", NULL, RISK_BIT(RISK_ARCHIVE) }, { "bytecode cache", "/mnt/pkg/__pycache__/m.pyc", 0644, "application/octet-stream", NULL, RISK_BIT(RISK_BYTECODE) }, { "python bytecode mime", "/mnt/pkg/module", 0644, "application/x-bytecode.python", NULL, RISK_BIT(RISK_BYTECODE) }, { "elisp bytecode mime", "/mnt/pkg/module", 0644, "application/x-elc", NULL, RISK_BIT(RISK_BYTECODE) }, { "zstd archive mime", "/mnt/cache/blob", 0644, "application/zstd", NULL, RISK_BIT(RISK_ARCHIVE) }, { "language mime", "/mnt/scripts/task.py", 0644, "text/x-python", &languages, RISK_BIT(RISK_LANGUAGE) }, { "combined risks", "/mnt/pkg/app.zip", 0755, "application/x-executable", NULL, RISK_BIT(RISK_EXECUTABLE_REGULAR) | RISK_BIT(RISK_ELF_SHARED) | RISK_BIT(RISK_ARCHIVE) }, { NULL, NULL, 0, NULL, NULL, 0 } }; struct dir_case dir_cases[] = { { "pycache dir", "/mnt/pkg/__pycache__", RISK_BIT(RISK_BYTECODE) }, { "runtime dir", "/mnt/pkg/site-packages", RISK_BIT(RISK_PLUGIN_RUNTIME_DIR) }, { "ordinary dir", "/mnt/data/reports", 0 }, { NULL, NULL, 0 } }; avl_init(&languages, compare_language_entry); if (insert_language_mime(&languages, "text/x-python")) error(1, 0, "failed to add language MIME"); test_language_mime_loading(); test_unstatable_walk_entry(); test_walked_open_rejects_replacements(); for (unsigned int i = 0; risk_cases[i].label; i++) expect_risk(&risk_cases[i]); for (unsigned int i = 0; dir_cases[i].label; i++) expect_dir_risk(&dir_cases[i]); free_language_mimes(&languages); return 0; } fapolicyd-2.0/src/tests/lru_test.c000066400000000000000000000116271524071644000172750ustar00rootroot00000000000000#include #include #include #include "lru.h" static unsigned int cleaned; static void cleanup_item(void *item) { if (item) cleaned++; } static void attach_item(QNode *node, int value) { int *data; data = malloc(sizeof(int)); if (data == NULL) error(1, 0, "malloc failed"); *data = value; node->item = data; } static void test_reuse_after_evict(void) { Queue *queue; QNode *first; QNode *second; cleaned = 0; queue = init_lru(3, cleanup_item, "reuse", NULL); if (queue == NULL) error(1, 0, "init_lru failed"); first = check_lru_cache(queue, 0); if (first == NULL) error(1, 0, "check_lru_cache returned NULL"); attach_item(first, 1); lru_evict(queue, 0); if (cleaned != 1) error(1, 0, "cleanup count %u does not match expected 1", cleaned); second = check_lru_cache(queue, 0); if (second != first) error(1, 0, "QNode was not reused after eviction"); if (second->uses != 1) error(1, 0, "QNode uses was not reset on reuse"); attach_item(second, 2); destroy_lru(queue); } static void test_empty_after_single_evict(void) { Queue *queue; QNode *first; cleaned = 0; queue = init_lru(1, cleanup_item, "empty", NULL); if (queue == NULL) error(1, 0, "init_lru failed"); first = check_lru_cache(queue, 0); if (first == NULL) error(1, 0, "check_lru_cache returned NULL"); attach_item(first, 5); lru_evict(queue, 0); if (queue->front != NULL || queue->end != NULL || queue->count != 0) error(1, 0, "single eviction left non-empty queue state"); lru_evict(queue, 0); if (cleaned != 1) error(1, 0, "empty eviction ran cleanup again"); destroy_lru(queue); } static void test_pool_exhaustion(void) { Queue *queue; QNode *first; QNode *second; QNode *reused; cleaned = 0; queue = init_lru(2, cleanup_item, "exhaust", NULL); if (queue == NULL) error(1, 0, "init_lru failed"); first = check_lru_cache(queue, 0); if (first == NULL) error(1, 0, "check_lru_cache returned NULL for key 0"); attach_item(first, 10); second = check_lru_cache(queue, 1); if (second == NULL) error(1, 0, "check_lru_cache returned NULL for key 1"); attach_item(second, 20); if (queue->free_list != NULL) error(1, 0, "free list not empty after filling cache"); lru_evict(queue, 1); if (cleaned != 1) error(1, 0, "cleanup count %u does not match expected 1", cleaned); reused = check_lru_cache(queue, 1); if (reused != second) error(1, 0, "QNode not reused after pool exhaustion"); if (queue->count != 2) error(1, 0, "queue count incorrect after reuse"); attach_item(reused, 30); destroy_lru(queue); } /* * test_metrics_reset - verify reset snapshots preserve cache occupancy. */ static void test_metrics_reset(void) { struct lru_metrics metrics; Queue *queue; QNode *first; queue = init_lru(1, cleanup_item, "metrics", NULL); if (queue == NULL) error(1, 0, "init_lru failed"); first = check_lru_cache(queue, 0); if (first == NULL) error(1, 0, "check_lru_cache returned NULL"); attach_item(first, 40); check_lru_cache(queue, 0); lru_metrics_snapshot(queue, &metrics, 1); if (metrics.count != 1 || metrics.total != 1) error(1, 0, "metrics reset changed cache state"); if (metrics.hits != 1 || metrics.misses != 1) error(1, 0, "metrics reset snapshot lost cache counters"); if (metrics.collisions != 0) error(1, 0, "metrics reset snapshot invented collisions"); lru_metrics_snapshot(queue, &metrics, 0); if (metrics.count != 1 || metrics.hits != 0 || metrics.misses != 0) error(1, 0, "metrics reset did not clear counters only"); destroy_lru(queue); } /* * test_collision_metrics - verify explicit collision counters reset. */ static void test_collision_metrics(void) { struct lru_metrics metrics; Queue *queue; QNode *first; queue = init_lru(1, cleanup_item, "collisions", NULL); if (queue == NULL) error(1, 0, "init_lru failed"); first = check_lru_cache(queue, 0); if (first == NULL) error(1, 0, "check_lru_cache returned NULL"); attach_item(first, 50); lru_record_collision(queue); lru_metrics_snapshot(queue, &metrics, 1); if (metrics.collisions != 1) error(1, 0, "collision snapshot lost cache counter"); lru_metrics_snapshot(queue, &metrics, 0); if (metrics.collisions != 0) error(1, 0, "collision counter was not reset"); destroy_lru(queue); } /* * test_null_queue_metrics - verify missing caches produce printable metrics. */ static void test_null_queue_metrics(void) { struct lru_metrics metrics; lru_metrics_snapshot(NULL, &metrics, 1); if (metrics.name == NULL || strcmp(metrics.name, "Unknown")) error(1, 0, "null queue snapshot did not set a cache name"); if (metrics.count || metrics.total || metrics.hits || metrics.misses || metrics.collisions || metrics.evictions) error(1, 0, "null queue snapshot did not clear counters"); } int main(void) { test_reuse_after_evict(); test_empty_after_single_evict(); test_pool_exhaustion(); test_metrics_reset(); test_collision_metrics(); test_null_queue_metrics(); return 0; } fapolicyd-2.0/src/tests/md5_backend_test.c000066400000000000000000000161171524071644000206260ustar00rootroot00000000000000/* * md5_backend_test.c - verify Debian MD5 backend return classifications */ #include "config.h" #include #include #include #include #include #include #include #include #include #include "fapolicyd-backend.h" #include "file.h" #include "md5-backend.h" #define BAD_MD5 "00000000000000000000000000000000" #define TEST_TEXT "dpkg-owned test file\n" #define CHECK(expr, code, msg) \ do { \ if (!(expr)) \ error(code, 0, "%s", msg); \ } while (0) /* * cleanup_hash_table - release duplicate tracking entries from the MD5 helper. * @table: hash table pointer updated to NULL after cleanup. * Returns nothing. */ static void cleanup_hash_table(struct _hash_record **table) { struct _hash_record *item, *tmp; HASH_ITER(hh, *table, item, tmp) { HASH_DEL(*table, item); free((void *)item->key); free(item); } *table = NULL; } /* * write_all - write a complete buffer to a descriptor. * @fd: open descriptor to write. * @data: nul-terminated test payload. * Returns nothing. Exits on write failure. */ static void write_all(int fd, const char *data) { size_t done = 0; size_t len = strlen(data); while (done < len) { ssize_t written = write(fd, data + done, len - done); if (written < 0) { if (errno == EINTR) continue; error(1, errno, "write failed"); } if (written == 0) error(1, 0, "write made no progress"); done += (size_t)written; } } /* * make_temp_file - create a regular file with the standard test payload. * @path: mkstemp template updated with the created path. * Returns nothing. Exits on setup failure. */ static void make_temp_file(char *path) { int fd = mkstemp(path); if (fd < 0) error(1, errno, "mkstemp failed"); write_all(fd, TEST_TEXT); if (close(fd) < 0) error(1, errno, "close failed"); } /* * make_memfd - create a writable backend snapshot descriptor for tests. * Returns an open memfd. Exits on setup failure. */ static int make_memfd(void) { int fd = memfd_create("md5-backend-test", MFD_CLOEXEC); if (fd < 0) error(1, errno, "memfd_create failed"); return fd; } /* * md5_for_file - compute the MD5 digest expected by the backend helper. * @path: regular file to hash. * Returns a heap-allocated digest string. Exits on setup or hash failure. */ static char *md5_for_file(const char *path) { struct stat sb; char *hash; int fd; fd = open(path, O_RDONLY); if (fd < 0) error(1, errno, "open failed"); if (fstat(fd, &sb) < 0) error(1, errno, "fstat failed"); hash = get_hash_from_fd2(fd, (size_t)sb.st_size, FILE_HASH_ALG_MD5); if (close(fd) < 0) error(1, errno, "close failed"); if (hash == NULL) error(1, 0, "MD5 digest returned NULL"); return hash; } /* * read_snapshot - read a memfd snapshot into a nul-terminated buffer. * @fd: memfd to rewind and read. * @buf: caller-provided destination buffer. * @size: size of @buf. * Returns the number of bytes read. Exits on read failure. */ static ssize_t read_snapshot(int fd, char *buf, size_t size) { ssize_t len; if (lseek(fd, 0, SEEK_SET) < 0) error(1, errno, "lseek failed"); len = read(fd, buf, size - 1); if (len < 0) error(1, errno, "read failed"); buf[len] = 0; return len; } /* * test_hash_mismatch_skips - changed dpkg-owned files must not abort loading. * Returns nothing. Exits on unexpected helper behavior. */ static void test_hash_mismatch_skips(void) { struct _hash_record *hashtable = NULL; char path[] = "/tmp/fapolicyd-md5-backend-XXXXXX"; backend dst = { .name = "test", .memfd = make_memfd(), .entries = -1 }; md5_backend_result_t rc; make_temp_file(path); rc = add_file_to_backend_by_md5(path, BAD_MD5, &hashtable, SRC_DEB, &dst); CHECK(rc == MD5_BACKEND_SKIPPED, 1, "[ERROR:1] hash mismatch was not classified as skipped"); CHECK(hashtable == NULL, 2, "[ERROR:2] skipped hash mismatch added a duplicate entry"); close(dst.memfd); unlink(path); } /* * test_missing_file_skips - dpkg file list races are per-file skips. * Returns nothing. Exits on unexpected helper behavior. */ static void test_missing_file_skips(void) { struct _hash_record *hashtable = NULL; char path[128]; backend dst = { .name = "test", .memfd = make_memfd(), .entries = -1 }; md5_backend_result_t rc; snprintf(path, sizeof(path), "/tmp/fapolicyd-md5-missing-%ld", (long)getpid()); unlink(path); rc = add_file_to_backend_by_md5(path, BAD_MD5, &hashtable, SRC_DEB, &dst); CHECK(rc == MD5_BACKEND_SKIPPED, 3, "[ERROR:3] missing file was not classified as skipped"); CHECK(hashtable == NULL, 4, "[ERROR:4] skipped missing file added a duplicate entry"); close(dst.memfd); } /* * test_matching_file_records - matching dpkg MD5 records a trust snapshot line. * Returns nothing. Exits on malformed output or helper failure. */ static void test_matching_file_records(void) { struct _hash_record *hashtable = NULL; char path[] = "/tmp/fapolicyd-md5-backend-XXXXXX"; char buf[512]; char sha[FILE_DIGEST_STRING_MAX]; unsigned int source; trustdb_size_t size; char *data; char *md5; backend dst = { .name = "test", .memfd = make_memfd(), .entries = -1 }; md5_backend_result_t rc; make_temp_file(path); md5 = md5_for_file(path); rc = add_file_to_backend_by_md5(path, md5, &hashtable, SRC_DEB, &dst); CHECK(rc == MD5_BACKEND_ADDED, 5, "[ERROR:5] matching file was not added"); CHECK(read_snapshot(dst.memfd, buf, sizeof(buf)) > 0, 6, "[ERROR:6] matching file wrote no snapshot data"); data = strchr(buf, ' '); CHECK(data != NULL, 7, "[ERROR:7] snapshot line missing data"); data++; CHECK(sscanf(data, DATA_FORMAT_IN, &source, &size, sha) == 3, 8, "[ERROR:8] snapshot data did not parse"); CHECK(source == SRC_DEB, 9, "[ERROR:9] snapshot source is not SRC_DEB"); CHECK(size == (trustdb_size_t)strlen(TEST_TEXT), 10, "[ERROR:10] snapshot size does not match file"); free(md5); cleanup_hash_table(&hashtable); close(dst.memfd); unlink(path); } /* * test_snapshot_write_failure_is_fatal - failed memfd writes abort snapshots. * Returns nothing. Exits on unexpected helper behavior. */ static void test_snapshot_write_failure_is_fatal(void) { struct _hash_record *hashtable = NULL; char path[] = "/tmp/fapolicyd-md5-backend-XXXXXX"; char *md5; int readonly_fd; backend dst = { .name = "test", .memfd = -1, .entries = -1 }; md5_backend_result_t rc; make_temp_file(path); md5 = md5_for_file(path); readonly_fd = open(path, O_RDONLY); if (readonly_fd < 0) error(1, errno, "open read-only failed"); dst.memfd = readonly_fd; rc = add_file_to_backend_by_md5(path, md5, &hashtable, SRC_DEB, &dst); CHECK(rc == MD5_BACKEND_FATAL, 11, "[ERROR:11] snapshot write failure was not fatal"); CHECK(hashtable == NULL, 12, "[ERROR:12] fatal snapshot write added a duplicate entry"); free(md5); close(readonly_fd); unlink(path); } /* * main - run MD5 backend return classification coverage. * Returns 0 on success. Exits with error() on test failure. */ int main(void) { test_hash_mismatch_skips(); test_missing_file_skips(); test_matching_file_records(); test_snapshot_write_failure_is_fatal(); return 0; } fapolicyd-2.0/src/tests/message_test.c000066400000000000000000000730341524071644000201170ustar00rootroot00000000000000/* * message_test.c - unit tests for async logging in message.c */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "message.h" #define CHECK(expr, code, msg) \ do { \ if (!(expr)) \ error(1, 0, "%s", msg); \ } while (0) /* * struct capture - redirect stderr to a pipe so a test can inspect what * msg() actually wrote, then restore the original stderr afterward. */ struct capture { int saved_stderr; int read_fd; }; /* * capture_start - point stderr at the write end of a fresh pipe. * @cap: capture state to initialize. * Returns nothing. Exits on any setup failure. */ static void capture_start(struct capture *cap) { int fds[2]; if (pipe(fds)) error(1, errno, "pipe failed"); cap->saved_stderr = dup(STDERR_FILENO); if (cap->saved_stderr < 0) error(1, errno, "dup failed"); if (dup2(fds[1], STDERR_FILENO) < 0) error(1, errno, "dup2 failed"); close(fds[1]); if (fcntl(fds[0], F_SETFL, O_NONBLOCK)) error(1, errno, "fcntl failed"); cap->read_fd = fds[0]; } /* * capture_read - copy everything currently buffered in the pipe into @buf. * @cap: active capture. * @buf: destination buffer, always NUL terminated on return. * @size: size of @buf. * Returns nothing. Only call once the writer side is done producing, or * some content may still be in flight and missed. */ static void capture_read(struct capture *cap, char *buf, size_t size) { size_t used = 0; ssize_t n; while (used + 1 < size) { n = read(cap->read_fd, buf + used, size - 1 - used); if (n > 0) { used += (size_t)n; continue; } if (n < 0 && errno == EINTR) continue; break; } buf[used] = 0; } /* * capture_end - restore the original stderr. * @cap: active capture. * Returns nothing. */ static void capture_end(struct capture *cap) { fflush(stderr); if (dup2(cap->saved_stderr, STDERR_FILENO) < 0) error(1, errno, "dup2 restore failed"); close(cap->saved_stderr); } /* * capture_close - release the read end of a finished capture. * @cap: capture to close. * Returns nothing. */ static void capture_close(struct capture *cap) { close(cap->read_fd); } #define ACCOUNTING_STABLE_ITERATIONS 200 /* * wait_for_accounting - block until @a and @b (received/dropped style * atomic counters) sum to at least @target, or until their sum stops * changing for a sustained interval, meaning nothing further will ever * arrive. Bounding the wait this way means a genuine accounting mismatch * fails fast via a later CHECK() instead of hanging until an alarm() * watchdog kills the whole process. * Returns nothing. */ static void wait_for_accounting(atomic_ulong *a, atomic_ulong *b, unsigned long target) { unsigned long last = (unsigned long)-1; int stable = 0; for (;;) { unsigned long total = atomic_load_explicit(a, memory_order_relaxed) + atomic_load_explicit(b, memory_order_relaxed); if (total >= target) return; if (total == last) { if (++stable >= ACCOUNTING_STABLE_ITERATIONS) return; } else { stable = 0; last = total; } struct timespec ts = { 0, 1000000 }; nanosleep(&ts, NULL); } } /* * test_rate_limit_allow - directly exercise message_rate_limit_allow() * across its documented edge cases: fresh state, interval boundaries, * clock rollback, disabled limits, and unavailable clocks. * Returns nothing. Exits with error() on failure. */ static void test_rate_limit_allow(void) { struct message_rate_limit rl = MESSAGE_RATE_LIMIT_INIT(10); struct message_rate_limit disabled = MESSAGE_RATE_LIMIT_INIT(0); struct message_rate_limit negative = MESSAGE_RATE_LIMIT_INIT(-5); int i; CHECK(message_rate_limit_allow(&rl, 1000) == 1, 27, "[ERROR:27] first call on a fresh rate limit was not allowed"); CHECK(message_rate_limit_allow(&rl, 1005) == 0, 28, "[ERROR:28] a call inside the interval was incorrectly allowed"); CHECK(message_rate_limit_allow(&rl, 1009) == 0, 29, "[ERROR:29] a call one second before the interval elapsed was " "incorrectly allowed"); CHECK(message_rate_limit_allow(&rl, 1010) == 1, 30, "[ERROR:30] a call exactly at the interval boundary was not " "allowed"); CHECK(message_rate_limit_allow(&rl, 1011) == 0, 31, "[ERROR:31] a call immediately after a reset was incorrectly " "allowed"); /* a wall-clock rollback must emit immediately rather than wait out * the interval against a now-unreachable future timestamp */ CHECK(message_rate_limit_allow(&rl, 500) == 1, 32, "[ERROR:32] a clock rollback did not immediately allow a " "message"); CHECK(message_rate_limit_allow(&rl, 501) == 0, 33, "[ERROR:33] a call just after a clock-rollback reset was " "incorrectly allowed"); for (i = 0; i < 5; i++) CHECK(message_rate_limit_allow(&disabled, 1000 + i) == 1, 34, "[ERROR:34] a zero-interval rate limit suppressed a " "message"); for (i = 0; i < 5; i++) CHECK(message_rate_limit_allow(&negative, 1000 + i) == 1, 35, "[ERROR:35] a negative-interval rate limit suppressed a " "message"); CHECK(message_rate_limit_allow(&rl, (time_t)-1) == 1, 36, "[ERROR:36] an unavailable clock reading was not always " "allowed"); CHECK(message_rate_limit_allow(NULL, 2000) == 1, 37, "[ERROR:37] a NULL rate limit was not always allowed"); } /* * test_message_mode_quiet - verify msg() emits nothing at all, at any * priority, while in MSG_QUIET mode. * Returns nothing. Exits with error() on failure. */ static void test_message_mode_quiet(void) { struct capture cap; char buf[4096]; set_message_mode(MSG_QUIET, DBG_NO); capture_start(&cap); msg(LOG_EMERG, "quiet-mode-should-not-appear"); msg(LOG_DEBUG, "quiet-mode-debug-should-not-appear"); capture_read(&cap, buf, sizeof(buf)); capture_end(&cap); capture_close(&cap); CHECK(buf[0] == 0, 38, "[ERROR:38] a message was emitted while in MSG_QUIET mode"); } /* * test_debug_gating - verify LOG_DEBUG messages are suppressed unless * debug output was explicitly enabled via set_message_mode(). * Returns nothing. Exits with error() on failure. */ static void test_debug_gating(void) { struct capture cap; char buf[4096]; int rc; set_message_mode(MSG_STDERR, DBG_NO); rc = message_async_start(); CHECK(rc == 0, 59, "[ERROR:59] message_async_start failed"); capture_start(&cap); msg(LOG_DEBUG, "debug-hidden-message"); message_async_stop(); capture_read(&cap, buf, sizeof(buf)); capture_end(&cap); capture_close(&cap); CHECK(buf[0] == 0, 39, "[ERROR:39] a LOG_DEBUG message was emitted with debug " "disabled"); set_message_mode(MSG_STDERR, DBG_YES); rc = message_async_start(); CHECK(rc == 0, 59, "[ERROR:59] message_async_start failed"); capture_start(&cap); msg(LOG_DEBUG, "debug-visible-message"); message_async_stop(); capture_read(&cap, buf, sizeof(buf)); capture_end(&cap); capture_close(&cap); CHECK(strstr(buf, "debug-visible-message") != NULL, 40, "[ERROR:40] a LOG_DEBUG message was suppressed with debug " "enabled"); set_message_mode(MSG_QUIET, DBG_NO); } /* * test_basic_async_logging - verify msg() reaches stderr once the async * writer thread is running, and message_async_stop() drains it. * Returns nothing. Exits with error() on failure. */ static void test_basic_async_logging(void) { struct capture cap; char buf[4096]; int rc; set_message_mode(MSG_STDERR, DBG_NO); rc = message_async_start(); CHECK(rc == 0, 3, "[ERROR:3] message_async_start failed"); capture_start(&cap); msg(LOG_WARNING, "async-basic-%d", 67890); message_async_stop(); capture_read(&cap, buf, sizeof(buf)); capture_end(&cap); capture_close(&cap); CHECK(strstr(buf, "async-basic-67890") != NULL, 4, "[ERROR:4] async message missing from stderr after stop"); set_message_mode(MSG_QUIET, DBG_NO); } /* * test_msg_without_async_start - verify msg() still delivers via the * best-effort path when the drain thread was never started (e.g. every CLI * tool that never calls message_async_start()). * Returns nothing. Exits with error() on failure. */ static void test_msg_without_async_start(void) { struct capture cap; char buf[4096]; set_message_mode(MSG_STDERR, DBG_NO); capture_start(&cap); msg(LOG_WARNING, "no-async-%d", 24680); capture_read(&cap, buf, sizeof(buf)); capture_end(&cap); capture_close(&cap); CHECK(strstr(buf, "no-async-24680") != NULL, 60, "[ERROR:60] msg() produced no output when async logging was " "never started"); set_message_mode(MSG_QUIET, DBG_NO); } /* * test_literal_percent_in_message - verify data passed as a msg() argument * (not as its format string) reaches stderr byte-for-byte. log_write() * forwards queued text via a literal "%s" (see message.c), so a * caller-controlled '%' must never be re-interpreted as a format specifier * on the way out. * Returns nothing. Exits with error() on failure. */ static void test_literal_percent_in_message(void) { struct capture cap; char buf[4096]; const char *payload = "50% off %n and %s and %x should stay literal"; int rc; set_message_mode(MSG_STDERR, DBG_NO); rc = message_async_start(); CHECK(rc == 0, 42, "[ERROR:42] message_async_start failed"); capture_start(&cap); msg(LOG_WARNING, "data: %s", payload); message_async_stop(); capture_read(&cap, buf, sizeof(buf)); capture_end(&cap); capture_close(&cap); CHECK(strstr(buf, payload) != NULL, 43, "[ERROR:43] format specifiers in message data were not " "preserved literally"); set_message_mode(MSG_QUIET, DBG_NO); } /* * test_async_start_failure - verify message_async_start() reports failure * when the writer thread cannot be created, and recovers cleanly afterward. * * RLIMIT_NPROC is not enforced for privileged processes, so this test skips * itself (with a visible note, not a failure) when it cannot actually force * pthread_create() to fail - that is an environment limitation, not a bug. * Returns nothing. Exits with error() on failure. */ static void test_async_start_failure(void) { struct rlimit orig, tiny; int rc; if (getrlimit(RLIMIT_NPROC, &orig)) error(1, errno, "getrlimit failed"); tiny.rlim_cur = 1; tiny.rlim_max = orig.rlim_max; if (setrlimit(RLIMIT_NPROC, &tiny)) { fprintf(stderr, "[SKIP] cannot lower RLIMIT_NPROC here, " "skipping message_async_start failure test\n"); return; } rc = message_async_start(); /* Restore before asserting anything, so a failed CHECK() below does * not leave the process thread-starved on exit. */ if (setrlimit(RLIMIT_NPROC, &orig)) error(1, errno, "setrlimit restore failed"); if (rc == 0) { fprintf(stderr, "[SKIP] RLIMIT_NPROC not enforced here " "(root or a sandbox); cannot force " "message_async_start() to fail, skipping\n"); message_async_stop(); return; } CHECK(rc != 0, 5, "[ERROR:5] message_async_start unexpectedly " "reported success"); /* A failed start must not corrupt state: once resources free up, * a later start must still succeed. */ rc = message_async_start(); CHECK(rc == 0, 6, "[ERROR:6] message_async_start did not recover " "after a failed attempt"); message_async_stop(); } /* * test_long_messages - verify a full-length path is logged intact, and a * grossly oversized message is truncated with a visible marker rather than * silently cut off. * Returns nothing. Exits with error() on failure. */ static void test_long_messages(void) { struct capture cap; char buf[32768]; char *path, *huge; size_t path_len = (size_t)PATH_MAX - 1; size_t huge_len = 3 * (size_t)PATH_MAX; int rc; path = malloc(path_len + 1); huge = malloc(huge_len + 1); if (!path || !huge) error(1, errno, "malloc failed"); memset(path, 'a', path_len); path[path_len] = 0; memset(huge, 'b', huge_len); huge[huge_len] = 0; set_message_mode(MSG_STDERR, DBG_NO); rc = message_async_start(); CHECK(rc == 0, 7, "[ERROR:7] message_async_start failed"); capture_start(&cap); msg(LOG_NOTICE, "Cannot open %s", path); msg(LOG_NOTICE, "%s", huge); message_async_stop(); capture_read(&cap, buf, sizeof(buf)); capture_end(&cap); capture_close(&cap); CHECK(strstr(buf, path) != NULL, 8, "[ERROR:8] a full-length path message was truncated"); CHECK(strstr(buf, "[truncated]") != NULL, 9, "[ERROR:9] a grossly oversized message was not marked " "truncated"); free(path); free(huge); set_message_mode(MSG_QUIET, DBG_NO); } /* * test_truncation_boundary_exact - verify the truncation decision at the * exact slot-size boundary, not just deep inside/outside it: a message one * byte under the internal LOG_SLOT_SIZE limit must survive intact, and one * exactly at the limit must be marked truncated. slot_size below mirrors * LOG_SLOT_SIZE (PATH_MAX * 2) in message.c, which isn't exposed to tests. * Returns nothing. Exits with error() on failure. */ static void test_truncation_boundary_exact(void) { struct capture cap; char buf[32768]; char *exact_fit, *one_over; size_t slot_size = (size_t)PATH_MAX * 2; size_t fit_len = slot_size - 1; int rc; exact_fit = malloc(fit_len + 1); one_over = malloc(slot_size + 1); if (!exact_fit || !one_over) error(1, errno, "malloc failed"); memset(exact_fit, 'c', fit_len); exact_fit[fit_len] = 0; memset(one_over, 'd', slot_size); one_over[slot_size] = 0; set_message_mode(MSG_STDERR, DBG_NO); rc = message_async_start(); CHECK(rc == 0, 44, "[ERROR:44] message_async_start failed"); capture_start(&cap); msg(LOG_NOTICE, "%s", exact_fit); msg(LOG_NOTICE, "MARK-AFTER-EXACT-FIT"); message_async_stop(); capture_read(&cap, buf, sizeof(buf)); capture_end(&cap); capture_close(&cap); CHECK(strstr(buf, exact_fit) != NULL, 45, "[ERROR:45] a message one byte under the slot limit was " "altered"); CHECK(strstr(buf, "[truncated]") == NULL, 46, "[ERROR:46] a message that fit exactly was marked truncated"); CHECK(strstr(buf, "MARK-AFTER-EXACT-FIT") != NULL, 47, "[ERROR:47] the message following an exact-fit message was " "lost"); rc = message_async_start(); CHECK(rc == 0, 48, "[ERROR:48] message_async_start failed"); capture_start(&cap); msg(LOG_NOTICE, "%s", one_over); message_async_stop(); capture_read(&cap, buf, sizeof(buf)); capture_end(&cap); capture_close(&cap); CHECK(strstr(buf, "[truncated]") != NULL, 49, "[ERROR:49] a message exactly at the slot limit was not " "marked truncated"); free(exact_fit); free(one_over); set_message_mode(MSG_QUIET, DBG_NO); } /* * test_drain_on_stop - verify every message queued before * message_async_stop() is called is still written before it returns. * Returns nothing. Exits with error() on failure. */ static void test_drain_on_stop(void) { struct capture cap; char buf[16384]; char expect[32]; int rc, i; const int n = 50; set_message_mode(MSG_STDERR, DBG_NO); rc = message_async_start(); CHECK(rc == 0, 10, "[ERROR:10] message_async_start failed"); capture_start(&cap); for (i = 0; i < n; i++) msg(LOG_NOTICE, "drain-marker-%d", i); message_async_stop(); capture_read(&cap, buf, sizeof(buf)); capture_end(&cap); capture_close(&cap); for (i = 0; i < n; i++) { snprintf(expect, sizeof(expect), "drain-marker-%d", i); CHECK(strstr(buf, expect) != NULL, 11, "[ERROR:11] a message queued before stop() was never " "drained"); } set_message_mode(MSG_QUIET, DBG_NO); } #define PRODUCER_THREADS 16 #define MESSAGES_PER_THREAD 10 struct producer_arg { int id; }; /* * producer_thread - log a fixed, identifiable burst of messages. * @arg: struct producer_arg pointer. * Returns NULL. */ static void *producer_thread(void *arg) { struct producer_arg *pa = arg; int i; for (i = 0; i < MESSAGES_PER_THREAD; i++) msg(LOG_NOTICE, "producer-%d-msg-%d", pa->id, i); return NULL; } /* * test_concurrent_producers - verify many threads logging at once neither * crash nor corrupt each other's messages. * Returns nothing. Exits with error() on failure. */ static void test_concurrent_producers(void) { struct capture cap; char buf[65536]; pthread_t threads[PRODUCER_THREADS]; struct producer_arg args[PRODUCER_THREADS]; char expect[48]; int rc, i, j; set_message_mode(MSG_STDERR, DBG_NO); rc = message_async_start(); CHECK(rc == 0, 12, "[ERROR:12] message_async_start failed"); capture_start(&cap); for (i = 0; i < PRODUCER_THREADS; i++) { args[i].id = i; rc = pthread_create(&threads[i], NULL, producer_thread, &args[i]); CHECK(rc == 0, 13, "[ERROR:13] pthread_create failed"); } for (i = 0; i < PRODUCER_THREADS; i++) { rc = pthread_join(threads[i], NULL); CHECK(rc == 0, 14, "[ERROR:14] pthread_join failed"); } message_async_stop(); capture_read(&cap, buf, sizeof(buf)); capture_end(&cap); capture_close(&cap); for (i = 0; i < PRODUCER_THREADS; i++) { for (j = 0; j < MESSAGES_PER_THREAD; j++) { snprintf(expect, sizeof(expect), "producer-%d-msg-%d", i, j); CHECK(strstr(buf, expect) != NULL, 15, "[ERROR:15] a concurrently-produced message " "was lost or corrupted"); } } set_message_mode(MSG_QUIET, DBG_NO); } /* * test_async_start_stop_idempotent - verify message_async_stop() is a safe * no-op when nothing is running, and a redundant message_async_start() * call while already running neither fails nor disturbs the running * writer thread. * Returns nothing. Exits with error() on failure. */ static void test_async_start_stop_idempotent(void) { struct capture cap; char buf[4096]; int rc; /* stopping when nothing is running must be a safe no-op */ message_async_stop(); message_async_stop(); set_message_mode(MSG_STDERR, DBG_NO); rc = message_async_start(); CHECK(rc == 0, 56, "[ERROR:56] message_async_start failed"); rc = message_async_start(); CHECK(rc == 0, 57, "[ERROR:57] a redundant message_async_start() " "call while already running reported failure"); capture_start(&cap); msg(LOG_NOTICE, "idempotent-start-check"); message_async_stop(); capture_read(&cap, buf, sizeof(buf)); capture_end(&cap); capture_close(&cap); CHECK(strstr(buf, "idempotent-start-check") != NULL, 58, "[ERROR:58] logging broke after a redundant " "message_async_start() call"); /* stopping twice in a row after a real stop must also be safe */ message_async_stop(); set_message_mode(MSG_QUIET, DBG_NO); } #define BLOCK_TEST_MESSAGES 40 #define BLOCK_TEST_PAYLOAD 2000 static atomic_bool reader_should_stop; static volatile sig_atomic_t watchdog_fired; /* * watchdog_handler - last-resort exit if the blocking test hangs. * @sig: unused signal number. * Returns nothing; terminates the process. */ static void watchdog_handler(int sig) { (void)sig; watchdog_fired = 1; _exit(99); } /* * pipe_reader_thread - drain and discard the capture pipe in the * background so a stalled drain thread can make progress again. * @arg: struct capture pointer. * Returns NULL. */ static void *pipe_reader_thread(void *arg) { struct capture *cap = arg; char discard[4096]; while (!atomic_load_explicit(&reader_should_stop, memory_order_relaxed)) { ssize_t n = read(cap->read_fd, discard, sizeof(discard)); if (n > 0) continue; if (n == 0) break; if (errno == EAGAIN || errno == EWOULDBLOCK) { struct timespec ts = { 0, 1000000 }; nanosleep(&ts, NULL); continue; } if (errno == EINTR) continue; break; } return NULL; } #define OVERFLOW_TEST_MESSAGES 400 #define OVERFLOW_TEST_PAYLOAD 2000 #define OVERFLOW_MSG_MARK "overflow-test-marker" #define PIPE_FILLER_CHUNK 65536 static atomic_ulong overflow_received; static atomic_ulong overflow_dropped_reported; /* * overflow_reader_thread - drain the capture pipe, tallying every * delivered overflow marker and every count from the drain thread's own * "Dropped N messages" report, so a flood that outruns the ring buffer * doesn't need to be retained in memory to prove nothing vanished * silently. Uses a tail-carry technique so a match split across two reads * is still counted exactly once. * @arg: struct capture pointer. * Returns NULL. */ static void *overflow_reader_thread(void *arg) { struct capture *cap = arg; char tail[64] = { 0 }; size_t tail_len = 0; while (!atomic_load_explicit(&reader_should_stop, memory_order_relaxed)) { char work[8192]; ssize_t n; memcpy(work, tail, tail_len); n = read(cap->read_fd, work + tail_len, sizeof(work) - sizeof(tail) - 1); if (n > 0) { size_t total = tail_len + (size_t)n; size_t keep; const char *p; work[total] = 0; p = work; while ((p = strstr(p, OVERFLOW_MSG_MARK)) != NULL) { size_t idx = (size_t)(p - work); if (idx + strlen(OVERFLOW_MSG_MARK) > tail_len) atomic_fetch_add_explicit( &overflow_received, 1, memory_order_relaxed); p += strlen(OVERFLOW_MSG_MARK); } p = work; while ((p = strstr(p, "Dropped ")) != NULL) { size_t idx = (size_t)(p - work); unsigned long n_dropped; if (idx + strlen("Dropped ") > tail_len && sscanf(p, "Dropped %lu", &n_dropped) == 1) atomic_fetch_add_explicit( &overflow_dropped_reported, n_dropped, memory_order_relaxed); p += strlen("Dropped "); } keep = total < sizeof(tail) ? total : sizeof(tail); memcpy(tail, work + total - keep, keep); tail_len = keep; continue; } if (n == 0) break; if (errno == EAGAIN || errno == EWOULDBLOCK) { struct timespec ts = { 0, 1000000 }; nanosleep(&ts, NULL); continue; } if (errno == EINTR) continue; break; } return NULL; } /* * test_queue_overflow_drops_and_reports - stall the log destination so the * ring buffer fills past LOG_QUEUE_DEPTH, then verify excess messages are * dropped (never corrupted, never hung) and the drain thread's own * drop-count report exactly accounts for what went missing. * * The pipe is pre-filled to capacity with raw filler bytes before any * msg() call is made, so the drain thread's very first write() blocks * immediately. Without that, the drain thread can slip in a handful of * successful writes (whatever fits in the pipe's own buffer) interleaved * with the producer loop, occasionally observing and reporting a tiny * non-zero drop count seconds before the real overflow develops; the * process-global 30-second throttle on log_drop_limit (message.c) then * swallows the real, much larger count, and this test's accounting never * converges. Pre-filling collapses the whole burst into one uninterrupted * window with a single check-and-report at the end. * * Must also run before any other test that could trigger a drop: that same * throttle has no reset hook, so only the *first* drop in the whole test * binary is guaranteed to be reported immediately. * Returns nothing. Exits with error() on failure/hang/unaccounted loss. */ static void test_queue_overflow_drops_and_reports(void) { pthread_t reader; struct capture cap; char *payload, *filler; unsigned long received, dropped; int rc, i, flags; payload = malloc(OVERFLOW_TEST_PAYLOAD + 1); filler = malloc(PIPE_FILLER_CHUNK); if (!payload || !filler) error(1, errno, "malloc failed"); memset(payload, 'o', OVERFLOW_TEST_PAYLOAD); payload[OVERFLOW_TEST_PAYLOAD] = 0; memset(filler, 'f', PIPE_FILLER_CHUNK); atomic_store_explicit(&overflow_received, 0, memory_order_relaxed); atomic_store_explicit(&overflow_dropped_reported, 0, memory_order_relaxed); atomic_store_explicit(&reader_should_stop, false, memory_order_relaxed); watchdog_fired = 0; signal(SIGALRM, watchdog_handler); alarm(20); set_message_mode(MSG_STDERR, DBG_NO); capture_start(&cap); flags = fcntl(STDERR_FILENO, F_GETFL); CHECK(flags != -1, 50, "[ERROR:50] fcntl F_GETFL on stderr failed"); rc = fcntl(STDERR_FILENO, F_SETFL, flags | O_NONBLOCK); CHECK(rc == 0, 50, "[ERROR:50] fcntl F_SETFL on stderr failed"); while (write(STDERR_FILENO, filler, PIPE_FILLER_CHUNK) > 0) ; rc = fcntl(STDERR_FILENO, F_SETFL, flags); CHECK(rc == 0, 50, "[ERROR:50] restoring stderr flags failed"); rc = message_async_start(); CHECK(rc == 0, 50, "[ERROR:50] message_async_start failed"); /* * The pipe is already full, so the drain thread's first write() * blocks as soon as it picks up message #1. Everything from here * until the reader thread starts purely enqueues into or drops from * the ring, with no interleaved draining. */ for (i = 0; i < OVERFLOW_TEST_MESSAGES; i++) msg(LOG_NOTICE, "%s-%s", OVERFLOW_MSG_MARK, payload); rc = pthread_create(&reader, NULL, overflow_reader_thread, &cap); CHECK(rc == 0, 51, "[ERROR:51] pthread_create for reader failed"); message_async_stop(); /* let the reader thread catch up with data already sitting in the * pipe; the alarm(20) watchdog above is the backstop if it never * converges, so it must stay armed across this wait */ wait_for_accounting(&overflow_received, &overflow_dropped_reported, OVERFLOW_TEST_MESSAGES); received = atomic_load_explicit(&overflow_received, memory_order_relaxed); dropped = atomic_load_explicit(&overflow_dropped_reported, memory_order_relaxed); atomic_store_explicit(&reader_should_stop, true, memory_order_relaxed); pthread_join(reader, NULL); capture_end(&cap); capture_close(&cap); alarm(0); free(payload); free(filler); set_message_mode(MSG_QUIET, DBG_NO); CHECK(!watchdog_fired, 52, "[ERROR:52] watchdog fired during the " "queue overflow test"); CHECK(received < OVERFLOW_TEST_MESSAGES, 53, "[ERROR:53] no message was actually dropped - the overflow " "path was never exercised"); CHECK(dropped > 0, 54, "[ERROR:54] the drain thread never reported " "the messages it dropped"); CHECK(received + dropped == OVERFLOW_TEST_MESSAGES, 55, "[ERROR:55] a message vanished without being delivered or " "counted as dropped"); } /* * test_msg_direct_does_not_block - verify msg_direct() returns promptly even * when stderr itself is stalled (a full, unread pipe), since it never * touches the drain thread's queue at all - it must skip the write, not * block on it. * Returns nothing. Exits with error() on failure. */ static void test_msg_direct_does_not_block(void) { struct capture cap; char filler[4096]; struct timespec start, end; double elapsed; memset(filler, 'x', sizeof(filler)); set_message_mode(MSG_STDERR, DBG_NO); capture_start(&cap); /* Fill the pipe with nothing reading it. msg_direct() must attempt * only a nonblocking write and return promptly. */ if (fcntl(STDERR_FILENO, F_SETFL, O_NONBLOCK)) error(1, errno, "fcntl failed"); while (write(STDERR_FILENO, filler, sizeof(filler)) > 0) ; clock_gettime(CLOCK_MONOTONIC, &start); msg_direct(LOG_NOTICE, "should-not-block"); clock_gettime(CLOCK_MONOTONIC, &end); capture_end(&cap); capture_close(&cap); set_message_mode(MSG_QUIET, DBG_NO); elapsed = (double)(end.tv_sec - start.tv_sec) + (double)(end.tv_nsec - start.tv_nsec) / 1e9; CHECK(elapsed < 1.0, 61, "[ERROR:61] msg_direct() blocked while " "stderr was stalled"); } /* * test_producer_does_not_block - verify msg() returns promptly even while * the log destination itself is stalled (a full, unread pipe standing in * for a wedged journald/stderr), which is the entire reason this async * logger exists. * Returns nothing. Exits with error() on failure. */ static void test_producer_does_not_block(void) { struct capture cap; pthread_t reader; char *payload; struct timespec start, end; double elapsed; int rc, i; payload = malloc(BLOCK_TEST_PAYLOAD + 1); if (!payload) error(1, errno, "malloc failed"); memset(payload, 'x', BLOCK_TEST_PAYLOAD); payload[BLOCK_TEST_PAYLOAD] = 0; atomic_store_explicit(&reader_should_stop, false, memory_order_relaxed); watchdog_fired = 0; signal(SIGALRM, watchdog_handler); alarm(10); set_message_mode(MSG_STDERR, DBG_NO); rc = message_async_start(); CHECK(rc == 0, 16, "[ERROR:16] message_async_start failed"); capture_start(&cap); /* * Nothing reads the pipe yet, and BLOCK_TEST_MESSAGES * * BLOCK_TEST_PAYLOAD comfortably exceeds a pipe's default 64KiB * buffer, so the drain thread is expected to be stuck inside * write() well before this loop finishes. If msg() blocked on the * same destination, this loop would take as long as the pipe stays * full; it must not. */ clock_gettime(CLOCK_MONOTONIC, &start); for (i = 0; i < BLOCK_TEST_MESSAGES; i++) msg(LOG_NOTICE, "%s", payload); clock_gettime(CLOCK_MONOTONIC, &end); elapsed = (double)(end.tv_sec - start.tv_sec) + (double)(end.tv_nsec - start.tv_nsec) / 1e9; CHECK(elapsed < 2.0, 17, "[ERROR:17] msg() blocked the producer " "while the log destination was stalled"); /* Now unstick the drain thread so shutdown can complete. */ rc = pthread_create(&reader, NULL, pipe_reader_thread, &cap); CHECK(rc == 0, 18, "[ERROR:18] pthread_create for pipe reader " "failed"); message_async_stop(); alarm(0); atomic_store_explicit(&reader_should_stop, true, memory_order_relaxed); pthread_join(reader, NULL); capture_end(&cap); capture_close(&cap); free(payload); set_message_mode(MSG_QUIET, DBG_NO); CHECK(!watchdog_fired, 19, "[ERROR:19] watchdog fired during " "blocking test"); } /* * main - run all message.c logging tests. * Returns 0 on success. Exits with error() on test failure. */ int main(void) { test_rate_limit_allow(); test_message_mode_quiet(); test_debug_gating(); test_basic_async_logging(); test_msg_without_async_start(); test_literal_percent_in_message(); test_async_start_failure(); test_long_messages(); test_truncation_boundary_exact(); test_drain_on_stop(); test_queue_overflow_drops_and_reports(); test_async_start_stop_idempotent(); test_concurrent_producers(); test_msg_direct_does_not_block(); test_producer_does_not_block(); return 0; } fapolicyd-2.0/src/tests/notify_test.c000066400000000000000000001565761524071644000200200ustar00rootroot00000000000000/* * notify_test.c - unit tests for daemon fanotify metadata handling */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "failure-action.h" #include "fanotify-fs-error.h" #include "notify.h" #include "decision-context.h" #include "decision-worker.h" #include "policy.h" #include "decision-config.h" #include "decision-defer.h" #include "decision-timing.h" #include "event.h" #include "lru.h" #include "process.h" #include "state-report.h" #include "subject.h" #include "systemd-notify.h" #ifndef FAN_Q_OVERFLOW #define FAN_Q_OVERFLOW 0x00004000 #endif #if defined(FAPOLICYD_ENABLE_FANOTIFY_FS_ERROR) && \ defined(FAN_FS_ERROR) && defined(FAN_REPORT_FID) && \ defined(FAN_MARK_FILESYSTEM) && \ defined(FAN_EVENT_INFO_TYPE_ERROR) && \ defined(FAN_EVENT_INFO_TYPE_FID) #define TEST_HAVE_FAN_FS_ERROR 1 struct test_fanotify_fs_error_info { struct fanotify_event_info_header hdr; int32_t error; uint32_t error_count; }; #else #define TEST_HAVE_FAN_FS_ERROR 0 #endif extern atomic_bool run_stats; extern atomic_uint signal_report_requests; extern conf_t config; #define DISPATCH_TRACE_CAP 32 #define CHECK(expr, code, msg) \ do { \ if (!(expr)) \ error(1, 0, "%s", msg); \ } while (0) struct dispatch_trace_event { pid_t pid; int fd; }; struct dispatch_trace_observed { unsigned int worker; pid_t pid; int fd; }; /* * seed_building_subject - place one live BUILDING subject in the cache. * @pid: live process id used for the cached subject fingerprint. * @slot: subject-cache slot that should be occupied. * * The deferred-dispatch regression needs event_subject_slot_is_blocked() to * take the same branch it takes for a real pre-STATE_FULL subject. Use a live * proc fingerprint so the stale-BUILDING guard does not evict the fixture. * * Returns nothing. Exits on test failure. */ static void seed_building_subject(pid_t pid, unsigned int slot) { struct decision_context *ctx = decision_context_current(); struct proc_info *info; s_array *subject; QNode *node; CHECK(ctx != NULL && ctx->subject_cache != NULL, 151, "[ERROR:151] decision context missing subject cache"); info = stat_proc_entry(pid); CHECK(info != NULL, 152, "[ERROR:152] failed to create subject fingerprint"); subject = malloc(sizeof(*subject)); CHECK(subject != NULL, 153, "[ERROR:153] failed to allocate subject fixture"); CHECK(subject_create(subject) == 0, 154, "[ERROR:154] failed to initialize subject fixture"); subject->info = info; node = check_lru_cache(ctx->subject_cache, slot); CHECK(node != NULL && node->item == NULL, 155, "[ERROR:155] failed to reserve subject cache slot"); node->item = subject; } void do_stat_report_reset(FILE *f, int shutdown, int reset) { (void)f; (void)shutdown; (void)reset; } void do_state_report(FILE *f, int shutdown) { (void)f; (void)shutdown; } void do_metrics_report_reset(FILE *f, int reset) { (void)f; (void)reset; } /* * read_decision_report - capture decision_report output for assertions. * @buf: destination buffer. * @size: size of @buf. * Returns nothing. Exits if the temporary stream cannot be used. */ static void read_decision_report(char *buf, size_t size, int reset) { FILE *f = tmpfile(); size_t used; if (f == NULL) error(1, 0, "tmpfile failed"); decision_report_reset(f, reset); fflush(f); rewind(f); used = fread(buf, 1, size - 1, f); buf[used] = 0; fclose(f); } /* * read_decision_metrics_report - capture metrics decision header output. * @buf: destination buffer. * @size: size of @buf. * Returns nothing. Exits if the temporary stream cannot be used. */ static void read_decision_metrics_report(char *buf, size_t size) { FILE *f = tmpfile(); size_t used; if (f == NULL) error(1, 0, "tmpfile failed"); decision_report_metrics_reset(f, 0); fflush(f); rewind(f); used = fread(buf, 1, size - 1, f); buf[used] = 0; fclose(f); } /* * read_fanotify_queue_report - capture fanotify queue metrics output. * @buf: destination buffer. * @size: size of @buf. * @reset: non-zero resets interval queue counters. * Returns nothing. Exits if the temporary stream cannot be used. */ static void read_fanotify_queue_report(char *buf, size_t size, int reset) { FILE *f = tmpfile(); size_t used; if (f == NULL) error(1, 0, "tmpfile failed"); fanotify_queue_report_reset(f, reset); fflush(f); rewind(f); used = fread(buf, 1, size - 1, f); buf[used] = 0; fclose(f); } /* * read_fanotify_health_report - capture fanotify worker health output. * @buf: destination buffer. * @size: size of @buf. * Returns nothing. Exits if the temporary stream cannot be used. */ static void read_fanotify_health_report(char *buf, size_t size) { FILE *f = tmpfile(); size_t used; if (f == NULL) error(1, 0, "tmpfile failed"); fanotify_queue_health_report(f); fflush(f); rewind(f); used = fread(buf, 1, size - 1, f); buf[used] = 0; fclose(f); } /* * test_nonblocking_handler_returns - run an empty nonblocking read in a child. * @set_fd: test hook that installs the empty pipe descriptor. * @handler: fanotify read loop under test. * @fd: nonblocking read side of an empty pipe. * * The alarm makes the regression fail rather than hanging the test suite when * a handler retries EAGAIN forever. Returns nothing. Exits on test failure. */ static void test_nonblocking_handler_returns(void (*set_fd)(int), void (*handler)(void), int fd) { int status; pid_t pid; pid = fork(); CHECK(pid >= 0, 168, "[ERROR:168] fork failed"); if (pid == 0) { set_fd(fd); alarm(1); handler(); _exit(0); } CHECK(waitpid(pid, &status, 0) == pid, 169, "[ERROR:169] waitpid failed"); CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0, 170, "[ERROR:170] nonblocking fanotify handler did not return"); } /* * test_nonblocking_fanotify_reads - verify EAGAIN ends both fanotify drains. * * A poll readiness notification can race with another drain. The next * nonblocking read then returns EAGAIN and must return to poll instead of * spinning. Returns nothing. Exits on test failure. */ static void test_nonblocking_fanotify_reads(void) { int pipe_fds[2]; int flags; CHECK(pipe(pipe_fds) == 0, 171, "[ERROR:171] nonblocking pipe setup failed"); flags = fcntl(pipe_fds[0], F_GETFL); CHECK(flags >= 0, 172, "[ERROR:172] failed reading pipe flags"); CHECK(fcntl(pipe_fds[0], F_SETFL, flags | O_NONBLOCK) == 0, 173, "[ERROR:173] failed setting nonblocking pipe"); test_nonblocking_handler_returns(test_notify_set_fanotify_fd, handle_events, pipe_fds[0]); test_nonblocking_handler_returns(test_fanotify_fs_error_set_fd, fanotify_fs_error_handle_events, pipe_fds[0]); close(pipe_fds[0]); close(pipe_fds[1]); } /* * test_fatal_signal_fanotify_close - verify emergency descriptor ownership. * * The first close must release the registered descriptor. A later emergency * close must not close an unrelated descriptor that reused the same integer. * Returns nothing. Exits on test failure. */ static void test_fatal_signal_fanotify_close(void) { int pipe_fds[2]; int group_fd; CHECK(pipe(pipe_fds) == 0, 193, "[ERROR:193] emergency close pipe setup failed"); group_fd = pipe_fds[0]; test_notify_set_fanotify_fd(group_fd); fanotify_close_on_fatal_signal(); errno = 0; CHECK(fcntl(group_fd, F_GETFD) == -1 && errno == EBADF, 194, "[ERROR:194] emergency close retained fanotify descriptor"); CHECK(dup2(pipe_fds[1], group_fd) == group_fd, 195, "[ERROR:195] failed reusing closed fanotify descriptor"); fanotify_close_on_fatal_signal(); CHECK(fcntl(group_fd, F_GETFD) >= 0, 196, "[ERROR:196] repeated emergency close closed reused descriptor"); test_notify_set_fanotify_fd(-1); close(group_fd); close(pipe_fds[1]); } /* * test_shutdown_fanotify_close - keep late replies off a reused group fd. * * Normal shutdown must close the group before worker joins to release watched * self-opens. A late worker still carries the original descriptor number, so * prove its cleanup closes the event fd without writing to a pipe that reused * the retired group descriptor. * * Returns nothing. Exits on test failure. */ static void test_shutdown_fanotify_close(void) { struct fanotify_event_metadata metadata = { .event_len = sizeof(metadata), .vers = FANOTIFY_METADATA_VERSION, .mask = FAN_OPEN_PERM, .pid = 1234, }; char byte; int event[2], group[2], reused[2]; int flags, group_fd; CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, group) == 0, 210, "[ERROR:210] shutdown group socket setup failed"); CHECK(pipe(reused) == 0, 211, "[ERROR:211] shutdown reused pipe setup failed"); CHECK(pipe(event) == 0, 212, "[ERROR:212] shutdown event pipe setup failed"); group_fd = group[0]; metadata.fd = event[0]; CHECK(reply_event_init(group_fd) == 0, 213, "[ERROR:213] shutdown response registration failed"); test_notify_set_fanotify_fd(group_fd); fanotify_close_for_shutdown(); errno = 0; CHECK(fcntl(group_fd, F_GETFD) == -1 && errno == EBADF, 214, "[ERROR:214] shutdown retained fanotify descriptor"); CHECK(dup2(reused[1], group_fd) == group_fd, 215, "[ERROR:215] failed reusing shutdown fanotify descriptor"); flags = fcntl(reused[0], F_GETFL); CHECK(flags >= 0 && fcntl(reused[0], F_SETFL, flags | O_NONBLOCK) == 0, 216, "[ERROR:216] failed setting reused pipe nonblocking"); reply_event(group_fd, &metadata, FAN_ALLOW, NULL); errno = 0; CHECK(read(reused[0], &byte, sizeof(byte)) == -1 && errno == EAGAIN, 217, "[ERROR:217] late reply wrote to reused descriptor"); errno = 0; CHECK(close(event[0]) == -1 && errno == EBADF, 218, "[ERROR:218] late reply retained event descriptor"); fanotify_close_for_shutdown(); CHECK(fcntl(group_fd, F_GETFD) >= 0, 219, "[ERROR:219] repeated shutdown closed reused descriptor"); test_notify_set_fanotify_fd(-1); close(group_fd); close(group[1]); close(reused[0]); close(reused[1]); close(event[1]); } /* * test_mount_list_reconcile - verify a complete mount snapshot is merged. * * The active list must retain existing marks, flag disappeared mounts for * removal, and transfer newly parsed paths without allocating during the * update. Returns nothing. Exits on test failure. */ static void test_mount_list_reconcile(void) { mlist current, next; mlist_create(¤t); mlist_create(&next); CHECK(mlist_append(¤t, "/old") == 0, 185, "[ERROR:185] failed adding old mount fixture"); CHECK(mlist_append(¤t, "/kept") == 0, 186, "[ERROR:186] failed adding kept mount fixture"); CHECK(mlist_append(&next, "/kept") == 0, 187, "[ERROR:187] failed adding replacement kept fixture"); CHECK(mlist_append(&next, "/new") == 0, 188, "[ERROR:188] failed adding replacement new fixture"); mlist_reconcile(¤t, &next); CHECK(mlist_find(¤t, "/old") && current.cur->status == MNT_DELETE, 189, "[ERROR:189] disappeared mount was not marked deleted"); CHECK(mlist_find(¤t, "/kept") && current.cur->status == MNT_NO_CHANGE, 190, "[ERROR:190] retained mount was not preserved"); CHECK(mlist_find(¤t, "/new") && current.cur->status == MNT_ADD, 191, "[ERROR:191] new mount was not marked for addition"); CHECK(mlist_first(&next) == NULL, 192, "[ERROR:192] replacement list retained transferred nodes"); mlist_clear(¤t); mlist_clear(&next); } /* * test_systemd_notify_runtime_gate - verify debug-mode notify suppression. * * Debug runs can inherit arbitrary NOTIFY_SOCKET/WATCHDOG_* values from a * shell. The runtime gate must make the watchdog helper ignore those values * before it consults the environment. * * Returns nothing. Exits on test failure. */ static void test_systemd_notify_runtime_gate(void) { char pid[32]; snprintf(pid, sizeof(pid), "%ld", (long)getpid()); CHECK(setenv("NOTIFY_SOCKET", "/tmp/fapolicyd-test-notify", 1) == 0, 122, "[ERROR:122] failed setting NOTIFY_SOCKET"); CHECK(setenv("WATCHDOG_USEC", "1234567", 1) == 0, 123, "[ERROR:123] failed setting WATCHDOG_USEC"); CHECK(setenv("WATCHDOG_PID", pid, 1) == 0, 124, "[ERROR:124] failed setting WATCHDOG_PID"); systemd_notify_set_enabled(0); CHECK(systemd_watchdog_interval_usec() == 0, 125, "[ERROR:125] disabled notify gate used watchdog environment"); CHECK(!systemd_watchdog_enabled(), 126, "[ERROR:126] disabled notify gate enabled watchdog"); systemd_notify_set_enabled(1); #ifdef FAPOLICYD_ENABLE_SYSTEMD_WATCHDOG CHECK(systemd_watchdog_interval_usec() == 1234567, 127, "[ERROR:127] enabled notify gate ignored watchdog environment"); #else CHECK(systemd_watchdog_interval_usec() == 0, 127, "[ERROR:127] disabled build returned watchdog interval"); #endif unsetenv("WATCHDOG_PID"); unsetenv("WATCHDOG_USEC"); unsetenv("NOTIFY_SOCKET"); } /* * test_systemd_notify_full_queue - verify notification sends never block. * * A full AF_UNIX datagram receive queue must make READY fail with EAGAIN so * the daemon can service fanotify events before retrying. Returns nothing. * Exits on test failure. */ static void test_systemd_notify_full_queue(void) { #ifdef FAPOLICYD_ENABLE_SYSTEMD_WATCHDOG struct sockaddr_un addr = { .sun_family = AF_UNIX }; char notify_path[sizeof(addr.sun_path)]; char buf[8]; int fill_fds[16]; socklen_t addr_len; size_t path_len; unsigned int fill_fd_count = 0, i; int receive_fd, rc; snprintf(notify_path, sizeof(notify_path), "@fapolicyd-notify-test-%ld", (long)getpid()); path_len = strlen(notify_path + 1); memcpy(addr.sun_path + 1, notify_path + 1, path_len); addr_len = offsetof(struct sockaddr_un, sun_path) + 1 + path_len; receive_fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0); CHECK(receive_fd >= 0, 193, "[ERROR:193] failed creating notify receive socket"); CHECK(bind(receive_fd, (const struct sockaddr *)&addr, addr_len) == 0, 194, "[ERROR:194] failed binding notify receive socket"); /* A sender can fill its own buffer before the shared receive queue. */ for (i = 0; i < 16; i++) { int sent = 0; fill_fds[i] = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0); CHECK(fill_fds[i] >= 0, 195, "[ERROR:195] failed creating notify fill socket"); fill_fd_count++; do { rc = sendto(fill_fds[i], "FILL", 4, MSG_DONTWAIT, (const struct sockaddr *)&addr, addr_len); if (rc == 4) sent++; } while (rc == 4); CHECK(rc == -1 && (errno == EAGAIN || errno == EWOULDBLOCK), 196, "[ERROR:196] failed filling notify receive queue"); if (sent == 0) break; } CHECK(i < 16, 201, "[ERROR:201] notify queue exceeds test fill capacity"); CHECK(setenv("NOTIFY_SOCKET", notify_path, 1) == 0, 197, "[ERROR:197] failed setting full notify socket"); systemd_notify_set_enabled(1); alarm(1); rc = systemd_notify_ready(); alarm(0); CHECK(rc == -1 && (errno == EAGAIN || errno == EWOULDBLOCK), 198, "[ERROR:198] full notify queue did not return EAGAIN"); CHECK(recv(receive_fd, buf, sizeof(buf), MSG_DONTWAIT) == 4, 199, "[ERROR:199] failed draining notify receive queue"); CHECK(systemd_notify_ready() == 0, 200, "[ERROR:200] READY retry failed after queue drain"); unsetenv("NOTIFY_SOCKET"); for (i = 0; i < fill_fd_count; i++) close(fill_fds[i]); close(receive_fd); #endif } /* * read_operating_mode_report - capture the state operating mode section. * @buf: destination buffer. * @size: size of @buf. * Returns nothing. Exits if the temporary stream cannot be used. */ static void read_operating_mode_report(char *buf, size_t size) { struct state_report_operating_mode mode = { .permissive = false, .integrity = "sha256", .reset_strategy = "manual", .config_generation = 5, .ruleset_generation = 7, .config_effective_since = 1, .ruleset_effective_since = 1, .trust_db = { .generation = 9, .entries = 11, .publish_time = 1, .lmdb_generation = 3, .lmdb_publish_time = 1, }, .config = &config, }; FILE *f = tmpfile(); size_t used; if (f == NULL) error(1, 0, "tmpfile failed"); config.timing_collection = TIMING_COLLECTION_MANUAL; state_report_operating_mode(f, &mode); fflush(f); rewind(f); used = fread(buf, 1, size - 1, f); buf[used] = 0; fclose(f); } #if TEST_HAVE_FAN_FS_ERROR /* * read_fs_error_report - capture recent FAN_FS_ERROR detail output. * @buf: destination buffer. * @size: size of @buf. * Returns nothing. Exits if the temporary stream cannot be used. */ static void read_fs_error_report(char *buf, size_t size) { FILE *f = tmpfile(); size_t used; if (f == NULL) error(1, 0, "tmpfile failed"); fanotify_fs_error_report(f); fflush(f); rewind(f); used = fread(buf, 1, size - 1, f); buf[used] = 0; fclose(f); } #endif /* * test_operating_mode_report_order - verify state field order. * * Config generation and ruleset generation are adjacent so readers can see the * decision configuration identity beside the active policy identity. * * Returns nothing. Exits on test failure. */ static void test_operating_mode_report_order(void) { const char *config_generation, *ruleset, *trust_db, *lmdb_env; const char *timing_mode; char report[1024]; read_operating_mode_report(report, sizeof(report)); config_generation = strstr(report, "Config generation: 5 " "(effective since "); ruleset = strstr(report, "Ruleset generation: 7 " "(effective since "); trust_db = strstr(report, "Trust database generation: 9 " "(effective since "); lmdb_env = strstr(report, "LMDB environment generation: 3 " "(effective since "); timing_mode = strstr(report, "Timing collection mode: manual\n"); CHECK(config_generation != NULL, 68, "[ERROR:68] operating mode report missing config generation"); CHECK(ruleset != NULL, 59, "[ERROR:59] operating mode report missing ruleset field"); CHECK(trust_db != NULL, 69, "[ERROR:69] operating mode report missing trust DB generation"); CHECK(lmdb_env != NULL, 71, "[ERROR:71] operating mode report missing LMDB generation"); CHECK(strstr(report, "Trust database entries: 11\n") != NULL, 70, "[ERROR:70] operating mode report missing trust DB entries"); CHECK(timing_mode != NULL, 58, "[ERROR:58] operating mode report missing timing mode field"); CHECK(config_generation < ruleset && ruleset < trust_db && trust_db < lmdb_env && lmdb_env < timing_mode, 60, "[ERROR:60] generation fields were not grouped"); } /* * test_shutdown_deferred_events - verify notify shutdown replies once. * * Deferred permission events own their metadata fd until shutdown cleanup * replies and closes it. Use pipe read ends as stand-ins for fanotify fds so * the test can prove the production cleanup path closes each one exactly once. * * Returns nothing. Exits on test failure. */ static void test_shutdown_deferred_events(void) { decision_event_t event; int pipes[3][2]; unsigned int i; CHECK(test_notify_defer_reset(1) == 0, 40, "[ERROR:40] notify defer reset failed"); for (i = 0; i < 3; i++) { CHECK(pipe(pipes[i]) == 0, 41, "[ERROR:41] pipe setup failed"); memset(&event, 0, sizeof(event)); event.metadata.fd = pipes[i][0]; event.metadata.pid = 500 + i; event.metadata.mask = FAN_OPEN_PERM; event.subject_slot = i; event.completed_subject_slot = DECISION_EVENT_NO_SLOT; CHECK(test_notify_defer_push(&event) == 0, 42, "[ERROR:42] notify defer push failed"); } __atomic_store_n(&config.permissive, true, __ATOMIC_RELAXED); CHECK(decision_config_publish(&config) == 0, 66, "[ERROR:66] decision config publish failed"); CHECK(test_notify_shutdown_deferred_events() == 3, 61, "[ERROR:61] deferred shutdown count mismatch"); __atomic_store_n(&config.permissive, false, __ATOMIC_RELAXED); CHECK(decision_config_publish(&config) == 0, 69, "[ERROR:69] decision config reset failed"); decision_config_destroy(); for (i = 0; i < 3; i++) { errno = 0; CHECK(close(pipes[i][0]) == -1 && errno == EBADF, 43, "[ERROR:43] deferred fd was not closed exactly once"); close(pipes[i][1]); } test_notify_defer_destroy(); } /* * test_shutdown_queued_events - verify notify shutdown drains queue fds. * * The decision worker can observe stop before it processes every event already * accepted from fanotify. Those queued permission events must still be answered * during shutdown or their requesting tasks can remain blocked. * * Returns nothing. Exits on test failure. */ static void test_shutdown_queued_events(void) { decision_event_t event; int pipes[3][2]; unsigned int i; CHECK(test_notify_queue_reset(4) == 0, 62, "[ERROR:62] notify queue reset failed"); for (i = 0; i < 3; i++) { CHECK(pipe(pipes[i]) == 0, 63, "[ERROR:63] queue pipe setup failed"); memset(&event, 0, sizeof(event)); event.metadata.fd = pipes[i][0]; event.metadata.pid = 600 + i; event.metadata.mask = FAN_OPEN_PERM; event.subject_slot = i; event.completed_subject_slot = DECISION_EVENT_NO_SLOT; CHECK(test_notify_queue_push(&event) == 0, 64, "[ERROR:64] notify queue push failed"); } __atomic_store_n(&config.permissive, true, __ATOMIC_RELAXED); CHECK(decision_config_publish(&config) == 0, 67, "[ERROR:67] decision config publish failed"); CHECK(test_notify_shutdown_queued_events() == 3, 65, "[ERROR:65] queued shutdown count mismatch"); __atomic_store_n(&config.permissive, false, __ATOMIC_RELAXED); CHECK(decision_config_publish(&config) == 0, 70, "[ERROR:70] decision config reset failed"); decision_config_destroy(); for (i = 0; i < 3; i++) { errno = 0; CHECK(close(pipes[i][0]) == -1 && errno == EBADF, 71, "[ERROR:71] queued fd was not closed exactly once"); close(pipes[i][1]); } test_notify_queue_destroy(); } /* * test_shutdown_rejects_late_enqueue - keep queue ownership safe at shutdown. * * Once shutdown begins, an event that was not already handed to a worker must * remain owned by the dispatcher. Events accepted before that boundary still * need the normal queue-drain reply. Returns nothing. Exits on test failure. */ static void test_shutdown_rejects_late_enqueue(void) { int accepted[2], rejected[2]; CHECK(test_notify_queue_reset(2) == 0, 174, "[ERROR:174] queue reset failed for shutdown admission test"); CHECK(pipe(accepted) == 0, 175, "[ERROR:175] accepted pipe setup failed"); CHECK(test_notify_enqueue_pid_fd(700, accepted[0]) == 0, 176, "[ERROR:176] pre-shutdown event was not queued"); test_notify_worker_pool_begin_shutdown(); CHECK(pipe(rejected) == 0, 177, "[ERROR:177] rejected pipe setup failed"); errno = 0; CHECK(test_notify_enqueue_pid_fd(701, rejected[0]) == -1 && errno == ESHUTDOWN, 178, "[ERROR:178] shutdown accepted a late event"); CHECK(test_notify_worker_queue_depth(0) == 1, 179, "[ERROR:179] shutdown changed accepted queue ownership"); __atomic_store_n(&config.permissive, true, __ATOMIC_RELAXED); CHECK(decision_config_publish(&config) == 0, 180, "[ERROR:180] decision config publish failed"); CHECK(test_notify_shutdown_queued_events() == 1, 181, "[ERROR:181] accepted event was not drained at shutdown"); __atomic_store_n(&config.permissive, false, __ATOMIC_RELAXED); CHECK(decision_config_publish(&config) == 0, 182, "[ERROR:182] decision config reset failed"); decision_config_destroy(); errno = 0; CHECK(close(accepted[0]) == -1 && errno == EBADF, 183, "[ERROR:183] accepted event fd was not closed"); CHECK(close(rejected[0]) == 0, 184, "[ERROR:184] rejected event fd changed ownership"); close(accepted[1]); close(rejected[1]); test_notify_queue_destroy(); } /* * test_dispatcher_worker_routing - verify stable subject worker selection. * * The dispatcher must not choose workers by queue pressure or round-robin: * all events with the same subject key need the same decision owner so the * subject cache sees an ordered startup sequence. * * Returns nothing. Exits on test failure. */ static void test_dispatcher_worker_routing(void) { CHECK(test_notify_worker_index(1001, 4) == 1, 77, "[ERROR:77] pid routing did not use stable modulo key"); CHECK(test_notify_worker_index(1001, 4) == test_notify_worker_index(1001, 4), 78, "[ERROR:78] same pid did not route to same worker"); CHECK(test_notify_worker_index(1002, 4) == 2, 79, "[ERROR:79] adjacent pid routing mismatch"); CHECK(test_notify_worker_index(-1, 4) == 0, 80, "[ERROR:80] invalid pid did not use fallback key"); CHECK(test_notify_worker_index(1001, 0) == 0, 81, "[ERROR:81] zero worker fallback changed"); } /* * test_dispatcher_same_pid_ordering - verify FIFO ownership for one pid. * * Same-pid fanotify events must enter the same worker queue in arrival order * so the subject startup state machine sees a coherent sequence. * * Returns nothing. Exits on test failure. */ static void test_dispatcher_same_pid_ordering(void) { pid_t pids[4]; int fds[4]; unsigned int worker; CHECK(test_notify_worker_pool_reset(4, 8) == 0, 91, "[ERROR:91] worker pool reset failed"); CHECK(fanotify_active_worker_count() == 4, 92, "[ERROR:92] active worker count mismatch"); worker = test_notify_worker_index(1001, 4); CHECK(test_notify_enqueue_pid_fd(1001, 10) == 0, 93, "[ERROR:93] first same-pid enqueue failed"); CHECK(test_notify_enqueue_pid_fd(1001, 11) == 0, 94, "[ERROR:94] second same-pid enqueue failed"); CHECK(test_notify_enqueue_pid_fd(1001, 12) == 0, 95, "[ERROR:95] third same-pid enqueue failed"); CHECK(test_notify_worker_queue_depth(worker) == 3, 96, "[ERROR:96] same-pid events did not share one queue"); CHECK(test_notify_worker_drain(worker, pids, fds, 4) == 3, 97, "[ERROR:97] same-pid drain count mismatch"); CHECK(pids[0] == 1001 && pids[1] == 1001 && pids[2] == 1001, 98, "[ERROR:98] drained pid changed"); CHECK(fds[0] == 10 && fds[1] == 11 && fds[2] == 12, 99, "[ERROR:99] same-pid queue order changed"); test_notify_worker_pool_destroy(); } /* * test_dispatcher_pid_reuse_stability - verify reused pid routing stability. * * Numeric PID reuse is still handled by the subject fingerprint inside the * owning worker. Routing must not send the reused numeric PID to a different * worker, or stale/startup state could be split across caches. * * Returns nothing. Exits on test failure. */ static void test_dispatcher_pid_reuse_stability(void) { pid_t pids[4]; int fds[4]; unsigned int worker; CHECK(test_notify_worker_pool_reset(4, 8) == 0, 100, "[ERROR:100] worker pool reset failed for pid reuse"); worker = test_notify_worker_index(2209, 4); CHECK(test_notify_enqueue_pid_fd(2209, 20) == 0, 101, "[ERROR:101] first reused-pid enqueue failed"); CHECK(test_notify_enqueue_pid_fd(2210, 30) == 0, 102, "[ERROR:102] adjacent pid enqueue failed"); CHECK(test_notify_enqueue_pid_fd(2209, 21) == 0, 103, "[ERROR:103] second reused-pid enqueue failed"); CHECK(test_notify_worker_drain(worker, pids, fds, 4) == 2, 104, "[ERROR:104] reused-pid owner queue count mismatch"); CHECK(pids[0] == 2209 && pids[1] == 2209, 105, "[ERROR:105] reused pid did not stay on owner queue"); CHECK(fds[0] == 20 && fds[1] == 21, 106, "[ERROR:106] reused-pid queue order changed"); test_notify_worker_pool_destroy(); } /* * collect_worker_trace - append one worker queue to an observed trace. * @worker_id: worker queue to drain. * @observed: destination trace. * @used: entries already populated in @observed. * * Returns the updated trace length. Exits if the fixed unit-test trace buffer * would overflow. */ static unsigned int collect_worker_trace(unsigned int worker_id, struct dispatch_trace_observed *observed, unsigned int used) { pid_t pids[DISPATCH_TRACE_CAP]; int fds[DISPATCH_TRACE_CAP]; unsigned int drained, i; CHECK(used < DISPATCH_TRACE_CAP, 130, "[ERROR:130] dispatcher trace buffer full"); drained = test_notify_worker_drain(worker_id, pids, fds, DISPATCH_TRACE_CAP - used); for (i = 0; i < drained; i++) { observed[used].worker = worker_id; observed[used].pid = pids[i]; observed[used].fd = fds[i]; used++; } return used; } /* * collect_pool_trace - drain every worker queue into an observed trace. * @workers: number of synthetic workers to drain. * @observed: destination trace. * * Returns the number of observed events. */ static unsigned int collect_pool_trace(unsigned int workers, struct dispatch_trace_observed *observed) { unsigned int i, used = 0; for (i = 0; i < workers; i++) used = collect_worker_trace(i, observed, used); return used; } /* * enqueue_dispatch_trace - enqueue a synthetic replay trace. * @trace: PID/fd sequence to enqueue. * @count: number of trace entries. * * Returns nothing. Exits when any enqueue fails. */ static void enqueue_dispatch_trace(const struct dispatch_trace_event *trace, unsigned int count) { unsigned int i; for (i = 0; i < count; i++) CHECK(test_notify_enqueue_pid_fd(trace[i].pid, trace[i].fd) == 0, 131, "[ERROR:131] dispatcher trace enqueue failed"); } /* * test_dispatcher_single_multi_replay_parity - compare queue replay order. * * A multi-worker daemon may interleave different worker queues differently * from the single-worker daemon, but each worker's input stream must equal the * single-worker stream filtered by that worker's stable subject owner. That is * the unit-testable invariant behind pattern-detection parity: all events * that can mutate one subject startup state arrive at the same worker in the * same order they had before worker-pool dispatch. * * Returns nothing. Exits on test failure. */ static void test_dispatcher_single_multi_replay_parity(void) { const struct dispatch_trace_event trace[] = { { 1001, 10 }, { 1002, 11 }, { 1001, 12 }, { 1004, 13 }, { 1008, 14 }, { 1002, 15 }, { 1005, 16 }, { 1001, 17 }, { 1009, 18 }, { -1, 19 }, { 1004, 20 }, { 1005, 21 }, }; const unsigned int trace_count = sizeof(trace) / sizeof(trace[0]); struct dispatch_trace_observed single[DISPATCH_TRACE_CAP]; struct dispatch_trace_observed multi[DISPATCH_TRACE_CAP]; unsigned int single_count, multi_count, worker, i, out; CHECK(test_notify_worker_pool_reset(1, trace_count) == 0, 132, "[ERROR:132] single-worker replay reset failed"); enqueue_dispatch_trace(trace, trace_count); single_count = collect_pool_trace(1, single); CHECK(single_count == trace_count, 133, "[ERROR:133] single-worker replay count mismatch"); for (i = 0; i < trace_count; i++) { CHECK(single[i].worker == 0, 134, "[ERROR:134] single-worker replay used wrong worker"); CHECK(single[i].pid == trace[i].pid && single[i].fd == trace[i].fd, 135, "[ERROR:135] single-worker replay order changed"); } test_notify_worker_pool_destroy(); CHECK(test_notify_worker_pool_reset(4, trace_count) == 0, 136, "[ERROR:136] multi-worker replay reset failed"); enqueue_dispatch_trace(trace, trace_count); multi_count = collect_pool_trace(4, multi); CHECK(multi_count == single_count, 137, "[ERROR:137] multi-worker replay count mismatch"); out = 0; for (worker = 0; worker < 4; worker++) { for (i = 0; i < single_count; i++) { if (test_notify_worker_index(single[i].pid, 4) != worker) continue; CHECK(out < multi_count, 138, "[ERROR:138] multi-worker replay ended early"); CHECK(multi[out].worker == worker, 139, "[ERROR:139] multi-worker replay worker mismatch"); CHECK(multi[out].pid == single[i].pid && multi[out].fd == single[i].fd, 140, "[ERROR:140] multi-worker replay parity failed"); out++; } } CHECK(out == multi_count, 141, "[ERROR:141] multi-worker replay had unexpected events"); test_notify_worker_pool_destroy(); } /* * test_dispatcher_hot_worker_queue_full - verify isolated queue pressure. * * Queue-full handling is per worker. When a hot subject bucket fills one * worker queue, other workers must still accept their own events and the * metrics report must identify the full worker. * * Returns nothing. Exits on test failure. */ static void test_dispatcher_hot_worker_queue_full(void) { char report[4096]; CHECK(test_notify_worker_pool_reset(4, 1) == 0, 142, "[ERROR:142] queue-full worker pool reset failed"); CHECK(test_notify_enqueue_pid_fd(1000, 50) == 0, 143, "[ERROR:143] hot worker first enqueue failed"); errno = 0; CHECK(test_notify_enqueue_pid_fd(1004, 51) == -1 && errno == ENOSPC, 144, "[ERROR:144] hot worker full enqueue did not fail"); CHECK(test_notify_enqueue_pid_fd(1001, 52) == 0, 145, "[ERROR:145] idle worker enqueue failed after hot worker full"); CHECK(test_notify_worker_queue_depth(0) == 1, 146, "[ERROR:146] hot worker depth mismatch"); CHECK(test_notify_worker_queue_depth(1) == 1, 147, "[ERROR:147] idle worker did not accept event"); read_fanotify_queue_report(report, sizeof(report), 0); CHECK(strstr(report, "Inter-thread queue full count: 1\n") != NULL, 148, "[ERROR:148] aggregate queue full count mismatch"); CHECK(strstr(report, " Decision worker 0 queue full count: 1\n") != NULL, 149, "[ERROR:149] hot worker full count missing"); CHECK(strstr(report, " Decision worker 1 queue full count: 0\n") != NULL, 150, "[ERROR:150] idle worker full count changed"); test_notify_worker_pool_destroy(); } /* * test_dispatcher_worker_skew - verify hot PID buckets remain observable. * * Stable pid modulo routing can skew traffic. The dispatcher must expose that * pressure through per-worker queue metrics instead of rebalancing same-pid * events onto other workers. * * Returns nothing. Exits on test failure. */ static void test_dispatcher_worker_skew(void) { char report[4096]; CHECK(test_notify_worker_pool_reset(4, 8) == 0, 107, "[ERROR:107] worker pool reset failed for skew"); CHECK(test_notify_enqueue_pid_fd(1000, 40) == 0, 108, "[ERROR:108] skew enqueue 1 failed"); CHECK(test_notify_enqueue_pid_fd(1004, 41) == 0, 109, "[ERROR:109] skew enqueue 2 failed"); CHECK(test_notify_enqueue_pid_fd(1008, 42) == 0, 110, "[ERROR:110] skew enqueue 3 failed"); CHECK(test_notify_enqueue_pid_fd(1012, 43) == 0, 111, "[ERROR:111] skew enqueue 4 failed"); CHECK(test_notify_worker_queue_depth(0) == 4, 112, "[ERROR:112] hot pid bucket did not stay on worker 0"); CHECK(test_notify_worker_queue_depth(1) == 0 && test_notify_worker_queue_depth(2) == 0 && test_notify_worker_queue_depth(3) == 0, 113, "[ERROR:113] skew was rebalanced across workers"); read_fanotify_queue_report(report, sizeof(report), 0); CHECK(strstr(report, " Decision worker 0 current queue depth: 4\n") != NULL, 114, "[ERROR:114] skew report missing hot worker depth"); CHECK(strstr(report, " Decision worker 1 current queue depth: 0\n") != NULL, 115, "[ERROR:115] skew report missing idle worker depth"); test_notify_worker_pool_destroy(); } /* * test_notify_queue_report_reset - verify per-worker queue metrics reset. * * Worker queues own their metrics independently. A metrics reset must clear * the full counter for the queue that reported it while preserving the live * depth state for future reports. * * Returns nothing. Exits on test failure. */ static void test_notify_queue_report_reset(void) { decision_event_t event = { 0 }; char report[2048]; CHECK(test_notify_queue_reset(1) == 0, 82, "[ERROR:82] notify queue reset failed for metrics"); CHECK(test_notify_queue_push(&event) == 0, 83, "[ERROR:83] queue metrics push failed"); errno = 0; CHECK(test_notify_queue_push(&event) == -1 && errno == ENOSPC, 84, "[ERROR:84] full queue did not reject metrics push"); read_fanotify_queue_report(report, sizeof(report), 1); CHECK(strstr(report, "Inter-thread current queue depth: 1\n") != NULL, 85, "[ERROR:85] aggregate queue depth missing"); CHECK(strstr(report, " Decision worker 0 current queue depth: 1\n") != NULL, 86, "[ERROR:86] worker current depth missing"); CHECK(strstr(report, " Decision worker 0 max queue depth: 1\n") != NULL, 87, "[ERROR:87] worker max depth missing"); CHECK(strstr(report, " Decision worker 0 queue full count: 1\n") != NULL, 88, "[ERROR:88] worker full count missing before reset"); CHECK(strstr(report, " Decision worker 0 oldest queued age: ") != NULL, 89, "[ERROR:89] worker oldest age missing"); CHECK(strstr(report, "Per-worker subject defer activity:\n") != NULL, 116, "[ERROR:116] worker defer section missing"); CHECK(strstr(report, " Decision worker 0 Subject defer fallbacks: 0\n") != NULL, 117, "[ERROR:117] worker defer fallback missing"); read_fanotify_health_report(report, sizeof(report)); CHECK(strstr(report, " Decision worker 0 health: state=idle heartbeat=") != NULL, 118, "[ERROR:118] worker health state missing"); CHECK(strstr(report, " current_event=idle\n") != NULL, 119, "[ERROR:119] worker current event age missing"); read_fanotify_queue_report(report, sizeof(report), 0); CHECK(strstr(report, " Decision worker 0 queue full count: 0\n") != NULL, 90, "[ERROR:90] worker full count did not reset"); test_notify_queue_destroy(); } /* * test_deferred_dispatch_refreshes_health - verify dequeue liveness. * * A worker that dequeues an event and parks it behind a BUILDING subject has * still made progress. The worker heartbeat must be refreshed before dispatch * can return from the successful deferral branch, otherwise the health monitor * can misclassify bounded deferral as a stalled worker under queue pressure. * * Returns nothing. Exits on test failure. */ static void test_deferred_dispatch_refreshes_health(void) { conf_t cfg = { 0 }; decision_event_t event; pid_t owner = getpid(); pid_t blocked = owner + 1; unsigned int slot; uint64_t before, after; int pipes[2]; cfg.subj_cache_size = 1; cfg.obj_cache_size = 1; CHECK(init_event_system(&cfg) == 0, 156, "[ERROR:156] failed to initialize event system"); CHECK(test_notify_queue_reset(2) == 0, 157, "[ERROR:157] notify queue reset failed"); slot = event_subject_slot(owner); seed_building_subject(owner, slot); CHECK(slot == event_subject_slot(blocked), 158, "[ERROR:158] subject cache did not force a collision"); CHECK(event_subject_slot_is_blocked(slot, blocked) == 1, 159, "[ERROR:159] fixture subject did not block collision"); CHECK(pipe(pipes) == 0, 160, "[ERROR:160] deferred dispatch pipe setup failed"); memset(&event, 0, sizeof(event)); event.metadata.fd = pipes[0]; event.metadata.pid = blocked; event.metadata.mask = FAN_OPEN_PERM; event.subject_slot = slot; event.completed_subject_slot = DECISION_EVENT_NO_SLOT; CHECK(test_notify_queue_push(&event) == 0, 161, "[ERROR:161] deferred dispatch queue push failed"); test_notify_worker_set_heartbeat_ns(0, 1); before = test_notify_worker_heartbeat_ns(0); CHECK(test_notify_worker_dequeue_dispatch(0) == 1, 162, "[ERROR:162] deferred dispatch did not process one event"); after = test_notify_worker_heartbeat_ns(0); CHECK(after > before, 163, "[ERROR:163] deferred dispatch did not refresh heartbeat"); CHECK(test_notify_worker_queue_depth(0) == 0, 164, "[ERROR:164] dispatched event remained queued"); CHECK(test_notify_shutdown_deferred_events() == 1, 166, "[ERROR:166] deferred dispatch cleanup missed event"); errno = 0; CHECK(close(pipes[0]) == -1 && errno == EBADF, 167, "[ERROR:167] deferred dispatch fd was not closed"); close(pipes[1]); test_notify_queue_destroy(); destroy_event_system(); } /* * test_interval_report_deadline - interval reports must not start at epoch. * * The queue timeout and timerfd deadline must describe the first configured * interval. An absolute value containing only the interval is already in the * past, which makes the timer fire as soon as the worker starts. * * Returns nothing. Exits on test failure. */ static void test_interval_report_deadline(void) { struct timespec before, timeout; struct itimerspec timer; CHECK(clock_gettime(CLOCK_REALTIME, &before) == 0, 168, "[ERROR:168] failed to read report timer clock"); CHECK(test_notify_report_timer_init(5, &timeout, &timer) == 0, 169, "[ERROR:169] failed to initialize report timer"); CHECK(timeout.tv_sec >= before.tv_sec + 5, 170, "[ERROR:170] initial report timeout was not in the future"); CHECK(timer.it_interval.tv_sec == 5 && timer.it_interval.tv_nsec == 0, 171, "[ERROR:171] report timer interval changed"); CHECK(timer.it_value.tv_sec >= 4, 172, "[ERROR:172] report timer expired before its first interval"); test_notify_report_timer_destroy(); } /* * test_interval_report_defer_deadline - preserve deferred-event liveness. * * Worker 0 owns interval reports, but its report deadline must not hide the * shorter deferred-event deadline. Otherwise a quiet worker never revisits a * permission event after the BUILDING subject ahead of it becomes stale. * * Returns nothing. Exits on test failure. */ static void test_interval_report_defer_deadline(void) { decision_event_t event = { 0 }; struct timespec now, report_timeout, selected_timeout; CHECK(test_notify_defer_reset(1) == 0, 202, "[ERROR:202] report deadline defer reset failed"); CHECK(clock_gettime(CLOCK_REALTIME, &now) == 0, 203, "[ERROR:203] failed reading report deadline clock"); report_timeout = now; report_timeout.tv_sec += 30; CHECK(test_notify_report_dequeue_timeout(&report_timeout, &selected_timeout) == 0, 204, "[ERROR:204] empty defer queue shortened report deadline"); CHECK(selected_timeout.tv_sec == report_timeout.tv_sec && selected_timeout.tv_nsec == report_timeout.tv_nsec, 205, "[ERROR:205] empty defer queue changed report deadline"); event.metadata.fd = FAN_NOFD; event.metadata.pid = 700; event.metadata.mask = FAN_OPEN_PERM; event.subject_slot = 0; event.completed_subject_slot = DECISION_EVENT_NO_SLOT; CHECK(test_notify_defer_push(&event) == 0, 206, "[ERROR:206] failed parking report-owner deferred event"); CHECK(test_notify_report_dequeue_timeout(&report_timeout, &selected_timeout) == 1, 207, "[ERROR:207] report wait missed deferred event"); CHECK(clock_gettime(CLOCK_REALTIME, &now) == 0, 208, "[ERROR:208] failed reading selected deadline clock"); CHECK(selected_timeout.tv_sec < report_timeout.tv_sec && selected_timeout.tv_sec <= now.tv_sec + 1, 209, "[ERROR:209] report wait did not use defer recheck deadline"); test_notify_defer_destroy(); } /* * test_interval_report_disable_closes_timer - invalidate a disabled timer. * * A later pool cleanup must not close an unrelated descriptor after the * operating system reuses the report timer's descriptor number. * * Returns nothing. Exits on test failure. */ static void test_interval_report_disable_closes_timer(void) { struct timespec timeout; struct itimerspec timer; CHECK(test_notify_report_timer_init(5, &timeout, &timer) == 0, 173, "[ERROR:173] failed to initialize report timer for disable"); CHECK(test_notify_report_timer_fd() != -1, 174, "[ERROR:174] report timer descriptor was not active"); test_notify_report_timer_disable(); CHECK(test_notify_report_timer_fd() == -1, 175, "[ERROR:175] disabled report timer kept its descriptor"); } /* * main - exercise synthetic FAN_NOFD kernel metadata. * Returns 0 on success. Exits with error() on test failure. */ int main(void) { struct fanotify_event_metadata metadata = { .event_len = sizeof(metadata), .vers = FANOTIFY_METADATA_VERSION, .fd = FAN_NOFD, .pid = 0, }; unsigned long before, after; unsigned long overflow_after, reply_after; #if TEST_HAVE_FAN_FS_ERROR unsigned long fs_error_after = 0; #endif char report[4096], expected[128]; int event_pipe[2]; test_operating_mode_report_order(); test_dispatcher_worker_routing(); test_dispatcher_same_pid_ordering(); test_dispatcher_pid_reuse_stability(); test_dispatcher_single_multi_replay_parity(); test_dispatcher_hot_worker_queue_full(); test_dispatcher_worker_skew(); test_notify_queue_report_reset(); test_deferred_dispatch_refreshes_health(); test_interval_report_deadline(); test_interval_report_defer_deadline(); test_interval_report_disable_closes_timer(); test_nonblocking_fanotify_reads(); test_shutdown_fanotify_close(); test_fatal_signal_fanotify_close(); test_mount_list_reconcile(); before = getKernelQueueOverflow(); metadata.mask = 0; // A FAN_NOFD event without FAN_Q_OVERFLOW is not a kernel event. CHECK(handle_kernel_event(&metadata) == 0, 1, "[ERROR:1] non-overflow FAN_NOFD event was consumed"); // Ignoring a non-overflow FAN_NOFD event must not change metrics. CHECK(getKernelQueueOverflow() == before, 2, "[ERROR:2] non-overflow event changed overflow count"); CHECK(failure_action_count(FAILURE_REASON_KERNEL_QUEUE_OVERFLOW) == before, 3, "[ERROR:3] non-overflow event changed failure count"); atomic_store(&run_stats, false); metadata.mask = FAN_Q_OVERFLOW; // FAN_Q_OVERFLOW should be consumed as a kernel queue failure. CHECK(handle_kernel_event(&metadata) == 1, 4, "[ERROR:4] FAN_Q_OVERFLOW event was not consumed"); after = getKernelQueueOverflow(); overflow_after = after; // Consuming FAN_Q_OVERFLOW should increment the overflow counter once. CHECK(after == before + 1, 5, "[ERROR:5] FAN_Q_OVERFLOW did not increment count"); CHECK(failure_action_count(FAILURE_REASON_KERNEL_QUEUE_OVERFLOW) == after, 6, "[ERROR:6] FAN_Q_OVERFLOW failure count mismatch"); // Queue overflow should request the configured failure action. CHECK(atomic_load(&run_stats), 7, "[ERROR:7] FAN_Q_OVERFLOW did not trigger failure action"); read_decision_report(report, sizeof(report), 0); snprintf(expected, sizeof(expected), "Kernel Queue Overflow: %lu", after); // The status report should expose the overflow counter value. CHECK(strstr(report, expected) != NULL, 8, "[ERROR:8] status report missing Kernel Queue Overflow"); snprintf(expected, sizeof(expected), "Failure action kernel_queue_overflow (observe): %lu", after); CHECK(strstr(report, expected) != NULL, 9, "[ERROR:9] status report missing kernel overflow failure count"); #if TEST_HAVE_FAN_FS_ERROR { struct { struct fanotify_event_metadata metadata; struct test_fanotify_fs_error_info error; } fs_error_event; memset(&fs_error_event, 0, sizeof(fs_error_event)); fs_error_event.metadata.event_len = sizeof(fs_error_event.metadata) + sizeof(fs_error_event.error); fs_error_event.metadata.vers = FANOTIFY_METADATA_VERSION; fs_error_event.metadata.metadata_len = sizeof(fs_error_event.metadata); fs_error_event.metadata.fd = FAN_NOFD; fs_error_event.metadata.pid = 5678; fs_error_event.metadata.mask = FAN_FS_ERROR; fs_error_event.error.hdr.info_type = FAN_EVENT_INFO_TYPE_ERROR; fs_error_event.error.hdr.len = sizeof(fs_error_event.error); fs_error_event.error.error = EIO; fs_error_event.error.error_count = 3; before = getFanotifyFilesystemErrors(); atomic_store(&run_stats, false); CHECK(fanotify_fs_error_handle_event( &fs_error_event.metadata) == 1, 46, "[ERROR:46] FAN_FS_ERROR event was not consumed"); after = getFanotifyFilesystemErrors(); fs_error_after = after; CHECK(after == before + 1, 47, "[ERROR:47] FAN_FS_ERROR did not increment count"); CHECK(failure_action_count( FAILURE_REASON_FANOTIFY_FS_ERROR) == after, 48, "[ERROR:48] FAN_FS_ERROR failure count mismatch"); CHECK(atomic_load(&run_stats), 49, "[ERROR:49] FAN_FS_ERROR did not trigger failure action"); read_decision_report(report, sizeof(report), 0); snprintf(expected, sizeof(expected), "Filesystem Errors: %lu", after); CHECK(strstr(report, expected) != NULL, 50, "[ERROR:50] status report missing Filesystem Errors"); snprintf(expected, sizeof(expected), "Failure action fanotify_filesystem_error " "(observe): %lu", after); CHECK(strstr(report, expected) != NULL, 51, "[ERROR:51] status report missing FS error failure"); read_fs_error_report(report, sizeof(report)); CHECK(strstr(report, "Filesystem error last status: ok") != NULL, 52, "[ERROR:52] FS error status missing"); CHECK(strstr(report, "Filesystem error last errno: 5") != NULL, 53, "[ERROR:53] FS error errno missing"); CHECK(strstr(report, "Filesystem error last suppressed count: 3") != NULL, 54, "[ERROR:54] FS error suppressed count missing"); } #endif // Use a real event fd so reply_event can prove it still closes once. CHECK(pipe(event_pipe) == 0, 10, "[ERROR:10] pipe failed"); metadata.fd = event_pipe[0]; metadata.pid = 1234; metadata.mask = FAN_OPEN_PERM; before = getReplyErrors(); reply_event(-1, &metadata, FAN_ALLOW, NULL); after = getReplyErrors(); reply_after = after; // A failed fanotify response write should increment reply_errors once. CHECK(after == before + 1, 11, "[ERROR:11] reply_event did not count EBADF write failure"); CHECK(failure_action_count(FAILURE_REASON_RESPONSE_WRITE_FAILURE) == after, 12, "[ERROR:12] response failure count mismatch"); errno = 0; // reply_event should close the event fd even when the response fails. CHECK(close(event_pipe[0]) == -1 && errno == EBADF, 13, "[ERROR:13] reply_event did not close event fd"); close(event_pipe[1]); read_decision_report(report, sizeof(report), 0); snprintf(expected, sizeof(expected), "Reply Errors: %lu", after); // The status report should expose the aggregate reply_errors value. CHECK(strstr(report, expected) != NULL, 14, "[ERROR:14] status report missing Reply Errors count"); snprintf(expected, sizeof(expected), "Failure action response_write_failure (observe): %lu", after); CHECK(strstr(report, expected) != NULL, 15, "[ERROR:15] status report missing response failure count"); CHECK(strstr(report, "Failure action queue_full (observe): ") != NULL, 16, "[ERROR:16] status report missing queue full failure count"); CHECK(strstr(report, "Failure action worker_stall (terminate): ") != NULL, 17, "[ERROR:17] status report missing worker stall failure count"); CHECK(strstr(report, "Failure action rule_reload_failure (observe): ") != NULL, 18, "[ERROR:18] status report missing rule reload failure count"); CHECK(strstr(report, "Failure action trust_reload_failure (observe): ") != NULL, 19, "[ERROR:19] status report missing trust reload failure count"); CHECK(strstr(report, "Failure action fanotify_filesystem_error (observe): ") != NULL, 55, "[ERROR:55] status report missing fs error failure count"); CHECK(strstr(report, "Allowed by rule: ") != NULL, 20, "[ERROR:20] status report missing rule allow count"); CHECK(strstr(report, "Allowed by fallthrough: ") != NULL, 21, "[ERROR:21] status report missing fallthrough allow count"); CHECK(strstr(report, "Allowed by fallthrough executable: ") == NULL, 22, "[ERROR:22] zero fallthrough report included ftype detail"); CHECK(strstr(report, "Ruleset generation: ") != NULL, 23, "[ERROR:23] status report missing ruleset generation"); read_decision_metrics_report(report, sizeof(report)); const char *metrics_reset = strstr(report, "Last metrics reset: never\n"); const char *metrics_config = strstr(report, "Config generation: 0 " "(effective since "); const char *metrics_ruleset = strstr(report, "Ruleset generation: "); CHECK(metrics_reset != NULL, 44, "[ERROR:44] metrics report missing last reset header"); CHECK(metrics_config != NULL, 72, "[ERROR:72] metrics report missing config generation header"); CHECK(metrics_ruleset != NULL, 45, "[ERROR:45] metrics report missing ruleset generation header"); CHECK(metrics_reset < metrics_config && metrics_config < metrics_ruleset, 73, "[ERROR:73] metrics identity headers out of order"); atomic_store(&run_stats, false); atomic_store(&signal_report_requests, 0); siginfo_t info; memset(&info, 0, sizeof(info)); info.si_code = SI_QUEUE; info.si_pid = 4321; info.si_uid = 0; info.si_value.sival_int = REPORT_INTENT_TIMING_ARM; usr1_handler(SIGUSR1, &info, NULL); CHECK(!atomic_load(&run_stats), 24, "[ERROR:24] timing start incorrectly requested state report"); CHECK(atomic_load(&signal_report_requests) == 0, 25, "[ERROR:25] timing start incremented state report requests"); config.timing_collection = TIMING_COLLECTION_OFF; decision_timing_process_requests(&config); FILE *timing = tmpfile(); CHECK(timing != NULL, 26, "[ERROR:26] tmpfile failed"); decision_timing_control_report(timing, &config); fflush(timing); rewind(timing); size_t used = fread(report, 1, sizeof(report) - 1, timing); report[used] = 0; fclose(timing); CHECK(strstr(report, "Timing collection mode: off") != NULL, 27, "[ERROR:27] timing mode missing from state report"); CHECK(strstr(report, "Timing collection armed: false") != NULL, 28, "[ERROR:28] timing unexpectedly armed while configured off"); config.timing_collection = TIMING_COLLECTION_MANUAL; usr1_handler(SIGUSR1, &info, NULL); decision_timing_process_requests(&config); timing = tmpfile(); CHECK(timing != NULL, 29, "[ERROR:29] tmpfile failed"); decision_timing_control_report(timing, &config); fflush(timing); rewind(timing); used = fread(report, 1, sizeof(report) - 1, timing); report[used] = 0; fclose(timing); CHECK(strstr(report, "Timing collection mode: manual") != NULL, 30, "[ERROR:30] manual timing mode missing from state report"); CHECK(strstr(report, "Timing collection armed: true") != NULL, 31, "[ERROR:31] privileged manual timing start was not applied"); CHECK(strstr(report, "Timing collection last start requester") == NULL, 32, "[ERROR:32] timing start requester still in state report"); info.si_value.sival_int = REPORT_INTENT_TIMING_STOP; usr1_handler(SIGUSR1, &info, NULL); decision_timing_process_requests(&config); timing = tmpfile(); CHECK(timing != NULL, 33, "[ERROR:33] tmpfile failed"); decision_timing_control_report(timing, &config); fflush(timing); rewind(timing); used = fread(report, 1, sizeof(report) - 1, timing); report[used] = 0; fclose(timing); CHECK(strstr(report, "Timing collection armed: false") != NULL, 34, "[ERROR:34] timing stop did not disarm"); CHECK(strstr(report, "Timing collection last stop requester") == NULL, 35, "[ERROR:35] timing stop requester still in state report"); read_decision_report(report, sizeof(report), 1); snprintf(expected, sizeof(expected), "Kernel Queue Overflow: %lu", overflow_after); CHECK(strstr(report, expected) != NULL, 36, "[ERROR:36] reset report lost pre-reset overflow count"); snprintf(expected, sizeof(expected), "Reply Errors: %lu", reply_after); CHECK(strstr(report, expected) != NULL, 37, "[ERROR:37] reset report lost pre-reset reply count"); #if TEST_HAVE_FAN_FS_ERROR snprintf(expected, sizeof(expected), "Filesystem Errors: %lu", fs_error_after); CHECK(strstr(report, expected) != NULL, 56, "[ERROR:56] reset report lost pre-reset fs error count"); #endif CHECK(getKernelQueueOverflow() == 0, 38, "[ERROR:38] reset report did not clear overflow count"); CHECK(getReplyErrors() == 0, 39, "[ERROR:39] reset report did not clear reply count"); #if TEST_HAVE_FAN_FS_ERROR CHECK(getFanotifyFilesystemErrors() == 0, 57, "[ERROR:57] reset report did not clear fs error count"); #endif test_shutdown_deferred_events(); test_shutdown_queued_events(); test_shutdown_rejects_late_enqueue(); test_systemd_notify_runtime_gate(); test_systemd_notify_full_queue(); return 0; } fapolicyd-2.0/src/tests/policy_concurrent_test.c000066400000000000000000000137071524071644000222350ustar00rootroot00000000000000/* * policy_concurrent_test.c - verify concurrent policy reads are local * * The test publishes one immutable policy snapshot, then evaluates the same * late-matching rule from several threads. The daemon still serializes * decisions today, but read-side rule iteration must not depend on the * mutable llist cursor before worker threads can be introduced. */ #include #include #include #include #include #include #include #include #include #include #include "conf.h" #include "policy.h" #include "subject.h" #include "object.h" #include "event.h" #include "message.h" #define WORKER_COUNT 32 #define ITERATIONS 2000 #define RELOAD_ITERATIONS 200 #define NO_OPINION_RULES 64 #define POLICY_BUFSIZE 8192 #define TARGET_AUID 4242 #define TARGET_PATH "/tmp/fapolicyd-concurrent-target" extern atomic_bool stop; static atomic_bool start_workers; static atomic_uint failures; /* * make_policy_text - build a policy with a late matching deny rule * @buf: destination buffer for newline-separated rules. * @buflen: size of @buf. * @variant: reload variant used to make each generation distinct. * Returns nothing. Exits on overflow because the test fixture is invalid. */ static void make_policy_text(char *buf, size_t buflen, unsigned int variant) { size_t off = 0; int len; unsigned int i; for (i = 0; i < NO_OPINION_RULES; i++) { len = snprintf(buf + off, buflen - off, "allow perm=any auid=%u : " "path=/no/match/%u/%u\n", i, variant, i); if (len < 0 || (size_t)len >= buflen - off) error(1, 0, "policy buffer overflow"); off += (size_t)len; } len = snprintf(buf + off, buflen - off, "deny perm=any auid=%u : path=%s\n", (unsigned int)TARGET_AUID, TARGET_PATH); if (len < 0 || (size_t)len >= buflen - off) error(1, 0, "policy buffer overflow"); } /* * load_policy_variant - publish one test policy generation * @variant: generation variant used for rules and syslog format. * Returns 0 on success, 1 on load failure. */ static int load_policy_variant(unsigned int variant) { conf_t cfg = { .syslog_format = variant & 1 ? "rule,dec,perm,pid,:,path" : "rule,dec,perm,auid,:,path" }; char policy[POLICY_BUFSIZE]; FILE *f; int rc; make_policy_text(policy, sizeof(policy), variant); f = fmemopen(policy, strlen(policy), "r"); if (!f) error(1, errno, "fmemopen failed"); rc = load_rules_from_stream(&cfg, f); fclose(f); return rc; } /* * load_test_policy - publish the initial policy used by reader threads * @void: no arguments are required. * Returns nothing. Exits if policy loading fails. */ static void load_test_policy(void) { if (load_policy_variant(0)) error(1, 0, "policy load failed"); } /* * prep_event - allocate and populate an event for policy evaluation * @e: event to initialize. * Returns nothing. Exits on allocation failure. */ static void prep_event(event_t *e) { subject_attr_t sattr = { .type = AUID, .uval = TARGET_AUID }; object_attr_t oattr = { .type = PATH, .o = strdup(TARGET_PATH) }; memset(e, 0, sizeof(*e)); e->type = FAN_OPEN_PERM; e->s = malloc(sizeof(s_array)); e->o = malloc(sizeof(o_array)); if (!e->s || !e->o || !oattr.o) error(1, errno, "event allocation failed"); if (subject_create(e->s) || object_create(e->o)) error(1, errno, "event array allocation failed"); e->s->info = calloc(1, sizeof(struct proc_info)); if (!e->s->info) error(1, errno, "proc_info allocation failed"); if (subject_add(e->s, &sattr)) error(1, 0, "subject_add failed"); if (object_add(e->o, &oattr)) error(1, 0, "object_add failed"); } /* * free_event - release memory allocated by prep_event() * @e: event to clear. * Returns nothing. */ static void free_event(event_t *e) { subject_clear(e->s); object_clear(e->o); free(e->s); free(e->o); } /* * reader_thread - repeatedly evaluate the active policy snapshot * @arg: unused pthread argument. * Return: NULL. */ static void *reader_thread(void *arg) { unsigned int i; (void)arg; while (!atomic_load_explicit(&start_workers, memory_order_acquire)) sched_yield(); for (i = 0; i < ITERATIONS; i++) { event_t e; decision_t decision; prep_event(&e); decision = process_event(&e); if (decision != DENY || e.num != NO_OPINION_RULES + 1) atomic_fetch_add_explicit(&failures, 1, memory_order_relaxed); free_event(&e); } return NULL; } /* * reloader_thread - publish replacement snapshots while readers run * @arg: unused pthread argument. * Return: NULL. */ static void *reloader_thread(void *arg) { unsigned int i; (void)arg; while (!atomic_load_explicit(&start_workers, memory_order_acquire)) sched_yield(); for (i = 0; i < RELOAD_ITERATIONS; i++) { if (load_policy_variant(i + 1)) atomic_fetch_add_explicit(&failures, 1, memory_order_relaxed); sched_yield(); } return NULL; } /* * main - exercise concurrent read-only rule evaluation * @void: no arguments are required. * Returns 0 on success. Exits with error() on test failure. */ int main(void) { pthread_t workers[WORKER_COUNT]; pthread_t reloader; unsigned int i; unsigned int failed; int rc; set_message_mode(MSG_STDERR, DBG_NO); load_test_policy(); for (i = 0; i < WORKER_COUNT; i++) { rc = pthread_create(&workers[i], NULL, reader_thread, NULL); if (rc) error(1, rc, "pthread_create failed"); } rc = pthread_create(&reloader, NULL, reloader_thread, NULL); if (rc) error(1, rc, "pthread_create reloader failed"); atomic_store_explicit(&start_workers, true, memory_order_release); for (i = 0; i < WORKER_COUNT; i++) { rc = pthread_join(workers[i], NULL); if (rc) error(1, rc, "pthread_join failed"); } rc = pthread_join(reloader, NULL); if (rc) error(1, rc, "pthread_join reloader failed"); failed = atomic_load_explicit(&failures, memory_order_relaxed); if (failed) error(1, 0, "%u concurrent decisions failed", failed); atomic_store(&stop, true); destroy_rules(); return 0; } fapolicyd-2.0/src/tests/policy_reload_test.c000066400000000000000000000331531524071644000213160ustar00rootroot00000000000000/* * policy_reload_test.c - verify failed rule reloads are transactional * * The test loads an initial policy, then attempts reloads that fail before * syslog parsing and during syslog parsing. In both cases, the previously * published policy and its syslog field list must remain active. */ #include #include #include #include #include #include #include #include "conf.h" #include "attr-lookup-metrics.h" #include "policy.h" #include "policy-metrics.h" #include "subject.h" #include "object.h" #include "event.h" #include "message.h" #define LOGBUF 4096 #define OLD_SYSLOG_FORMAT "rule,dec,perm,auid,:,path" #define PID_SYSLOG_FORMAT "rule,dec,perm,pid,:,path" #define PROC_FALLBACK_SYSLOG_FORMAT "rule,dec,perm,uid,gid,comm,:,path" extern atomic_bool stop; /* * load_text_policy - load a policy from an in-memory rule string * @cfg: configuration providing syslog_format. * @rules: newline-terminated policy text. * Returns 0 on success, 1 on load failure. */ static int load_text_policy(const conf_t *cfg, const char *rules) { FILE *f; int rc; f = fmemopen((void *)rules, strlen(rules), "r"); if (!f) error(1, errno, "fmemopen failed"); rc = load_rules_from_stream(cfg, f); fclose(f); return rc; } /* * prep_event - allocate and populate an event for policy evaluation * @e: event to initialize * @auid: subject audit uid * @path: object path * * Returns: none. */ static void prep_event(event_t *e, unsigned int auid, const char *path) { subject_attr_t sattr = { .type = AUID, .uval = auid }; object_attr_t oattr = { .type = PATH, .o = strdup(path) }; memset(e, 0, sizeof(*e)); e->s = malloc(sizeof(s_array)); e->o = malloc(sizeof(o_array)); if (!e->s || !e->o || !oattr.o) error(1, errno, "event allocation failed"); if (subject_create(e->s) || object_create(e->o)) error(1, errno, "event array allocation failed"); e->s->info = calloc(1, sizeof(struct proc_info)); if (!e->s->info) error(1, errno, "proc_info allocation failed"); if (subject_add(e->s, &sattr)) error(1, 0, "subject_add failed"); if (object_add(e->o, &oattr)) error(1, 0, "object_add failed"); } /* * free_event - release memory allocated by prep_event() * @e: event to clear. * Returns nothing. */ static void free_event(event_t *e) { subject_clear(e->s); object_clear(e->o); free(e->s); free(e->o); } /* * add_cached_object_attrs - add attributes policy metrics may summarize * @e: event receiving cached object attributes. * Returns nothing. */ static void add_cached_object_attrs(event_t *e) { object_attr_t trust = { .type = OBJ_TRUST, .val = 1 }; object_attr_t ftype = { .type = FTYPE, .o = strdup("application/x-executable") }; if (!ftype.o) error(1, errno, "ftype allocation failed"); if (object_add(e->o, &trust)) error(1, 0, "object_add trust failed"); if (object_add(e->o, &ftype)) error(1, 0, "object_add ftype failed"); } /* * reset_object_attr_metrics - clear object lookup metrics used by this test * Returns nothing. */ static void reset_object_attr_metrics(void) { struct attr_lookup_metric_snapshot snapshot; attr_lookup_metrics_object_snapshot(PATH, &snapshot, 1); attr_lookup_metrics_object_snapshot(OBJ_TRUST, &snapshot, 1); attr_lookup_metrics_object_snapshot(FTYPE, &snapshot, 1); } /* * require_no_object_attr_metrics - verify no lazy object getter was invoked * @phase: diagnostic label included in failure messages. * Returns nothing. */ static void require_no_object_attr_metrics(const char *phase) { struct attr_lookup_metric_snapshot snapshot; object_type_t types[] = { PATH, OBJ_TRUST, FTYPE }; unsigned int i; for (i = 0; i < sizeof(types) / sizeof(types[0]); i++) { if (attr_lookup_metrics_object_snapshot(types[i], &snapshot, 0)) error(1, 0, "%s: object metric snapshot failed", phase); if (snapshot.requests || snapshot.lookups) error(1, 0, "%s: %s getter used by metrics: %llu/%llu", phase, obj_val_to_name(types[i]), snapshot.requests, snapshot.lookups); } } /* * reset_policy_metrics - clear counters that this test inspects * Returns nothing. */ static void reset_policy_metrics(void) { decision_metrics_t metrics; getAllowedReset(1); getDeniedReset(1); getDecisionMetricsReset(&metrics, 1); } /* * process_capture - evaluate an event and capture stderr logging output * @e: event to evaluate. * @buf: destination for captured stderr text. * @buflen: size of @buf. * Returns the decision from process_event(). */ static decision_t process_capture(event_t *e, char *buf, size_t buflen, decision_source_t *source) { decision_t decision; ssize_t r; int p[2]; int save; if (pipe(p)) error(1, errno, "pipe failed"); fflush(stderr); save = dup(STDERR_FILENO); if (save == -1) error(1, errno, "dup failed"); if (dup2(p[1], STDERR_FILENO) == -1) error(1, errno, "dup2 failed"); close(p[1]); decision = process_event_with_source(e, source, NULL); fflush(stderr); if (dup2(save, STDERR_FILENO) == -1) error(1, errno, "dup2 restore failed"); close(save); r = read(p[0], buf, buflen - 1); if (r < 0) r = 0; buf[r] = '\0'; close(p[0]); return decision; } /* * process_path - evaluate one event for a path. * @path: object path to place on the event. * Returns the policy decision. */ static decision_t process_path(const char *path) { char log[LOGBUF]; event_t e; decision_t decision; prep_event(&e, 1000, path); decision = process_capture(&e, log, sizeof(log), NULL); free_event(&e); return decision; } /* * read_rule_hits_report - capture the per-rule hit report. * @buf: destination for captured report text. * @buflen: size of @buf. * @reset: non-zero resets counters after copying them. * Returns nothing. */ static void read_rule_hits_report(char *buf, size_t buflen, int reset) { FILE *f; size_t used; f = tmpfile(); if (!f) error(1, errno, "tmpfile failed"); policy_rule_hits_report_reset(f, reset); fflush(f); rewind(f); used = fread(buf, 1, buflen - 1, f); buf[used] = '\0'; fclose(f); } /* * require_old_policy - verify the initially loaded policy is still active * @phase: label included in failure diagnostics. * Returns nothing. */ static void require_old_policy(const char *phase) { char log[LOGBUF]; event_t e; decision_t decision; prep_event(&e, 1000, "/bin/ls"); decision = process_capture(&e, log, sizeof(log), NULL); free_event(&e); if (decision != ALLOW_SYSLOG) error(1, 0, "%s: old allow_syslog policy not preserved", phase); if (strstr(log, "dec=allow_syslog") == NULL) error(1, 0, "%s: old decision missing from log: %s", phase, log); if (strstr(log, "auid=1000") == NULL) error(1, 0, "%s: old syslog auid field missing: %s", phase, log); if (strstr(log, " path=/bin/ls") == NULL) error(1, 0, "%s: old syslog path field missing: %s", phase, log); if (strstr(log, " pid=") != NULL) error(1, 0, "%s: stale reload syslog field leaked: %s", phase, log); } /* * require_decision_sources - verify policy evaluation reports rule/fallback * @void: no arguments are required. * Returns nothing. */ static void require_decision_sources(void) { char log[LOGBUF]; event_t e; decision_t decision; decision_source_t source; prep_event(&e, 1000, "/bin/ls"); decision = process_capture(&e, log, sizeof(log), &source); free_event(&e); if (decision != ALLOW_SYSLOG || source != DECISION_SOURCE_RULE) error(1, 0, "rule allow source not reported"); prep_event(&e, 1000, "/bin/cat"); decision = process_capture(&e, log, sizeof(log), &source); free_event(&e); if (decision != ALLOW || source != DECISION_SOURCE_FALLTHROUGH) error(1, 0, "fallthrough allow source not reported"); } /* * require_missing_proc_fields_log - log absent process attributes safely. * * The synthetic event has pid 0, so /proc status lookup fails. syslog * formatting must use placeholders rather than dereferencing a missing * UID/GID attribute set. * * Returns nothing; exits through error() on an unexpected decision or log. */ static void require_missing_proc_fields_log(void) { conf_t cfg = { .syslog_format = PROC_FALLBACK_SYSLOG_FORMAT }; char log[LOGBUF]; event_t e; decision_t decision; if (load_text_policy(&cfg, "allow_syslog perm=any auid=1000 : path=/bin/ls\n")) error(1, 0, "proc fallback policy load failed"); prep_event(&e, 1000, "/bin/ls"); decision = process_capture(&e, log, sizeof(log), NULL); free_event(&e); if (decision != ALLOW_SYSLOG) error(1, 0, "proc fallback policy did not match"); if (strstr(log, "uid=?") == NULL || strstr(log, "gid=?") == NULL || strstr(log, "comm=?") == NULL) error(1, 0, "missing proc fields were not logged safely: %s", log); } /* * require_fallthrough_metrics_use_cached_attrs - prevent lazy metric lookups * @void: no arguments are required. * Returns nothing. */ static void require_fallthrough_metrics_use_cached_attrs(void) { decision_metrics_t metrics; event_t e; reset_policy_metrics(); reset_object_attr_metrics(); prep_event(&e, 1000, "/bin/ls"); e.type = FAN_OPEN_EXEC_PERM; add_cached_object_attrs(&e); policy_metrics_record_decision(ALLOW, &e, DECISION_SOURCE_FALLTHROUGH); free_event(&e); require_no_object_attr_metrics("fallthrough metrics"); getDecisionMetricsReset(&metrics, 1); getAllowedReset(1); if (metrics.allowed_by_fallthrough != 1 || metrics.fallthrough_execute != 1 || metrics.fallthrough_trusted != 1 || metrics.fallthrough_executable != 1) error(1, 0, "fallthrough metrics did not use cached attrs"); if (metrics.fallthrough_unknown_ftype || metrics.fallthrough_trust_unknown) error(1, 0, "cached fallthrough attrs reported as unknown"); } /* * require_rule_hit_counters - verify per-rule hits and generation reset. * @cfg: configuration providing syslog_format. * Returns nothing. */ static void require_rule_hit_counters(const conf_t *cfg) { char report[LOGBUF]; if (load_text_policy(cfg, "allow perm=any auid=1000 : path=/bin/ls\n" "deny perm=any auid=1000 : path=/bin/rm\n")) error(1, 0, "rule hit policy load failed"); if (process_path("/bin/ls") != ALLOW) error(1, 0, "allow rule did not match"); if (process_path("/bin/ls") != ALLOW) error(1, 0, "allow rule did not match second event"); if (process_path("/bin/rm") != DENY) error(1, 0, "deny rule did not match"); if (process_path("/bin/cat") != ALLOW) error(1, 0, "fallthrough path was not allowed"); read_rule_hits_report(report, sizeof(report), 0); if (strstr(report, "Hits/rule: 1 2 allow perm=any auid=1000 : path=/bin/ls\n") == NULL) error(1, 0, "allow rule hit report missing: %s", report); if (strstr(report, "Hits/rule: 2 1 deny perm=any auid=1000 : path=/bin/rm\n") == NULL) error(1, 0, "deny rule hit report missing: %s", report); if (strstr(report, "/bin/cat") != NULL) error(1, 0, "fallthrough path appeared in rule hits: %s", report); read_rule_hits_report(report, sizeof(report), 1); if (strstr(report, "Hits/rule: 1 2 allow perm=any auid=1000 : path=/bin/ls\n") == NULL) error(1, 0, "reset report lost allow rule hits: %s", report); if (strstr(report, "Hits/rule: 2 1 deny perm=any auid=1000 : path=/bin/rm\n") == NULL) error(1, 0, "reset report lost deny rule hits: %s", report); read_rule_hits_report(report, sizeof(report), 0); if (strstr(report, "Hits/rule: 1 0 allow perm=any auid=1000 : path=/bin/ls\n") == NULL) error(1, 0, "manual reset did not clear allow hits: %s", report); if (strstr(report, "Hits/rule: 2 0 deny perm=any auid=1000 : path=/bin/rm\n") == NULL) error(1, 0, "manual reset did not clear deny hits: %s", report); if (process_path("/bin/rm") != DENY) error(1, 0, "deny rule did not match after manual reset"); read_rule_hits_report(report, sizeof(report), 0); if (strstr(report, "Hits/rule: 2 1 deny perm=any auid=1000 : path=/bin/rm\n") == NULL) error(1, 0, "deny rule did not count after manual reset: %s", report); if (load_text_policy(cfg, "allow perm=any auid=1000 : path=/bin/ls\n" "deny perm=any auid=1000 : path=/bin/rm\n")) error(1, 0, "rule hit policy reload failed"); read_rule_hits_report(report, sizeof(report), 0); if (strstr(report, "Hits/rule: 1 0 allow perm=any auid=1000 : path=/bin/ls\n") == NULL) error(1, 0, "allow rule hits did not reset: %s", report); if (strstr(report, "Hits/rule: 2 0 deny perm=any auid=1000 : path=/bin/rm\n") == NULL) error(1, 0, "deny rule hits did not reset: %s", report); } /* * main - exercise transactional reload failure paths * @void: no arguments are required. * Returns 0 on success. Exits with error() on test failure. */ int main(void) { conf_t good_cfg = { .syslog_format = OLD_SYSLOG_FORMAT }; conf_t pid_cfg = { .syslog_format = PID_SYSLOG_FORMAT }; conf_t bad_syslog_cfg = { .syslog_format = "rule,dec,perm,pid,:,path,bogus" }; set_message_mode(MSG_STDERR, DBG_NO); if (load_text_policy(&good_cfg, "allow_syslog perm=any auid=1000 : path=/bin/ls\n")) error(1, 0, "initial policy load failed"); require_old_policy("initial load"); require_decision_sources(); require_fallthrough_metrics_use_cached_attrs(); if (load_text_policy(&pid_cfg, "deny_syslog perm=any auid=1000 uid=-1 : path=/bin/ls\n") == 0) error(1, 0, "invalid rule reload succeeded"); require_old_policy("invalid rule reload"); if (load_text_policy(&bad_syslog_cfg, "deny_syslog perm=any auid=1000 : path=/bin/ls\n") == 0) error(1, 0, "invalid syslog reload succeeded"); require_old_policy("invalid syslog reload"); require_rule_hit_counters(&good_cfg); require_missing_proc_fields_log(); atomic_store(&stop, true); destroy_rules(); return 0; } fapolicyd-2.0/src/tests/queue_test.c000066400000000000000000000145771524071644000176260ustar00rootroot00000000000000/* * queue_test.c - verify queue metric accounting */ #include #include #include #include #include #include "queue.h" #define CHECK(expr, code, msg) \ do { \ if (!(expr)) \ error(1, 0, "%s", msg); \ } while (0) /* * read_queue_report - capture queue text report output. * @metrics: metrics to write. * @buf: destination buffer. * @size: size of @buf. * Returns nothing. Exits if the temporary stream cannot be used. */ static void read_queue_report(const struct queue_metrics *metrics, char *buf, size_t size) { FILE *f = tmpfile(); size_t used; if (f == NULL) error(1, 0, "tmpfile failed"); q_metrics_report(f, metrics); fflush(f); rewind(f); used = fread(buf, 1, size - 1, f); buf[used] = 0; fclose(f); } /* * read_queue_worker_report - capture per-worker queue report output. * @metrics: metrics to write. * @buf: destination buffer. * @size: size of @buf. * Returns nothing. Exits if the temporary stream cannot be used. */ static void read_queue_worker_report(const struct queue_metrics *metrics, char *buf, size_t size) { FILE *f = tmpfile(); size_t used; if (f == NULL) error(1, 0, "tmpfile failed"); q_metrics_report_worker(f, 7, metrics); fflush(f); rewind(f); used = fread(buf, 1, size - 1, f); buf[used] = 0; fclose(f); } /* * wait_for_queue_age - give monotonic queue-age metrics time to advance. * Returns nothing. */ static void wait_for_queue_age(void) { struct timespec ts = { .tv_sec = 0, .tv_nsec = 1000000, }; while (nanosleep(&ts, &ts) == -1 && errno == EINTR) ; } /* * main - exercise queue current depth, max depth, full count, and reporting. * Returns 0 on success. Exits with error() on test failure. */ int main(void) { decision_event_t event = { 0 }; decision_event_t out = { 0 }; struct queue_metrics metrics; struct queue *q; char report[256]; char worker_report[256]; unsigned int run_max, saved; q = q_open(2); CHECK(q != NULL, 1, "[ERROR:1] q_open failed"); q_metrics_snapshot(q, &metrics); CHECK(metrics.current_depth == 0, 2, "[ERROR:2] initial current depth not zero"); CHECK(metrics.max_depth == 0, 3, "[ERROR:3] initial max depth not zero"); CHECK(metrics.full_count == 0, 4, "[ERROR:4] initial full count not zero"); CHECK(metrics.oldest_age_ns == 0, 31, "[ERROR:31] initial oldest age not zero"); CHECK(q_enqueue(q, &event) == 0, 5, "[ERROR:5] first enqueue failed"); CHECK(q_enqueue(q, &event) == 0, 6, "[ERROR:6] second enqueue failed"); wait_for_queue_age(); errno = 0; CHECK(q_enqueue(q, &event) == -1 && errno == ENOSPC, 7, "[ERROR:7] full queue did not return ENOSPC"); q_metrics_snapshot(q, &metrics); CHECK(metrics.current_depth == 2, 8, "[ERROR:8] full current depth incorrect"); CHECK(metrics.max_depth == 2, 9, "[ERROR:9] max depth incorrect"); CHECK(metrics.full_count == 1, 10, "[ERROR:10] full count incorrect"); CHECK(metrics.oldest_age_ns > 0, 32, "[ERROR:32] full queue oldest age did not advance"); CHECK(q_dequeue(q, &out) == 1, 11, "[ERROR:11] dequeue failed"); q_metrics_snapshot(q, &metrics); CHECK(metrics.current_depth == 1, 12, "[ERROR:12] dequeue current depth incorrect"); CHECK(metrics.max_depth == 2, 13, "[ERROR:13] dequeue changed max depth"); CHECK(metrics.full_count == 1, 14, "[ERROR:14] dequeue changed full count"); read_queue_report(&metrics, report, sizeof(report)); CHECK(strstr(report, "Inter-thread current queue depth: 1\n") != NULL, 15, "[ERROR:15] queue report missing current depth"); CHECK(strstr(report, "Inter-thread max queue depth: 2\n") != NULL, 37, "[ERROR:37] queue report missing max depth"); CHECK(strstr(report, "Inter-thread queue full count: 1\n") != NULL, 38, "[ERROR:38] queue report missing full count"); CHECK(strstr(report, "Inter-thread oldest queued age: ") != NULL, 39, "[ERROR:39] queue report missing oldest age"); read_queue_worker_report(&metrics, worker_report, sizeof(worker_report)); CHECK(strstr(worker_report, " Decision worker 7 current queue depth: 1\n") != NULL, 33, "[ERROR:33] worker report missing current depth"); CHECK(strstr(worker_report, " Decision worker 7 max queue depth: 2\n") != NULL, 34, "[ERROR:34] worker report missing max depth"); CHECK(strstr(worker_report, " Decision worker 7 queue full count: 1\n") != NULL, 35, "[ERROR:35] worker report missing full count"); CHECK(strstr(worker_report, " Decision worker 7 oldest queued age: ") != NULL, 36, "[ERROR:36] worker report missing oldest age"); q_metrics_snapshot_reset(q, &metrics, 1); CHECK(metrics.current_depth == 1, 16, "[ERROR:16] reset snapshot changed current depth"); CHECK(metrics.max_depth == 2, 17, "[ERROR:17] reset snapshot lost previous max depth"); CHECK(metrics.full_count == 1, 18, "[ERROR:18] reset snapshot lost previous full count"); q_metrics_snapshot(q, &metrics); CHECK(metrics.current_depth == 1, 19, "[ERROR:19] reset changed current depth state"); CHECK(metrics.max_depth == 1, 20, "[ERROR:20] reset did not restart max depth at current depth"); CHECK(metrics.full_count == 0, 21, "[ERROR:21] reset did not clear full count"); saved = q_max_depth_snapshot_reset(q); CHECK(saved == 1, 22, "[ERROR:22] max depth reset returned wrong saved value"); CHECK(q_enqueue(q, &event) == 0, 23, "[ERROR:23] enqueue after max depth reset failed"); run_max = q_max_depth_snapshot_restore(q, saved); CHECK(run_max == 2, 24, "[ERROR:24] max depth restore returned wrong run value"); q_metrics_snapshot(q, &metrics); CHECK(metrics.max_depth == 2, 25, "[ERROR:25] restore changed larger run max depth"); CHECK(q_dequeue(q, &out) == 1, 26, "[ERROR:26] second dequeue failed"); saved = q_max_depth_snapshot_reset(q); CHECK(saved == 2, 27, "[ERROR:27] second max depth reset lost saved high water"); q_metrics_snapshot(q, &metrics); CHECK(metrics.max_depth == 1, 28, "[ERROR:28] max depth reset did not restart at current depth"); run_max = q_max_depth_snapshot_restore(q, saved); CHECK(run_max == 1, 29, "[ERROR:29] restore did not return timing run max depth"); q_metrics_snapshot(q, &metrics); CHECK(metrics.max_depth == 2, 30, "[ERROR:30] restore did not preserve pre-run max depth"); q_close(q); return 0; } fapolicyd-2.0/src/tests/rpm_backend_test.c000066400000000000000000000062301524071644000207320ustar00rootroot00000000000000/* * rpm_backend_test.c - verify rpm backend loader failure handling */ #include "config.h" #include #include #include #include #include #include #include #include #include #include "backend-manager.h" #include "fapolicyd-backend.h" extern backend rpm_backend; #define CHILD_WAIT_MS 2000 #define LOADER_TEST_TIMEOUT_MS 100 #define LOADER_TEST_MAX_WAIT_MS 1000 #define CHECK(expr, code, msg) \ do { \ if (!(expr)) \ error(code, 0, "%s", msg); \ } while (0) /* * monotonic_ms - return monotonic milliseconds for timeout validation. * Returns -1 when the monotonic clock is unavailable. */ static int64_t monotonic_ms(void) { struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now)) return -1; return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000; } /* * wait_for_parent_close - act as a loader that never sends a snapshot. * Returns after the parent closes fd 3 or after a test safety timeout. */ static int wait_for_parent_close(void) { struct pollfd pfd = { .fd = 3, .events = POLLIN }; int rc; do { rc = poll(&pfd, 1, CHILD_WAIT_MS); } while (rc < 0 && errno == EINTR); return rc > 0 ? 0 : 1; } /* * true_path - find a harmless helper that exits without sending an fd. * Returns an executable path, or NULL when none is available. */ static const char *true_path(void) { if (access("/bin/true", X_OK) == 0) return "/bin/true"; if (access("/usr/bin/true", X_OK) == 0) return "/usr/bin/true"; return NULL; } /* * main - run rpm loader IPC failure coverage. * Returns 0 on success. Exits with error() on test failure. */ int main(void) { const char *path = true_path(); char self_path[PATH_MAX]; conf_t cfg; int64_t elapsed; int64_t start; ssize_t len; int rc; if (getenv("FAPO_SOCK_FD") != NULL) return wait_for_parent_close(); if (path == NULL) return 77; memset(&cfg, 0, sizeof(cfg)); cfg.trust = "rpmdb"; CHECK(backend_init(&cfg) == 0, 1, "[ERROR:1] rpmdb init failed"); rpm_backend.memfd = -1; rpm_backend.entries = -1; rc = rpm_backend_load_from_path_for_tests(&cfg, path, LOADER_TEST_MAX_WAIT_MS); CHECK(rc != 0, 2, "[ERROR:2] rpm IPC failure returned success"); CHECK(rpm_backend.memfd == -1, 3, "[ERROR:3] failed rpm load published a memfd"); CHECK(rpm_backend.entries == -1, 4, "[ERROR:4] failed rpm load published entries"); len = readlink("/proc/self/exe", self_path, sizeof(self_path) - 1); CHECK(len > 0, 5, "[ERROR:5] cannot resolve rpm test executable"); self_path[len] = '\0'; start = monotonic_ms(); CHECK(start >= 0, 6, "[ERROR:6] cannot read monotonic clock"); rc = rpm_backend_load_from_path_for_tests(&cfg, self_path, LOADER_TEST_TIMEOUT_MS); elapsed = monotonic_ms() - start; CHECK(rc != 0, 7, "[ERROR:7] stalled rpm loader returned success"); CHECK(elapsed >= 0 && elapsed < LOADER_TEST_MAX_WAIT_MS, 8, "[ERROR:8] stalled rpm loader wait was not bounded"); CHECK(rpm_backend.memfd == -1, 9, "[ERROR:9] stalled rpm load published a memfd"); CHECK(rpm_backend.entries == -1, 10, "[ERROR:10] stalled rpm load published entries"); backend_close(); return 0; } fapolicyd-2.0/src/tests/rules_test.c000066400000000000000000000671661524071644000176360ustar00rootroot00000000000000/* * rules_test.c - verify parsing and evaluation of policy rules * * Test strategy summary * --------------------- * This harness exercises the rule parser and evaluator for: * 1. direct values and %set references * 2. rule_evaluate() subject/object matching * 3. error paths: undefined sets, type mismatches, and mixed * valid/invalid same-side attributes * * Valid rules live in src/tests/fixtures/rules-valid.rules. Each line is * fed through rules_append() to mimic fagenrules processing. Negative * cases are described in the err_cases array below; QE can extend * coverage by appending new entries. */ #include "config.h" #include #include #include #include #include #include #include #include "attr-sets.h" #include "conf.h" #include "rules.h" #include "subject.h" #include "object.h" #include "event.h" #include "message.h" #define ERRBUF 4096 #ifndef TEST_BASE #define TEST_BASE "." #endif #define VALID_RULES TEST_BASE "/src/tests/fixtures/rules-valid.rules" /* globals expected by library code */ conf_t config; int debug_mode; atomic_bool stop; /* definition of a negative parsing test */ struct err_case { const char *lines[3]; const char *expect; }; static decision_t evaluate(const llist *l, event_t *e); static const struct err_case errors[] = { { { "allow perm=any auid=%missing : path=/bin/ls", NULL }, "set 'missing' was not defined before" }, { { "allow perm=any all : path=%missing", NULL }, "set 'missing' was not defined before" }, { { "%strs=foo,bar", "allow perm=any auid=%strs : path=/bin/ls", NULL }, "cannot assign %strs which has STRING type to auid (UNSIGNED expected)" }, { { "%nums=1,2", "allow perm=any all : path=%nums", NULL }, "SIGNED set nums to the STRING attribute" }, { { "%strs=wheel,staff", "allow perm=any gid=%strs : path=/bin/ls", NULL }, "cannot assign %strs which has STRING type to gid (UNSIGNED expected)" }, { { "%dupe=1,2", "%dupe=3,4", NULL }, "set dupe was already defined!" }, { { "%languages+=application/x-vendor-script", NULL }, "set languages must be defined before it can be extended" }, { { "%custom=one", "%custom+=two", NULL }, "set append is only supported for %languages" }, { { "%languages=1", "%languages+=application/x-vendor-script", NULL }, "set languages must have STRING type" }, { { "%languages=text/x-python", "%languages+=", NULL }, "%languages+= requires at least one MIME type" }, { { "%languages=text/x-python", "%languages+=glob:application/*", NULL }, "glob values are not valid in %languages" }, { { "allow auid=1000 uid=-1 path=/bin/ls", NULL }, "negative value -1 not allowed for uid" }, { { "allow perm=any auid=-1 : path=/bin/ls", NULL }, "negative value -1 not allowed for auid" }, { { "allow auid=1000 path=/bin/ls trust=2", NULL }, "trust can be set to 1 or 0" }, { { "allow perm=any auid=1000 uid=-1 : path=/bin/ls", NULL }, "negative value -1 not allowed for uid" }, { { "allow perm=any auid=1000 : path=/bin/ls trust=2", NULL }, "trust can be set to 1 or 0" }, { { "allow perm=any dir=glob:/home/*/bin/ : all", NULL }, "subject dir does not support glob patterns; glob: is valid only with exe and path" }, { { "allow perm=any all : dir=glob:/home/*/bin/", NULL }, "object dir does not support glob patterns; glob: is valid only with exe and path" }, { { "%dirs=glob:/home/*/bin/", "allow perm=any dir=%dirs : all", NULL }, "subject dir does not support glob patterns; glob: is valid only with exe and path" }, { { "%dirs=glob:/home/*/bin/", "allow perm=any all : dir=%dirs", NULL }, "object dir does not support glob patterns; glob: is valid only with exe and path" }, { { "allow perm=any comm=glob:python* : all", NULL }, "subject comm does not support glob patterns; glob: is valid only with exe and path" }, { { "allow perm=any all : ftype=glob:application/*", NULL }, "object ftype does not support glob patterns; glob: is valid only with exe and path" }, { { "allow perm=any all : path=glob:", NULL }, "object path glob pattern must be an absolute path" }, { { "allow perm=any exe=glob:opt/app-* : all", NULL }, "subject exe glob pattern must be an absolute path" } }; /* * append_capture - invoke rules_append() while capturing stderr * * l: rule list * line: rule text * ln: line number for error reporting * buf: destination buffer for any message emitted */ static int append_capture(llist *l, const char *line, unsigned ln, char *buf, size_t buflen) { int p[2]; if (pipe(p)) error(1, errno, "pipe failed"); fflush(stderr); int save = dup(STDERR_FILENO); if (save == -1) error(1, errno, "dup failed"); if (dup2(p[1], STDERR_FILENO) == -1) error(1, errno, "dup2 failed"); close(p[1]); char *tmp = strdup(line); if (!tmp) error(1, errno, "strdup failed"); int rc = rules_append(l, tmp, ln); free(tmp); fflush(stderr); if (dup2(save, STDERR_FILENO) == -1) error(1, errno, "dup2 restore failed"); close(save); ssize_t r = read(p[0], buf, buflen - 1); if (r < 0) r = 0; buf[r] = '\0'; close(p[0]); return rc; } /* * prep_event - allocate and populate an event for evaluation */ static void prep_event(event_t *e, unsigned int auid, const char *path) { e->s = malloc(sizeof(s_array)); e->o = malloc(sizeof(o_array)); if (!e->s || !e->o) error(1, errno, "malloc failed"); if (subject_create(e->s) || object_create(e->o)) error(1, errno, "event array allocation failed"); e->s->info = calloc(1, sizeof(struct proc_info)); if (!e->s->info) error(1, errno, "calloc failed"); subject_attr_t sattr = { .type = AUID, .uval = auid }; if (subject_add(e->s, &sattr)) error(1, 0, "subject_add failed"); object_attr_t oattr = { .type = PATH, .o = strdup(path) }; if (!oattr.o) error(1, errno, "strdup failed"); if (object_add(e->o, &oattr)) error(1, 0, "object_add failed"); e->type = 0; } /* * prep_macro_event - build an event with explicit subject/object paths * * e: event to populate * exe: subject executable path * obj: object path * * Returns: none */ static void prep_macro_event(event_t *e, const char *exe, const char *obj) { e->s = malloc(sizeof(s_array)); e->o = malloc(sizeof(o_array)); if (!e->s || !e->o) error(1, errno, "malloc failed"); if (subject_create(e->s) || object_create(e->o)) error(1, errno, "event array allocation failed"); e->s->info = calloc(1, sizeof(struct proc_info)); if (!e->s->info) error(1, errno, "calloc failed"); subject_attr_t exe_attr = { .type = EXE, .str = strdup(exe) }; if (!exe_attr.str) error(1, errno, "strdup failed"); if (subject_add(e->s, &exe_attr)) error(1, 0, "subject_add failed"); object_attr_t path_attr = { .type = PATH, .o = strdup(obj) }; if (!path_attr.o) error(1, errno, "strdup failed"); if (object_add(e->o, &path_attr)) error(1, 0, "object_add failed"); e->type = 0; } /* * prep_nfsd_event - build a kernel NFS server open event for policy tests. * @e: event to initialize. * @comm: kernel thread comm value. * @ppid: parent process ID reported by procfs. * @event_type: fanotify permission event mask. * * The object is a language file so a following language denial verifies that * an nfsd exception must match before the ordinary language policy. */ static void prep_nfsd_event(event_t *e, const char *comm, pid_t ppid, uint64_t event_type) { subject_attr_t comm_attr = { .type = COMM, .str = strdup(comm) }; subject_attr_t ppid_attr = { .type = PPID, .pid = ppid }; object_attr_t path_attr = { .type = PATH, .o = strdup("/srv/export/module.py") }; object_attr_t ftype_attr = { .type = FTYPE, .o = strdup("text/x-python") }; memset(e, 0, sizeof(*e)); e->s = malloc(sizeof(s_array)); e->o = malloc(sizeof(o_array)); if (!e->s || !e->o) error(1, errno, "malloc failed"); if (subject_create(e->s) || object_create(e->o)) error(1, errno, "event array allocation failed"); e->s->info = calloc(1, sizeof(struct proc_info)); if (!e->s->info) error(1, errno, "calloc failed"); if (!comm_attr.str || !path_attr.o || !ftype_attr.o) error(1, errno, "strdup failed"); if (subject_add(e->s, &comm_attr) || subject_add(e->s, &ppid_attr) || object_add(e->o, &path_attr) || object_add(e->o, &ftype_attr)) error(1, 0, "attribute setup failed"); e->type = event_type; } /* * add_trust_attrs - add cached subject and object trust values * * e: event to update * subj_trusted: subject trust value to cache * obj_trusted: object trust value to cache * * Returns: none */ static void add_trust_attrs(event_t *e, unsigned int subj_trusted, int obj_trusted) { subject_attr_t subj_trust = { .type = SUBJ_TRUST, .uval = subj_trusted }; object_attr_t obj_trust = { .type = OBJ_TRUST, .val = obj_trusted }; if (subject_add(e->s, &subj_trust)) error(1, 0, "subject_add trust failed"); if (object_add(e->o, &obj_trust)) error(1, 0, "object_add trust failed"); } /* * prep_pattern_event - build an event with explicit pattern state * * e: event to populate * state: startup-pattern state to expose to rule evaluation * elf_info: ELF classification flags to expose to rule evaluation * path1: first startup path recorded for the subject * * Returns: none */ static void prep_pattern_event(event_t *e, state_t state, uint32_t elf_info, const char *path1) { memset(e, 0, sizeof(*e)); e->s = malloc(sizeof(s_array)); e->o = malloc(sizeof(o_array)); if (!e->s || !e->o) error(1, errno, "malloc failed"); if (subject_create(e->s) || object_create(e->o)) error(1, errno, "event array allocation failed"); e->s->info = calloc(1, sizeof(struct proc_info)); if (!e->s->info) error(1, errno, "calloc failed"); e->s->info->pid = getpid(); e->s->info->state = state; e->s->info->elf_info = elf_info; e->s->info->path1 = strdup(path1); if (!e->s->info->path1) error(1, errno, "strdup failed"); e->type = FAN_OPEN_EXEC_PERM; } /* * free_event - release memory from prep_event() */ static void free_event(event_t *e) { subject_clear(e->s); object_clear(e->o); free(e->s); free(e->o); } /* * test_language_set_extension - verify package MIME additions to %languages. * * The extension is a set union, so a duplicate MIME remains harmless while a * new MIME becomes visible to ftype rules that use the built-in macro. * Returns nothing. Exits on test failure. */ static void test_language_set_extension(void) { char err[ERRBUF]; llist l; event_t e; object_attr_t ftype = { .type = FTYPE, .o = strdup("application/x-vendor-script"), }; attr_sets_entry_t *languages; int rc; if (!ftype.o) error(1, errno, "strdup failed"); if (rules_create(&l)) error(1, 0, "rules_create failed"); rc = append_capture(&l, "%languages=text/x-python", 1, err, sizeof(err)); if (rc) error(1, 0, "language set parse failed: %s", err); rc = append_capture(&l, "%languages+=application/x-vendor-script,text/x-python", 2, err, sizeof(err)); if (rc) error(1, 0, "language set extension parse failed: %s", err); rc = append_capture(&l, "allow perm=open all : ftype=%languages", 3, err, sizeof(err)); if (rc) error(1, 0, "language ftype rule parse failed: %s", err); languages = attr_sets_find(l.sets, "languages"); if (!languages || !attr_set_check_str(languages, "application/x-vendor-script")) error(1, 0, "language extension MIME was not added"); prep_event(&e, 1000, "/tmp/vendor-script"); if (object_add(e.o, &ftype)) error(1, 0, "language ftype setup failed"); e.type = FAN_OPEN_PERM; if (evaluate(&l, &e) != ALLOW) error(1, 0, "extended language MIME did not match ftype rule"); free_event(&e); rules_clear(&l); } /* * load_fixture - parse rule lines from a fixture file */ static void load_fixture(const char *path, llist *l) { char err[ERRBUF]; FILE *f = fopen(path, "r"); char line[256]; unsigned ln = 1; if (!f) error(1, errno, "open %s", path); while (fgets(line, sizeof(line), f)) { line[strcspn(line, "\n")] = '\0'; if (append_capture(l, line, ln, err, sizeof(err))) error(1, 0, "fixture parse failed line %u: %s", ln, err); ln++; } fclose(f); } /* * evaluate - walk the rule list until a decision is reached */ static decision_t evaluate(const llist *l, event_t *e) { lnode *cur; for (cur = l->head; cur; cur = cur->next) { decision_t d = rule_evaluate(cur, e); if (d != NO_OPINION) return d; } return NO_OPINION; } /* * evaluate_glob_rule - parse and evaluate one path-oriented rule * @rule: policy rule text to parse. * @exe: concrete subject executable path. * @path: concrete object path. * * Returns: the decision produced by the parsed rule. */ static decision_t evaluate_glob_rule(const char *rule, const char *exe, const char *path) { char err[ERRBUF]; llist l; event_t e; decision_t decision; if (rules_create(&l)) error(1, 0, "rules_create failed"); if (append_capture(&l, rule, 1, err, sizeof(err))) error(1, 0, "glob rule parse failed: %s", err); prep_macro_event(&e, exe, path); decision = evaluate(&l, &e); free_event(&e); rules_clear(&l); return decision; } /* * test_glob_rules - verify exe and path glob semantics * * Explicitly marked globs match whole paths without crossing directory * components or matching a leading period implicitly. Unmarked * metacharacters remain exact for compatibility. Escapes, named sets, * immutable event paths, literal dir prefixes, and first-match ordering are * covered alongside the positive cases. * * Returns: none. Exits on test failure. */ static void test_glob_rules(void) { static const struct { const char *rule; const char *exe; const char *path; decision_t expected; const char *name; } cases[] = { { "allow perm=any all : path=glob:/home/*/bin/tool", "/usr/bin/bash", "/home/alice/bin/tool", ALLOW, "object component wildcard" }, { "allow perm=any all : path=glob:/home/*/bin/tool", "/usr/bin/bash", "/home/alice/project/bin/tool", NO_OPINION, "object wildcard crossed slash" }, { "allow perm=any all : path=glob:/opt/app-?/bin/tool", "/usr/bin/bash", "/opt/app-7/bin/tool", ALLOW, "question wildcard" }, { "allow perm=any all : path=glob:/opt/app-?/bin/tool", "/usr/bin/bash", "/opt/app-77/bin/tool", NO_OPINION, "question wildcard width" }, { "allow perm=any all : path=glob:/srv/app-[0-9]/tool", "/usr/bin/bash", "/srv/app-4/tool", ALLOW, "bracket wildcard" }, { "allow perm=any all : path=glob:/home/*/bin/tool", "/usr/bin/bash", "/home/.admin/bin/tool", NO_OPINION, "implicit leading period" }, { "allow perm=any all : path=glob:/home/.*/bin/tool", "/usr/bin/bash", "/home/.admin/bin/tool", ALLOW, "explicit leading period" }, { "allow perm=any all : path=glob:/home/alice/bin/*", "/usr/bin/bash", "/home/alice/bin/.tool", NO_OPINION, "implicit basename period" }, { "allow perm=any all : path=glob:/home/alice/bin/.*", "/usr/bin/bash", "/home/alice/bin/.tool", ALLOW, "explicit basename period" }, { "allow perm=any all : path=glob:/opt/app-*/bin/tool", "/usr/bin/bash", "/opt/app-2/bin/tool.bak", NO_OPINION, "whole path matching" }, { "allow perm=any exe=glob:/opt/vendor/app-*/bin/app : all", "/opt/vendor/app-2/bin/app", "/tmp/input", ALLOW, "subject executable wildcard" }, { "allow perm=any exe=glob:/opt/*/bin/app : all", "/opt/vendor/release/bin/app", "/tmp/input", NO_OPINION, "subject wildcard crossed slash" }, { "allow perm=any all : path=/tmp/name[1]", "/usr/bin/bash", "/tmp/name[1]", ALLOW, "literal bracket path" }, { "allow perm=any all : path=/tmp/name[1]", "/usr/bin/bash", "/tmp/name1", NO_OPINION, "literal bracket does not broaden" }, { "allow perm=any all : path=/tmp/name*", "/usr/bin/bash", "/tmp/name*", ALLOW, "literal star path" }, { "allow perm=any all : path=/tmp/name*", "/usr/bin/bash", "/tmp/name1", NO_OPINION, "literal star does not broaden" }, { "allow perm=any all : path=/tmp/name?", "/usr/bin/bash", "/tmp/name1", NO_OPINION, "literal question mark does not broaden" }, { "allow perm=any all : path=glob:/tmp/name[1]", "/usr/bin/bash", "/tmp/name1", ALLOW, "marked bracket expression" }, { "allow perm=any all : path=glob:/tmp/name\\*", "/usr/bin/bash", "/tmp/name*", ALLOW, "escaped wildcard in pattern" }, { "allow perm=any all : path=/no/match,glob:/home/*/bin/tool", "/usr/bin/bash", "/home/alice/bin/tool", ALLOW, "inline path alternatives" }, { "allow perm=any all : dir=/home/*/bin/", "/usr/bin/bash", "/home/*/bin/tool", ALLOW, "literal directory metacharacter" }, }; char err[ERRBUF]; llist l; event_t e; object_attr_t *path_attr; for (size_t i = 0; i < sizeof(cases)/sizeof(cases[0]); i++) { decision_t decision = evaluate_glob_rule(cases[i].rule, cases[i].exe, cases[i].path); if (decision != cases[i].expected) error(1, 0, "%s produced decision %d, expected %d", cases[i].name, decision, cases[i].expected); } if (rules_create(&l)) error(1, 0, "rules_create failed"); if (append_capture(&l, "%wine=glob:/home/*/.wine/drive_c/windows/notepad.exe,/opt/wine/notepad.exe", 1, err, sizeof(err))) error(1, 0, "glob set parse failed: %s", err); if (append_capture(&l, "allow perm=any all : path=%wine", 2, err, sizeof(err))) error(1, 0, "glob set rule parse failed: %s", err); prep_macro_event(&e, "/usr/bin/bash", "/home/alice/.wine/drive_c/windows/notepad.exe"); if (evaluate(&l, &e) != ALLOW) error(1, 0, "named glob set did not match"); path_attr = object_access(e.o, PATH); if (!path_attr || strcmp(path_attr->o, "/home/alice/.wine/drive_c/windows/notepad.exe")) error(1, 0, "glob evaluation changed the concrete object path"); free_event(&e); rules_clear(&l); if (rules_create(&l)) error(1, 0, "rules_create failed"); if (append_capture(&l, "deny perm=any all : path=glob:/home/*/bin/tool", 1, err, sizeof(err))) error(1, 0, "ordered glob rule parse failed: %s", err); if (append_capture(&l, "allow perm=any all : path=/home/alice/bin/tool", 2, err, sizeof(err))) error(1, 0, "ordered exact rule parse failed: %s", err); prep_macro_event(&e, "/usr/bin/bash", "/home/alice/bin/tool"); if (evaluate(&l, &e) != DENY) error(1, 0, "glob rules did not preserve first-match ordering"); free_event(&e); rules_clear(&l); } /* * evaluate_pattern_rule - parse and evaluate one pattern rule * * rule: policy rule text to parse * state: startup-pattern state for the synthetic event * elf_info: ELF classification flags for the synthetic event * path1: first startup path recorded for the synthetic event * * Returns: the decision produced by the parsed rule. */ static decision_t evaluate_pattern_rule(const char *rule, state_t state, uint32_t elf_info, const char *path1) { char err[ERRBUF]; llist l; event_t e; decision_t decision; if (rules_create(&l)) error(1, 0, "rules_create failed"); if (append_capture(&l, rule, 1, err, sizeof(err))) error(1, 0, "pattern rule parse failed: %s", err); prep_pattern_event(&e, state, elf_info, path1); decision = evaluate(&l, &e); free_event(&e); rules_clear(&l); return decision; } /* * test_pattern_outcome_rules - verify policy-visible pattern outcomes * * Pattern rules mutate and then clear startup path state after evaluation. * Exercise each pattern in isolation so the test pins the policy-visible * outcome without depending on rule ordering side effects. * * Returns: none. Exits on test failure. */ static void test_pattern_outcome_rules(void) { if (evaluate_pattern_rule("allow perm=any pattern=normal : all", STATE_FULL, IS_ELF|HAS_DYNAMIC, "/usr/bin/dynamic-app") != ALLOW) error(1, 0, "normal pattern rule did not allow"); if (evaluate_pattern_rule("deny perm=any pattern=ld_so : all", STATE_FULL, IS_ELF|HAS_DYNAMIC, SYSTEM_LD_SO) != DENY) error(1, 0, "ld_so pattern rule did not deny"); if (evaluate_pattern_rule("deny perm=any pattern=static : all", STATE_COLLECTING, IS_ELF, "/usr/bin/static-app") != DENY) error(1, 0, "static pattern rule did not deny"); if (evaluate_pattern_rule("deny perm=any pattern=ld_so : all", STATE_FULL, IS_ELF|HAS_DYNAMIC, "/usr/bin/dynamic-app") != NO_OPINION) error(1, 0, "ld_so pattern matched normal startup"); } /* * test_nfsd_kernel_thread_rule - verify the optional NFS server exception. * * nfsd has no executable to place in the trust database. The policy must * identify its kernel worker by comm and PPID, permit only open events, and * precede the normal language-file denial. */ static void test_nfsd_kernel_thread_rule(void) { char err[ERRBUF]; llist l; event_t e; int rc; if (rules_create(&l)) error(1, 0, "rules_create failed"); rc = append_capture(&l, "allow perm=open ppid=2 comm=nfsd : all", 1, err, sizeof(err)); if (rc) error(1, 0, "nfsd rule parse failed: %s", err); rc = append_capture(&l, "deny perm=any all : ftype=text/x-python", 2, err, sizeof(err)); if (rc) error(1, 0, "language deny rule parse failed: %s", err); prep_nfsd_event(&e, "nfsd", 2, FAN_OPEN_PERM); if (evaluate(&l, &e) != ALLOW) error(1, 0, "nfsd open did not bypass language denial"); free_event(&e); prep_nfsd_event(&e, "nfsd", 2, FAN_OPEN_EXEC_PERM); if (evaluate(&l, &e) != DENY) error(1, 0, "nfsd rule unexpectedly allowed execute event"); free_event(&e); prep_nfsd_event(&e, "nfsd", 1, FAN_OPEN_PERM); if (evaluate(&l, &e) != DENY) error(1, 0, "non-kernel nfsd process matched exception"); free_event(&e); prep_nfsd_event(&e, "kworker", 2, FAN_OPEN_PERM); if (evaluate(&l, &e) != DENY) error(1, 0, "unrelated kernel thread matched nfsd exception"); free_event(&e); rules_clear(&l); } /* * test_unset_auid_rule - verify the unsigned representation of an unset auid. * * Audit uses -1 internally for an unset login uid, but fapolicyd rule values * are unsigned. The parser must reject -1 and a rule using 4294967295 must * match the value reported for an unset audit uid. */ static void test_unset_auid_rule(void) { char err[ERRBUF]; llist l; event_t e; int rc; if (rules_create(&l)) error(1, 0, "rules_create failed"); rc = append_capture(&l, "allow perm=open auid=4294967295 : all", 1, err, sizeof(err)); if (rc) error(1, 0, "unset auid rule parse failed: %s", err); prep_event(&e, 4294967295U, "/bin/ls"); e.type = FAN_OPEN_PERM; if (evaluate(&l, &e) != ALLOW) error(1, 0, "unset auid did not match unsigned rule value"); free_event(&e); rules_clear(&l); } int main(void) { char err[ERRBUF]; llist l; event_t e; unsigned i, j; int rc; set_message_mode(MSG_STDERR, DBG_NO); test_pattern_outcome_rules(); test_glob_rules(); test_nfsd_kernel_thread_rule(); test_unset_auid_rule(); test_language_set_extension(); /* positive path using fixture file */ if (rules_create(&l)) error(1, 0, "rules_create failed"); load_fixture(VALID_RULES, &l); prep_event(&e, 1000, "/bin/ls"); if (evaluate(&l, &e) != ALLOW) error(1, 0, "direct rule evaluation failed"); free_event(&e); prep_event(&e, 1001, "/bin/ls"); if (evaluate(&l, &e) != ALLOW) error(1, 0, "set rule evaluation failed"); free_event(&e); prep_event(&e, 1001, "/usr/bin/id"); if (evaluate(&l, &e) != ALLOW) error(1, 0, "object set evaluation failed"); free_event(&e); prep_event(&e, 2000, "/bin/ls"); if (evaluate(&l, &e) != NO_OPINION) error(1, 0, "subject mismatch unexpected result"); free_event(&e); prep_event(&e, 1001, "/tmp/xx"); if (evaluate(&l, &e) != NO_OPINION) error(1, 0, "object mismatch unexpected result"); free_event(&e); rules_clear(&l); /* macro keyword matching on dir attributes */ if (rules_create(&l)) error(1, 0, "rules_create failed"); rc = append_capture(&l, "allow perm=any dir=execdirs : all", 1, err, sizeof(err)); if (rc) error(1, 0, "execdirs subject rule parse failed: %s", err); rc = append_capture(&l, "allow perm=any all : dir=systemdirs", 2, err, sizeof(err)); if (rc) error(1, 0, "systemdirs object rule parse failed: %s", err); prep_macro_event(&e, "/usr/bin/bash", "/tmp/xx"); if (evaluate(&l, &e) != ALLOW) error(1, 0, "execdirs macro subject match failed"); free_event(&e); prep_macro_event(&e, "/opt/my-tool", "/etc/hosts"); if (evaluate(&l, &e) != ALLOW) error(1, 0, "systemdirs macro object match failed"); free_event(&e); prep_macro_event(&e, "/opt/my-tool", "/var/tmp/xx"); if (evaluate(&l, &e) != NO_OPINION) error(1, 0, "unexpected macro match"); free_event(&e); rules_clear(&l); /* deprecated dir=untrusted warnings and compatibility */ if (rules_create(&l)) error(1, 0, "rules_create failed"); rc = append_capture(&l, "allow perm=any dir=untrusted : path=/tmp/payload", 1, err, sizeof(err)); if (rc) error(1, 0, "subject untrusted dir parse failed: %s", err); if (strstr(err, "subject dir=untrusted is deprecated") == NULL) error(1, 0, "subject untrusted dir warning missing: %s", err); prep_macro_event(&e, "/opt/untrusted-tool", "/tmp/payload"); add_trust_attrs(&e, 0, 0); if (evaluate(&l, &e) != ALLOW) error(1, 0, "subject untrusted dir compatibility failed"); free_event(&e); prep_macro_event(&e, "/opt/untrusted-tool", "/tmp/payload"); add_trust_attrs(&e, 0, 1); if (evaluate(&l, &e) != NO_OPINION) error(1, 0, "trusted object matched legacy exception"); free_event(&e); rules_clear(&l); if (rules_create(&l)) error(1, 0, "rules_create failed"); rc = append_capture(&l, "allow perm=any all : dir=untrusted", 1, err, sizeof(err)); if (rc) error(1, 0, "object untrusted dir parse failed: %s", err); if (strstr(err, "object dir=untrusted is deprecated") == NULL) error(1, 0, "object untrusted dir warning missing: %s", err); prep_macro_event(&e, "/usr/bin/bash", "/tmp/payload"); add_trust_attrs(&e, 1, 0); if (evaluate(&l, &e) != ALLOW) error(1, 0, "object untrusted dir compatibility failed"); free_event(&e); prep_macro_event(&e, "/usr/bin/bash", "/tmp/payload"); add_trust_attrs(&e, 1, 1); if (evaluate(&l, &e) != NO_OPINION) error(1, 0, "trusted object matched object untrusted dir"); free_event(&e); rules_clear(&l); if (rules_create(&l)) error(1, 0, "rules_create failed"); rc = append_capture(&l, "%legacy=untrusted", 1, err, sizeof(err)); if (rc) error(1, 0, "legacy set parse failed: %s", err); if (strstr(err, "dir=untrusted is deprecated") != NULL) error(1, 0, "set definition emitted dir warning: %s", err); rc = append_capture(&l, "allow perm=any all : dir=%legacy", 2, err, sizeof(err)); if (rc) error(1, 0, "object untrusted dir set parse failed: %s", err); if (strstr(err, "object dir=untrusted is deprecated") == NULL) error(1, 0, "set-based untrusted dir warning missing: %s", err); rules_clear(&l); if (rules_create(&l)) error(1, 0, "rules_create failed"); rc = append_capture(&l, "allow perm=any exe=untrusted : all", 1, err, sizeof(err)); if (rc) error(1, 0, "exe untrusted rule parse failed: %s", err); if (strstr(err, "dir=untrusted is deprecated") != NULL) error(1, 0, "exe untrusted emitted dir warning: %s", err); rules_clear(&l); /* duplicate inline string values should remain harmless */ if (rules_create(&l)) error(1, 0, "rules_create failed"); rc = append_capture(&l, "allow perm=any all : path=/bin/ls,/bin/ls", 1, err, sizeof(err)); if (rc) error(1, 0, "inline duplicate string rejected: %s", err); rules_clear(&l); /* negative parsing scenarios */ for (i = 0; i < sizeof(errors)/sizeof(errors[0]); i++) { const struct err_case *c = &errors[i]; if (rules_create(&l)) error(1, 0, "rules_create failed"); for (j = 0; c->lines[j]; j++) { rc = append_capture(&l, c->lines[j], j + 1, err, sizeof(err)); if (c->lines[j + 1] == NULL) { if (rc == 0) error(1, 0, "error case %u accepted", i); if (strstr(err, c->expect) == NULL) error(1, 0, "case %u message: %s", i, err); } else if (rc) { error(1, 0, "setup line %u failed: %s", j, err); } } rules_clear(&l); } return 0; } fapolicyd-2.0/src/tests/stress/000077500000000000000000000000001524071644000166045ustar00rootroot00000000000000fapolicyd-2.0/src/tests/stress/Makefile.am000066400000000000000000000015271524071644000206450ustar00rootroot00000000000000# Copyright 2026 Red Hat Inc. # All Rights Reserved. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2, or # (at your option) any later version. if WITH_STRESS noinst_PROGRAMS = fapolicyd-stress endif CLEANFILES = fapolicyd-stress AM_CPPFLAGS = -I${top_builddir} \ -I${top_srcdir}/src/library \ -DSTRESS_SCRIPT_DIR=\"$(abs_srcdir)/scripts\" \ -DSTRESS_CLI_PATH=\"$(abs_top_builddir)/src/fapolicyd-cli\" AM_CFLAGS = -std=gnu11 -fPIE -DPIE -g -W -Wall -Wshadow -Wundef \ -Wno-unused-result -Wno-unused-parameter -D_GNU_SOURCE fapolicyd_stress_SOURCES = fapolicyd-stress.c fapolicyd_stress_LDFLAGS = -pie -Wl,-z,relro -Wl,-z,now EXTRA_DIST = README.md \ scripts/with-shebang.sh \ scripts/without-shebang fapolicyd-2.0/src/tests/stress/README.md000066400000000000000000000550461524071644000200750ustar00rootroot00000000000000# fapolicyd stress harness `fapolicyd-stress` is a non-installed test helper for generating high-rate fanotify decision traffic against a running `fapolicyd` daemon. It is intended for development, QE, sizing, and regression work. It is not installed by `make install`. The harness exercises the currently running daemon. It does not start or select the daemon executable. By default, daemon reports are collected with `fapolicyd-cli` from the same build tree as the stress helper. The helper creates configurable process trees and runs workloads that exercise process startup tracking, subject cache collisions, object cache churn, interpreter handling, no-shebang script handling, file opens, execs, and large file reads. ## Building The helper is built only when configure is run with `--enable-stress`: ``` ./configure --enable-stress ``` The binary is built in: ``` src/tests/stress/fapolicyd-stress ``` `--enable-stress` defaults to off. A normal build does not enter this directory and does not build the helper. ## Trust Setup When `fapolicyd` is running in enforcing mode, this helper is just another locally built executable. It is not trusted by package metadata. On a typical policy it can be blocked before the stress workload starts. The interpreter workloads use committed scripts from `src/tests/stress/scripts`. Add both the helper and that scripts directory to the file trust database before running against an enforcing daemon: ``` stress_dir="$PWD/src/tests/stress" fapolicyd-cli --file add "$stress_dir/fapolicyd-stress" fapolicyd-cli --file add "$stress_dir/scripts" fapolicyd-cli --update ``` Run these commands as root. If you rebuild the helper, refresh the helper entry because the size or hash may have changed. The same paths can be passed to `fapolicyd-cli --file update`, followed by `fapolicyd-cli --update`. The harness still creates data, hash, and churn files under its temporary work directory. Those generated files are workload inputs, not persistent test programs. ## Privileges The workload generator itself does not need to run as root. It forks, execs, opens files, creates temporary files, and reads generated data using ordinary user permissions. Root is needed for the administrative parts: - Adding or updating the stress helper and scripts in the trust database. - Reliably collecting daemon status with `--status`. - Using `--timing`, because manual decision timing requests are privileged. If `--timing` is used as a non-root user, the harness exits before starting the workload and reports that the option requires root or equivalent privilege. If status collection is requested by a non-root user, the harness prints a warning because the request usually cannot signal a root-owned daemon. For local workload-only smoke tests, use `--no-status` and omit `--timing`. ## ld_so Regression Stress The subject-deferral regression workload is a named fork/exec preset: ``` src/tests/stress/fapolicyd-stress --preset ld-so-regression --timing ``` It uses the same high-concurrency fork/exec shape as the early-eviction workload. Use it to compare a build with subject deferral against one without it. A healthy deferral run should show reduced `Early subject cache evictions` and `Denied accesses` under the same daemon configuration. `Subject defer max depth` may rise during the run, while sustained `Subject defer fallbacks` means the fixed defer array or subject cache is too small for the workload. ## fapolicyd.conf Settings No special `fapolicyd.conf` setting is required for basic stress generation or for plain status snapshots. `timing_collection=manual` is required when using `--timing`. The harness asks the running daemon for a status report and verifies that the active `Timing collection mode` is `manual` before starting the workload. If the active mode is not `manual`, the harness exits with an explicit error. `reset_strategy` is not required by the harness. The harness does not reset daemon counters. It collects state and metrics before and after the workload and computes deltas from the reported counters. For easiest interpretation, leave `reset_strategy=never`, which is the default. If `reset_strategy=auto` and an interval report resets counters during the stress run, before/after deltas can undercount the workload. The harness reads `/etc/fapolicyd/fapolicyd.conf` for `report_interval` and `reset_strategy`, and it prefers the active `reset_strategy` from the daemon state report when that report is available. If it sees `reset_strategy=auto` with a nonzero `report_interval`, it prints a warning before the workload starts. `report_interval` is not required. The harness asks the daemon for state with `fapolicyd-cli --check-status` and metrics with `fapolicyd-cli --check-metrics`. If interval reports are enabled with `reset_strategy=auto`, avoid running an interval reset in the middle of a stress measurement unless that reset behavior is what you are testing. `subj_cache_size`, `obj_cache_size`, and `q_size` control how much pressure is needed before collisions, evictions, or queueing appear. The `early-evict` preset is deliberately wide so it can trigger subject cache collisions on many systems. ## Command Form And Terms A representative full command is: ``` src/tests/stress/fapolicyd-stress --workload fork-exec --roots 32 \ --fanout 8 --depth 1 --iterations 0 --seconds 60 --timing ``` The important terms are: - `workload`: The kind of activity each leaf process performs. - `root process`: A top-level worker forked by the harness parent. - `fanout`: Number of children each non-leaf process creates. - `depth`: Number of process-tree levels below each root process. - `leaf process`: A process at the bottom of the tree. Leaf processes run the workload. - `iteration`: One pass through the selected workload in a leaf process. - `operation`: A local harness action, such as an exec attempt, open, or large file read. One operation is not the same as one daemon decision. - `workdir`: The temporary directory for generated data, hash, and churn files. - `status`: Before/after daemon state snapshots. - `timing`: A daemon decision timing window wrapped around the workload. The estimated leaf process count is: ``` roots * fanout ^ depth ``` The harness does not use pthreads. Concurrency comes from processes. More root processes, fanout, or depth create more concurrent PIDs, which is the pressure needed for subject-cache collisions. On large systems, increase those process-tree controls to keep more CPUs busy. ## Harmless Default Commands When no `--command` option is given, the harness uses harmless installed system commands. For each command name it chooses the first executable path from `/usr/bin` and `/bin`: - `who` - `users` - `uname` - `pwd` - `printenv` - `nproc` - `ls` - `hostid` - `env` - `dir` - `date` - `arch` - `groups` - `hostname` - `id` - `whoami` If none of those are available, it falls back to `true` from `/usr/bin` or `/bin`. These commands are normally present on Linux systems, short-lived, read-only for normal users, and safe to run repeatedly. They can still generate substantial `fapolicyd` work because each exec produces permission events and process startup state. The harness redirects their stdout and stderr to `/dev/null` so repeated runs do not create command output logs. Use `--command PATH` to replace the default target set. It takes one executable path per option and may be repeated: ``` src/tests/stress/fapolicyd-stress --command /usr/bin/date \ --command /usr/bin/id --workload fork-exec ``` `--command` is not comma separated. It is not a single shell string containing multiple commands. If a path contains shell metacharacters or spaces, quote it for the shell that launches `fapolicyd-stress`. ## Script Workloads The script workloads use these committed, inspectable files: - `src/tests/stress/scripts/with-shebang.sh` - `src/tests/stress/scripts/without-shebang` `with-shebang.sh` has `#!/bin/sh`, accepts the generated data file path as its first argument, and loops four times running: ``` cat "$data" >/dev/null ``` `without-shebang` has no `#!` line, accepts the generated data file path as its first argument, and loops four times running: ``` test -r "$data" ``` The harness runs these scripts in place from `src/tests/stress/scripts`. The trust setup above adds that directory to file trust. The generated data file is created under the harness work directory for each run. ## Workloads Select a workload with `-w NAME` or `--workload NAME`. - `fork-exec`: Tight fork/exec loop. Each leaf process repeatedly forks a child that execs one configured harmless command. This is the default workload and the best starting point for subject-cache and startup-state pressure. - `exec-open`: Opens every configured command path and also executes one target per iteration. This adds object open traffic around the exec stream. - `interpreter`: Runs `with-shebang.sh` directly and through the selected shell. This exercises interpreter and script startup handling. - `noshebang`: Attempts a direct exec of `without-shebang`, treating `ENOEXEC` as expected, then runs the same script through the selected shell. This exercises no-shebang programmatic content paths. - `hash`: Creates a generated large file and repeatedly reads it while computing a small local hash. This produces large file open/read activity and is useful when daemon integrity settings or policy cause file hashing. - `churn`: Creates many distinct small files and opens them in rotation. This is meant to churn the object cache and expose object collisions or evictions. - `all`: Runs every workload in each leaf iteration. Use this for broad coverage, not for isolating a single bottleneck. ## Process Tree Controls - `-r N`, `--roots N`: Number of root processes to fork. Default: `4`. - `-f N`, `--fanout N`: Number of child processes created by each non-leaf process. Default: `1`. - `-d N`, `--depth N`: Number of process-tree levels below each root. Default: `0`. More leaves increase parallel pressure. Wide process trees are the main way to create PID modulo collisions in the subject cache. ## Run Length Controls - `-i N`, `--iterations N`: Number of workload iterations per leaf process. Default: `100`. Use `0` only with `--seconds`. - `-s N`, `--seconds N`: Wall-clock run length. Default: `0`, meaning the run is iteration-limited. When nonzero, the parent stops remaining workers after the deadline. At least one of `--iterations` or `--seconds` must be nonzero. ## Workload Input Controls - `-c PATH`, `--command PATH`: Add one executable target for `fork-exec` and `exec-open`. May be repeated. If omitted, the harmless default commands listed above are used. - `--hash-mb N`: Size of the generated large file for the `hash` workload. Default: `16`. - `--churn-files N`: Number of generated files for the `churn` workload. Default: `2048`. - `--workdir DIR`: Base directory where the harness creates a private temporary work directory. Default: `/tmp`. - `--keep-workdir`: Keep generated files after the run. This is useful for debugging policy denials against generated data, hash, or churn files. Without this option, the harness removes generated files before it exits. - `--shell PATH`: Shell used for the shell-invoked half of the interpreter workloads. Default selection is `/bin/sh`, then `/usr/bin/sh`. ## Daemon Report Controls By default, the harness uses exactly `$(abs_top_builddir)/src/fapolicyd-cli` and captures daemon status before and after the workload. Invocation does not depend on the current working directory. The harness does not search `PATH`, the current working directory, environment variables, or system installation directories for another client. - `--status`: Capture daemon status. This is the default. - `--no-status`: Do not capture daemon status. Use this for local smoke tests, unprivileged runs, or systems where `fapolicyd` is not running. - `--timing`: Ask the daemon to start manual decision timing before the workload and stop it after the workload. This requires root or equivalent privilege and active `timing_collection=manual`. - `--cli PATH`: Explicitly use a different `fapolicyd-cli`. If omitted, the harness uses only the client in the build tree at the path above. - `-v`, `--verbose`: Print helper command failures, such as failed status or timing captures. - `-h`, `--help`: Print the command-line help and exit. The complete daemon state and timing reports are written by the daemon under `/run/fapolicyd/`. The harness parses selected fields for its summary. ## Presets `--preset early-evict` expands to an aggressive fork/exec collision workload: ``` --workload fork-exec --roots 32 --fanout 8 --depth 1 --iterations 0 --seconds 60 ``` This creates an estimated 256 leaf processes. It is designed to go wide enough that many unrelated PIDs map to the same subject-cache slots. When one process is still in startup pattern detection and another process collides with its slot, the daemon may report early subject cache evictions. This preset is intentionally noisy. Run it on a test system. ## Examples Build and run a short local smoke test without daemon reports: ``` ./configure --enable-stress --without-deb make -j32 src/tests/stress/fapolicyd-stress --no-status --workload fork-exec \ --roots 2 --iterations 10 ``` Run a timed fork/exec pressure test against a configured daemon: ``` src/tests/stress/fapolicyd-stress --workload fork-exec --roots 32 \ --seconds 30 --timing ``` Run the early subject eviction preset: ``` src/tests/stress/fapolicyd-stress --preset early-evict --timing ``` Run broad mixed coverage: ``` src/tests/stress/fapolicyd-stress --workload all --roots 8 --fanout 4 \ --depth 1 --seconds 60 --timing ``` ## Harness Output The first section echoes the run configuration: - `workload` - `roots` - `fanout` - `depth` - `estimated leaf processes` - `iterations per leaf` - `seconds` - `workdir` The local workload summary is generated by the harness itself: - `operations`: Count of local workload operations attempted. The exact meaning depends on the workload. For example, an exec attempt, a file open, or a generated large file read counts as an operation. - `errors`: Local workload failures. Nonzero errors can mean policy denied one of the generated operations, an executable was missing, a temporary file could not be opened, or a child process failed. Use `--verbose` and daemon logs to separate policy denials from local setup problems. - `throughput_ops_per_sec`: Local harness operations per second. This is not the same as daemon decision throughput because one local operation can generate zero, one, or multiple fanotify permission events. ## Daemon Metrics Output When daemon collection succeeds, the harness prints `Daemon status deltas`. These are parsed from `fapolicyd-cli --check-status` and `fapolicyd-cli --check-metrics` before and after the run. Important fields: - `Inter-thread max queue depth`: Highest daemon userspace queue depth seen. If this grows near `q_size`, the decision thread is not keeping up with event intake. - `Early subject cache evictions`: Subject cache entries evicted before startup pattern detection completed. These are the main signal for the early-eviction problem. The `early-evict` preset is built to make this counter move. - `Subject deferred events`, `Subject defer max depth`, and `Subject defer fallbacks`: Subject-slot deferral pressure since the last metrics reset. Fallbacks mean the defer array was full and the daemon used the historical eviction path. - `Subject collisions`: Populated subject cache slots whose full process identity did not match the current event. This should rise during wide process pressure and helps confirm that early evictions were collision-driven. - `Subject evictions`: Subject cache entries evicted and reused. Compare this with subject collisions and early subject evictions. Many evictions with no early evictions usually means churn happened after subject state was complete. - `Object collisions` and `Object evictions`: Object cache churn indicators. These should move during `churn`, `all`, and workloads that touch many distinct files. - `Allowed accesses` and `Denied accesses`: Policy decisions during the interval. Denials are expected if the selected workload intentionally hits policy-blocked script or programmatic paths. They are not expected for a pure throughput run using only trusted commands. - `Kernel queue overflows`: Lost kernel fanotify visibility. Any nonzero value is a serious signal that the workload exceeded what the daemon and kernel queue could observe. - `Reply errors`: Failed fanotify response writes. Any nonzero value needs investigation. If the harness prints: ``` Daemon status: not observed ``` then it did not successfully collect both before and after status reports. Common causes are: - `fapolicyd-cli` was not available at its expected build-tree path or at the path supplied with `--cli`. - `fapolicyd` was not running. - The user could not signal the daemon or read the report. - The daemon timed out while writing the report. Use `--verbose` to show status capture failures. If `--no-status` was used, the harness skips this section entirely instead of printing `not observed`. ## Timing Output With `--timing`, the harness verifies the active timing mode, then wraps the workload in: ``` fapolicyd-cli --timing-start fapolicyd-cli --timing-stop ``` The daemon writes `/run/fapolicyd/fapolicyd.timing`, and the CLI prints it. The harness consumes that CLI output, prints the report path, and parses a short summary from that report: - `Decisions`: Number of completed daemon decisions timed during the run. - `Max queue depth during timing`: Highest userspace queue depth observed during the timing window. - `Timed throughput`: Daemon decisions per wall-clock second while timing was armed. - `Active decision rate`: Decisions per second using accumulated `decision:total` worker time. This can differ from wall-clock throughput when the workload is bursty or when more than one decision worker is active. - `Decision latency`: Parsed average, maximum, and p95 bucket for `decision:total`. If the harness prints: ``` Decision timing: not observed ``` then no timing summary was parsed. Common causes are: - `fapolicyd-cli` was not available at its expected build-tree path or at the path supplied with `--cli`. - The daemon was not running. - The daemon did not write `/run/fapolicyd/fapolicyd.timing`. - The timing report format did not contain the fields parsed by the harness. Use `--verbose` to show timing command failures. Also check the normal state report fields `Timing collection mode` and `Timing collection armed`. If `--timing` was omitted, the harness skips this section entirely instead of printing `not observed`. ## Using The Timing Report Use the full timing report with the harness output. The harness tells you what load was generated and gives a compact summary; the timing report tells you where the daemon spent time. Start with the run summary: - `Decisions` should be nonzero. If it is zero, the workload did not generate daemon decisions during the timing window. - `Max queue depth` shows whether events backed up while timing was armed. - Compare `Throughput` and `Active decision rate`. If wall-clock throughput is low but active decision rate is high, the workload may be bursty or idle part of the time. If both are low, decision work is expensive. Then review these timing sections: - `Overall decision latency`: Look at average, max, p95 bucket, and tail percentages. A high p95 means the slow path is common. A high max with a low p95 usually means rare outliers. - `Queueing`: High average or p95 queue wait means requests are waiting before evaluation. If queue wait is high and max queue depth is near `q_size`, increase pressure carefully and consider whether the daemon needs tuning. - `Decision phase timing`: Shows whether time is mostly in event build, rule evaluation, or response formatting. - `Combined lazy helper attribution`: Useful for finding expensive helpers that are called lazily from rules or response formatting. - `Detailed stage timing`: Sorts observed stages by total time. Use this to identify the main cost center. Common interpretations: - High `event_build:proc_fingerprint` means process identity lookups are a large cost. This is common in fork/exec-heavy tests. - High `event_build:fd_stat` means object fingerprinting is expensive. - High `evaluation:lock_wait` means the decision path waited for the rule lock. - High `evaluation:total` with low helper cost points to rule traversal or rule matching cost. - High `evaluation:mime_detection:*` or `response:mime_detection:*` points to file type detection cost. - High `evaluation:mime_detection:libmagic_fallback` means fast MIME classification was not enough and libmagic was used often or expensively. - High `evaluation:hash_sha:total` or `evaluation:hash_ima:total` means file integrity work is significant. The `hash` workload is useful for making this visible. - High `evaluation:trust_db_lookup:*` means trust database lookup or lock/read time is material. - High `response:syslog_debug_format:total` means reporting/debug formatting is dominating response cost. This can be more visible in debug-heavy runs than in normal daemon operation. - High `response:fanotify_write` means completing the kernel permission response is expensive or blocked. Use one workload at a time when isolating a bottleneck. Use `all` only after you understand the individual workload costs. ## State Report Review Checklist After a run, collect state and metrics reports: ``` fapolicyd-cli --check-status fapolicyd-cli --check-metrics ``` Review: - `Inter-thread max queue depth` - `Allowed accesses` - `Denied accesses` - `Kernel queue overflow` - `Reply errors` - `Per worker Subject cache size` - `Subject slots in use` - `Subject hits` - `Subject misses` - `Subject collisions` - `Subject evictions` - `Subject deferred events` - `Subject defer max depth` - `Subject defer fallbacks` - `Subject defer oldest age` - `Early subject cache evictions` - `Per worker Object cache size` - `Object slots in use` - `Object hits` - `Object misses` - `Object collisions` - `Object evictions` For early subject eviction testing, the strongest evidence is: - `Subject collisions` increased. - `Early subject cache evictions` increased. - The workload was wide enough to create many concurrent process startups. For object cache testing, look for increased object misses, collisions, and evictions during `churn` or `all`. For queue pressure, compare `Inter-thread max queue depth` with configured `q_size` and correlate it with the timing report's `Queueing` section. fapolicyd-2.0/src/tests/stress/fapolicyd-stress.c000066400000000000000000001745461524071644000222640ustar00rootroot00000000000000/* * fapolicyd-stress.c - fanotify decision stress helper * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #include "config.h" #include "gcc-attributes.h" #include "paths.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define DEFAULT_ROOTS 4U #define DEFAULT_FANOUT 1U #define DEFAULT_DEPTH 0U #define DEFAULT_ITERATIONS 100U #define DEFAULT_SECONDS 0U #define DEFAULT_HASH_MB 16U #define DEFAULT_CHURN_FILES 2048U #define GRACEFUL_STOP_NS 5000000000ULL #define TERM_STOP_NS 250000000ULL #define MAX_COMMANDS 64 #define CAPTURE_LIMIT (1024 * 1024) #define HASH_BLOCK_SIZE 65536 #ifndef STRESS_SCRIPT_DIR #define STRESS_SCRIPT_DIR "scripts" #endif #ifndef STRESS_CLI_PATH #error "STRESS_CLI_PATH must be set by the build system" #endif enum workload_type { WORKLOAD_EXEC_OPEN, WORKLOAD_FORK_EXEC, WORKLOAD_INTERPRETER, WORKLOAD_NOSHEBANG, WORKLOAD_HASH, WORKLOAD_CHURN, WORKLOAD_ALL, }; struct command_set { const char *paths[MAX_COMMANDS]; unsigned int count; }; struct stress_options { enum workload_type workload; unsigned int roots; unsigned int fanout; unsigned int depth; unsigned int iterations; unsigned int seconds; unsigned int hash_mb; unsigned int churn_files; int collect_status; int collect_timing; int keep_workdir; int verbose; const char *workdir_base; const char *cli_path; const char *shell_path; struct command_set commands; }; struct stress_paths { char workdir[PATH_MAX]; char churn_dir[PATH_MAX]; char hash_file[PATH_MAX]; char small_file[PATH_MAX]; char shebang_script[PATH_MAX]; char no_shebang_script[PATH_MAX]; int created_workdir; }; struct stress_shared { volatile sig_atomic_t stop; unsigned long long operations; unsigned long long errors; }; struct leaf_stats { unsigned long long operations; unsigned long long errors; }; struct capture { char *data; size_t len; int status; }; struct daemon_metrics { int present; unsigned long long queue_max_depth; unsigned long long subject_defer_current; unsigned long long subject_defer_max_depth; unsigned long long subject_defer_fallbacks; char subject_defer_oldest_age[32]; unsigned long long early_subject_evictions; unsigned long long subject_tracer_evictions; unsigned long long subject_stale_evictions; unsigned long long subject_collisions; unsigned long long subject_evictions; unsigned long long object_collisions; unsigned long long object_evictions; unsigned long long allowed; unsigned long long denied; unsigned long long kernel_overflow; unsigned long long reply_errors; int have_timing_mode; char timing_mode[32]; int have_reset_strategy; char reset_strategy[32]; }; struct timing_metrics { int present; unsigned long long decisions; unsigned long long max_queue_depth; double throughput; double active_rate; char avg_latency[32]; char max_latency[32]; char p95_latency[32]; }; struct daemon_config_snapshot { unsigned int report_interval; int have_report_interval; int have_reset_strategy; char reset_strategy[32]; }; static struct stress_shared *signal_shared; /* * usage - print command line help. * @prog: executable name. * Returns nothing. */ static void usage(const char *prog) { printf("Usage: %s [options]\n\n", prog); printf("Workloads:\n"); printf(" -w, --workload exec-open Open command paths and exec them\n"); printf(" -w, --workload fork-exec Tight fork/exec loops\n"); printf(" -w, --workload interpreter Shell interpreter script workload\n"); printf(" -w, --workload noshebang Direct no-shebang exec plus shell run\n"); printf(" -w, --workload hash Read and hash a large file\n"); printf(" -w, --workload churn Open many distinct files\n"); printf(" -w, --workload all Run every workload in each loop\n\n"); printf("Tree and run controls:\n"); printf(" -r, --roots N Root process count (default %u)\n", DEFAULT_ROOTS); printf(" -f, --fanout N Children per non-leaf node (default %u)\n", DEFAULT_FANOUT); printf(" -d, --depth N Tree depth below roots (default %u)\n", DEFAULT_DEPTH); printf(" -i, --iterations N Iterations per leaf, 0 for timed-only\n"); printf(" -s, --seconds N Timed run length, 0 disables timer\n"); printf(" --preset early-evict Aggressive collision workload\n"); printf(" --preset ld-so-regression Fork/exec false ld_so pressure\n\n"); printf("Workload inputs:\n"); printf(" -c, --command PATH Add one exec target; repeat as needed\n"); printf(" --hash-mb N Hash file size in MiB (default %u)\n", DEFAULT_HASH_MB); printf(" --churn-files N Cache churn file count (default %u)\n", DEFAULT_CHURN_FILES); printf(" --workdir DIR Base directory for generated files\n"); printf(" --keep-workdir Keep generated files after the run\n"); printf(" --shell PATH Shell path for interpreter workloads\n\n"); printf("Daemon reporting:\n"); printf(" --status Capture daemon status (default)\n"); printf(" --no-status Do not capture daemon status\n"); printf(" --timing Wrap run in decision timing; " "requires root\n"); printf(" --cli PATH fapolicyd-cli path\n"); printf(" -v, --verbose Print helper command failures\n"); printf(" -h, --help Show this help\n"); } /* * parse_uint - parse a non-negative unsigned integer option. * @text: option value. * @out: parsed destination. * Returns 0 on success, 1 on parse error. */ static int parse_uint(const char *text, unsigned int *out) { char *end = NULL; unsigned long value; if (text == NULL || *text == 0) return 1; errno = 0; value = strtoul(text, &end, 10); if (errno || end == text || *end || value > UINT_MAX) return 1; *out = (unsigned int)value; return 0; } /* * parse_workload - convert a workload name to an enum. * @name: workload name. * @workload: parsed destination. * Returns 0 on success, 1 on unknown workload. */ static int parse_workload(const char *name, enum workload_type *workload) { if (strcmp(name, "exec-open") == 0) *workload = WORKLOAD_EXEC_OPEN; else if (strcmp(name, "fork-exec") == 0) *workload = WORKLOAD_FORK_EXEC; else if (strcmp(name, "interpreter") == 0) *workload = WORKLOAD_INTERPRETER; else if (strcmp(name, "noshebang") == 0) *workload = WORKLOAD_NOSHEBANG; else if (strcmp(name, "hash") == 0) *workload = WORKLOAD_HASH; else if (strcmp(name, "churn") == 0) *workload = WORKLOAD_CHURN; else if (strcmp(name, "all") == 0) *workload = WORKLOAD_ALL; else return 1; return 0; } /* * workload_name - return a printable workload name. * @workload: workload enum. * Returns a stable string. */ static const char *workload_name(enum workload_type workload) { switch (workload) { case WORKLOAD_EXEC_OPEN: return "exec-open"; case WORKLOAD_FORK_EXEC: return "fork-exec"; case WORKLOAD_INTERPRETER: return "interpreter"; case WORKLOAD_NOSHEBANG: return "noshebang"; case WORKLOAD_HASH: return "hash"; case WORKLOAD_CHURN: return "churn"; case WORKLOAD_ALL: return "all"; } return "unknown"; } /* * command_set_add - append a command path. * @commands: command set to update. * @path: command path to add. * Returns 0 on success, 1 when the set is full. */ static int command_set_add(struct command_set *commands, const char *path) { if (commands->count >= MAX_COMMANDS) return 1; commands->paths[commands->count++] = path; return 0; } /* * add_first_existing - add the first executable path from a candidate list. * @commands: command set to update. * @candidates: NULL-terminated executable candidates. * Returns 0 when a candidate was added, 1 otherwise. */ static int add_first_existing(struct command_set *commands, const char * const *candidates) { unsigned int idx; for (idx = 0; candidates[idx]; idx++) { if (access(candidates[idx], X_OK) == 0) return command_set_add(commands, candidates[idx]); } return 1; } /* * add_default_commands - populate harmless installed exec targets. * @commands: command set to update. * Returns 0 when at least one target is available, 1 otherwise. */ static int add_default_commands(struct command_set *commands) { static const char * const arch_cmds[] = { "/usr/bin/arch", "/bin/arch", NULL }; static const char * const date_cmds[] = { "/usr/bin/date", "/bin/date", NULL }; static const char * const dir_cmds[] = { "/usr/bin/dir", "/bin/dir", NULL }; static const char * const env_cmds[] = { "/usr/bin/env", "/bin/env", NULL }; static const char * const groups_cmds[] = { "/usr/bin/groups", "/bin/groups", NULL }; static const char * const hostname_cmds[] = { "/usr/bin/hostname", "/bin/hostname", NULL }; static const char * const hostid_cmds[] = { "/usr/bin/hostid", "/bin/hostid", NULL }; static const char * const id_cmds[] = { "/usr/bin/id", "/bin/id", NULL }; static const char * const ls_cmds[] = { "/usr/bin/ls", "/bin/ls", NULL }; static const char * const nproc_cmds[] = { "/usr/bin/nproc", "/bin/nproc", NULL }; static const char * const printenv_cmds[] = { "/usr/bin/printenv", "/bin/printenv", NULL }; static const char * const pwd_cmds[] = { "/usr/bin/pwd", "/bin/pwd", NULL }; static const char * const whoami_cmds[] = { "/usr/bin/whoami", "/bin/whoami", NULL }; static const char * const uname_cmds[] = { "/usr/bin/uname", "/bin/uname", NULL }; static const char * const users_cmds[] = { "/usr/bin/users", "/bin/users", NULL }; static const char * const who_cmds[] = { "/usr/bin/who", "/bin/who", NULL }; static const char * const true_cmds[] = { "/usr/bin/true", "/bin/true", NULL }; unsigned int before = commands->count; add_first_existing(commands, who_cmds); add_first_existing(commands, users_cmds); add_first_existing(commands, uname_cmds); add_first_existing(commands, pwd_cmds); add_first_existing(commands, printenv_cmds); add_first_existing(commands, nproc_cmds); add_first_existing(commands, ls_cmds); add_first_existing(commands, hostid_cmds); add_first_existing(commands, env_cmds); add_first_existing(commands, dir_cmds); add_first_existing(commands, date_cmds); add_first_existing(commands, arch_cmds); add_first_existing(commands, groups_cmds); add_first_existing(commands, hostname_cmds); add_first_existing(commands, id_cmds); add_first_existing(commands, whoami_cmds); if (commands->count == before) add_first_existing(commands, true_cmds); return commands->count == before ? 1 : 0; } /* * find_shell - choose a shell for interpreter workloads. * @opts: stress options to update. * Returns 0 on success, 1 when no shell is executable. */ static int find_shell(struct stress_options *opts) { if (opts->shell_path) return access(opts->shell_path, X_OK) == 0 ? 0 : 1; if (access("/bin/sh", X_OK) == 0) { opts->shell_path = "/bin/sh"; return 0; } if (access("/usr/bin/sh", X_OK) == 0) { opts->shell_path = "/usr/bin/sh"; return 0; } return 1; } /* * set_defaults - initialize stress options. * @opts: options object to initialize. * Returns nothing. */ static void set_defaults(struct stress_options *opts) { memset(opts, 0, sizeof(*opts)); opts->workload = WORKLOAD_FORK_EXEC; opts->roots = DEFAULT_ROOTS; opts->fanout = DEFAULT_FANOUT; opts->depth = DEFAULT_DEPTH; opts->iterations = DEFAULT_ITERATIONS; opts->seconds = DEFAULT_SECONDS; opts->hash_mb = DEFAULT_HASH_MB; opts->churn_files = DEFAULT_CHURN_FILES; opts->collect_status = 1; } /* * apply_preset - apply a named preset to options. * @opts: options object to update. * @name: preset name. * Returns 0 on success, 1 on unknown preset. */ static int apply_preset(struct stress_options *opts, const char *name) { if (strcmp(name, "early-evict") != 0 && strcmp(name, "ld-so-regression") != 0) return 1; opts->workload = WORKLOAD_FORK_EXEC; opts->roots = 32; opts->fanout = 8; opts->depth = 1; opts->iterations = 0; opts->seconds = 60; return 0; } /* * parse_args - parse command line options. * @opts: options object to fill. * @argc: argument count. * @argv: argument vector. * Returns 0 on success, 1 on invalid arguments, 2 after help. */ static int parse_args(struct stress_options *opts, int argc, char **argv) { enum { OPT_HASH_MB = 256, OPT_CHURN_FILES, OPT_WORKDIR, OPT_KEEP_WORKDIR, OPT_STATUS, OPT_NO_STATUS, OPT_TIMING, OPT_CLI, OPT_SHELL, OPT_PRESET, }; static const struct option long_opts[] = { {"workload", required_argument, NULL, 'w'}, {"roots", required_argument, NULL, 'r'}, {"fanout", required_argument, NULL, 'f'}, {"depth", required_argument, NULL, 'd'}, {"iterations", required_argument, NULL, 'i'}, {"seconds", required_argument, NULL, 's'}, {"command", required_argument, NULL, 'c'}, {"hash-mb", required_argument, NULL, OPT_HASH_MB}, {"churn-files", required_argument, NULL, OPT_CHURN_FILES}, {"workdir", required_argument, NULL, OPT_WORKDIR}, {"keep-workdir", no_argument, NULL, OPT_KEEP_WORKDIR}, {"status", no_argument, NULL, OPT_STATUS}, {"no-status", no_argument, NULL, OPT_NO_STATUS}, {"timing", no_argument, NULL, OPT_TIMING}, {"cli", required_argument, NULL, OPT_CLI}, {"shell", required_argument, NULL, OPT_SHELL}, {"preset", required_argument, NULL, OPT_PRESET}, {"verbose", no_argument, NULL, 'v'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0} }; int opt; while ((opt = getopt_long(argc, argv, "w:r:f:d:i:s:c:vh", long_opts, NULL)) != -1) { switch (opt) { case 'w': if (parse_workload(optarg, &opts->workload)) return 1; break; case 'r': if (parse_uint(optarg, &opts->roots)) return 1; break; case 'f': if (parse_uint(optarg, &opts->fanout)) return 1; break; case 'd': if (parse_uint(optarg, &opts->depth)) return 1; break; case 'i': if (parse_uint(optarg, &opts->iterations)) return 1; break; case 's': if (parse_uint(optarg, &opts->seconds)) return 1; break; case 'c': if (command_set_add(&opts->commands, optarg)) return 1; break; case 'v': opts->verbose = 1; break; case 'h': usage(argv[0]); return 2; case OPT_HASH_MB: if (parse_uint(optarg, &opts->hash_mb)) return 1; break; case OPT_CHURN_FILES: if (parse_uint(optarg, &opts->churn_files)) return 1; break; case OPT_WORKDIR: opts->workdir_base = optarg; break; case OPT_KEEP_WORKDIR: opts->keep_workdir = 1; break; case OPT_STATUS: opts->collect_status = 1; break; case OPT_NO_STATUS: opts->collect_status = 0; break; case OPT_TIMING: opts->collect_timing = 1; break; case OPT_CLI: opts->cli_path = optarg; break; case OPT_SHELL: opts->shell_path = optarg; break; case OPT_PRESET: if (apply_preset(opts, optarg)) return 1; break; default: return 1; } } if (optind != argc) return 1; if (opts->roots == 0) return 1; if (opts->depth && opts->fanout == 0) return 1; if (opts->iterations == 0 && opts->seconds == 0) return 1; return 0; } /* * monotonic_ns - read monotonic time in nanoseconds. * Returns a best-effort monotonic timestamp. */ static unsigned long long monotonic_ns(void) { struct timespec ts; if (clock_gettime(CLOCK_MONOTONIC, &ts)) return 0; return (unsigned long long)ts.tv_sec * 1000000000ULL + (unsigned long long)ts.tv_nsec; } /* * signal_stop - request workload stop from a signal handler. * @sig: received signal. * Returns nothing. */ static void signal_stop(int sig __attribute__((unused))) { if (signal_shared) signal_shared->stop = 1; } /* * install_signal_handlers - arrange graceful parent interruption. * @shared: shared run state to mark on interruption. * Returns 0 on success, 1 on failure. */ static int install_signal_handlers(struct stress_shared *shared) { struct sigaction act; signal_shared = shared; memset(&act, 0, sizeof(act)); act.sa_handler = signal_stop; sigemptyset(&act.sa_mask); if (sigaction(SIGINT, &act, NULL)) return 1; if (sigaction(SIGTERM, &act, NULL)) return 1; return 0; } /* * shared_add - add leaf-local counters to shared totals. * @shared: shared counter block. * @stats: local counters to flush. * Returns nothing. */ static void shared_add(struct stress_shared *shared, struct leaf_stats *stats) { if (stats->operations) __sync_fetch_and_add(&shared->operations, stats->operations); if (stats->errors) __sync_fetch_and_add(&shared->errors, stats->errors); stats->operations = 0; stats->errors = 0; } /* * join_path - join a directory and file name. * @dst: destination buffer. * @dir: directory path. * @name: final component. * Returns 0 on success, 1 on truncation. */ static int join_path(char *dst, const char *dir, const char *name) { int rc = snprintf(dst, PATH_MAX, "%s/%s", dir, name); return rc < 0 || rc >= PATH_MAX; } /* * make_workdir - create the generated file directory. * @opts: run options. * @paths: path object to fill. * Returns 0 on success, 1 on failure. */ static int make_workdir(const struct stress_options *opts, struct stress_paths *paths) { const char *base = opts->workdir_base ? opts->workdir_base : "/tmp"; memset(paths, 0, sizeof(*paths)); if (snprintf(paths->workdir, sizeof(paths->workdir), "%s/fapolicyd-stress.XXXXXX", base) >= (int)sizeof(paths->workdir)) return 1; if (mkdtemp(paths->workdir) == NULL) return 1; paths->created_workdir = 1; return 0; } /* * write_file_data - create a file with exact contents and mode. * @path: destination path. * @buf: bytes to write. * @len: byte count. * @mode: file mode to set. * Returns 0 on success, 1 on failure. */ static int write_file_data(const char *path, const void *buf, size_t len, mode_t mode) { const char *ptr = buf; size_t done = 0; int fd; fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, mode); if (fd < 0) return 1; while (done < len) { ssize_t rc = write(fd, ptr + done, len - done); if (rc < 0) { if (errno == EINTR) continue; close(fd); return 1; } done += (size_t)rc; } if (fchmod(fd, mode)) { close(fd); return 1; } close(fd); return 0; } /* * create_large_file - create the file used by the hash workload. * @path: destination path. * @size_mb: size in MiB. * Returns 0 on success, 1 on failure. */ static int create_large_file(const char *path, unsigned int size_mb) { unsigned char buf[HASH_BLOCK_SIZE]; unsigned long long total = (unsigned long long)size_mb * 1024ULL * 1024ULL; unsigned long long written = 0; size_t idx; int fd; for (idx = 0; idx < sizeof(buf); idx++) buf[idx] = (unsigned char)(idx * 31U + 17U); fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600); if (fd < 0) return 1; while (written < total) { size_t want = sizeof(buf); ssize_t rc; if (total - written < want) want = (size_t)(total - written); rc = write(fd, buf, want); if (rc < 0) { if (errno == EINTR) continue; close(fd); return 1; } written += (unsigned long long)rc; } close(fd); return 0; } /* * create_churn_files - create many small files for object-cache churn. * @paths: generated path object. * @count: number of files to create. * Returns 0 on success, 1 on failure. */ static int create_churn_files(struct stress_paths *paths, unsigned int count) { char file_path[PATH_MAX]; unsigned int idx; if (join_path(paths->churn_dir, paths->workdir, "churn")) return 1; if (mkdir(paths->churn_dir, 0700) && errno != EEXIST) return 1; for (idx = 0; idx < count; idx++) { char data[64]; int len; if (snprintf(file_path, sizeof(file_path), "%s/file-%06u.dat", paths->churn_dir, idx) >= (int)sizeof(file_path)) return 1; len = snprintf(data, sizeof(data), "fapolicyd-stress %u\n", idx); if (len < 0 || len >= (int)sizeof(data)) return 1; if (write_file_data(file_path, data, (size_t)len, 0600)) return 1; } return 0; } /* * workload_needs_scripts - decide whether script inputs are needed. * @workload: selected workload. * Returns true when script files are needed. */ static bool workload_needs_scripts(enum workload_type workload) { return workload == WORKLOAD_INTERPRETER || workload == WORKLOAD_NOSHEBANG || workload == WORKLOAD_ALL; } /* * setup_script_inputs - locate committed scripts and create their data file. * @paths: generated path object. * Returns 0 on success, 1 on failure. */ static int setup_script_inputs(struct stress_paths *paths) { if (join_path(paths->small_file, paths->workdir, "script-data.txt")) return 1; if (write_file_data(paths->small_file, "script data\n", 12, 0600)) return 1; if (join_path(paths->shebang_script, STRESS_SCRIPT_DIR, "with-shebang.sh")) return 1; if (join_path(paths->no_shebang_script, STRESS_SCRIPT_DIR, "without-shebang")) return 1; if (access(paths->shebang_script, X_OK) || access(paths->no_shebang_script, X_OK)) return 1; return 0; } /* * setup_paths - create all generated inputs for the selected workload. * @opts: run options. * @paths: generated path object to fill. * Returns 0 on success, 1 on failure. */ static int setup_paths(const struct stress_options *opts, struct stress_paths *paths) { if (make_workdir(opts, paths)) return 1; if (workload_needs_scripts(opts->workload) && setup_script_inputs(paths)) return 1; if (opts->workload == WORKLOAD_HASH || opts->workload == WORKLOAD_ALL) { if (join_path(paths->hash_file, paths->workdir, "large-hash-file.dat")) return 1; if (create_large_file(paths->hash_file, opts->hash_mb)) return 1; } if (opts->workload == WORKLOAD_CHURN || opts->workload == WORKLOAD_ALL) { if (create_churn_files(paths, opts->churn_files)) return 1; } return 0; } /* * remove_churn_files - remove generated churn files. * @paths: generated path object. * @count: number of churn files. * Returns nothing. */ static void remove_churn_files(const struct stress_paths *paths, unsigned int count) { char file_path[PATH_MAX]; unsigned int idx; for (idx = 0; idx < count; idx++) { if (snprintf(file_path, sizeof(file_path), "%s/file-%06u.dat", paths->churn_dir, idx) < (int)sizeof(file_path)) unlink(file_path); } rmdir(paths->churn_dir); } /* * cleanup_paths - remove generated inputs unless retention was requested. * @opts: run options. * @paths: generated path object. * Returns nothing. */ static void cleanup_paths(const struct stress_options *opts, const struct stress_paths *paths) { if (opts->keep_workdir || !paths->created_workdir) return; if (opts->workload == WORKLOAD_CHURN || opts->workload == WORKLOAD_ALL) remove_churn_files(paths, opts->churn_files); if (paths->hash_file[0]) unlink(paths->hash_file); if (paths->small_file[0]) unlink(paths->small_file); rmdir(paths->workdir); } /* * redirect_child_output - send child stdout and stderr to /dev/null. * Returns nothing. */ static void redirect_child_output(void) { int fd = open("/dev/null", O_WRONLY | O_CLOEXEC); if (fd < 0) return; dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); close(fd); } /* * child_exec - execute a target from a forked child. * @path: executable path. * @argv: argument vector. * @enoexec_ok: non-zero treats ENOEXEC as success. * Returns only on exec failure by exiting the child. */ static __noreturn void child_exec(const char *path, char *const argv[], int enoexec_ok) { redirect_child_output(); execvp(path, argv); if (enoexec_ok && errno == ENOEXEC) _exit(0); _exit(errno == EACCES ? 126 : 127); } /* * wait_exec - wait for a forked exec child. * @pid: child pid. * Returns 0 when the child succeeded, 1 otherwise. */ static int wait_exec(pid_t pid) { int status; for (;;) { if (waitpid(pid, &status, 0) >= 0) break; if (errno != EINTR) return 1; } if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) return 1; return 0; } /* * run_exec_argv - fork and execute one command. * @path: executable path. * @argv: argument vector. * @enoexec_ok: non-zero treats ENOEXEC as success. * Returns 0 on success, 1 on failure. */ static int run_exec_argv(const char *path, char *const argv[], int enoexec_ok) { pid_t pid = fork(); if (pid < 0) return 1; if (pid == 0) child_exec(path, argv, enoexec_ok); return wait_exec(pid); } /* * run_simple_exec - fork and execute a single-argument command. * @path: executable path. * @enoexec_ok: non-zero treats ENOEXEC as success. * Returns 0 on success, 1 on failure. */ static int run_simple_exec(const char *path, int enoexec_ok) { char *const argv[] = {(char *)path, NULL}; return run_exec_argv(path, argv, enoexec_ok); } /* * stat_add_exec_result - account for one exec attempt. * @stats: leaf-local counters. * @rc: operation return code. * Returns nothing. */ static void stat_add_exec_result(struct leaf_stats *stats, int rc) { stats->operations++; if (rc) stats->errors++; } /* * do_fork_exec - run one harmless installed command. * @opts: run options. * @stats: leaf-local counters. * @iteration: loop iteration used to rotate targets. * Returns nothing. */ static void do_fork_exec(const struct stress_options *opts, struct leaf_stats *stats, unsigned int iteration) { const char *cmd = opts->commands.paths[iteration % opts->commands.count]; stat_add_exec_result(stats, run_simple_exec(cmd, 0)); } /* * do_exec_open - open all configured command paths and exec one target. * @opts: run options. * @stats: leaf-local counters. * @iteration: loop iteration used to rotate targets. * Returns nothing. */ static void do_exec_open(const struct stress_options *opts, struct leaf_stats *stats, unsigned int iteration) { unsigned int idx; for (idx = 0; idx < opts->commands.count; idx++) { const char *cmd = opts->commands.paths[idx]; int fd; if (strchr(cmd, '/') == NULL) continue; stats->operations++; fd = open(cmd, O_RDONLY | O_CLOEXEC); if (fd < 0) stats->errors++; else close(fd); } do_fork_exec(opts, stats, iteration); } /* * do_interpreter - run a shebang script directly and through the shell. * @opts: run options. * @paths: generated input paths. * @stats: leaf-local counters. * Returns nothing. */ static void do_interpreter(const struct stress_options *opts, const struct stress_paths *paths, struct leaf_stats *stats) { char *const direct_argv[] = { (char *)paths->shebang_script, (char *)paths->small_file, NULL }; char *const shell_argv[] = { (char *)opts->shell_path, (char *)paths->shebang_script, (char *)paths->small_file, NULL }; stat_add_exec_result(stats, run_exec_argv(paths->shebang_script, direct_argv, 0)); stat_add_exec_result(stats, run_exec_argv(opts->shell_path, shell_argv, 0)); } /* * do_noshebang - run a no-shebang script directly and through the shell. * @opts: run options. * @paths: generated input paths. * @stats: leaf-local counters. * Returns nothing. */ static void do_noshebang(const struct stress_options *opts, const struct stress_paths *paths, struct leaf_stats *stats) { char *const direct_argv[] = { (char *)paths->no_shebang_script, (char *)paths->small_file, NULL }; char *const shell_argv[] = { (char *)opts->shell_path, (char *)paths->no_shebang_script, (char *)paths->small_file, NULL }; stat_add_exec_result(stats, run_exec_argv(paths->no_shebang_script, direct_argv, 1)); stat_add_exec_result(stats, run_exec_argv(opts->shell_path, shell_argv, 0)); } /* * do_hash_file - read and hash the generated large file. * @paths: generated input paths. * @stats: leaf-local counters. * Returns nothing. */ static void do_hash_file(const struct stress_paths *paths, struct leaf_stats *stats) { unsigned char buf[HASH_BLOCK_SIZE]; uint64_t hash = 1469598103934665603ULL; int fd; fd = open(paths->hash_file, O_RDONLY | O_CLOEXEC); stats->operations++; if (fd < 0) { stats->errors++; return; } for (;;) { ssize_t len = read(fd, buf, sizeof(buf)); ssize_t pos; if (len < 0) { if (errno == EINTR) continue; stats->errors++; break; } if (len == 0) break; for (pos = 0; pos < len; pos++) { hash ^= buf[pos]; hash *= 1099511628211ULL; } } if (hash == 0) stats->errors++; close(fd); } /* * do_cache_churn - open one generated churn file. * @opts: run options. * @paths: generated input paths. * @stats: leaf-local counters. * @iteration: loop iteration used to rotate files. * Returns nothing. */ static void do_cache_churn(const struct stress_options *opts, const struct stress_paths *paths, struct leaf_stats *stats, unsigned int iteration) { char file_path[PATH_MAX]; char byte; int fd; if (opts->churn_files == 0) return; if (snprintf(file_path, sizeof(file_path), "%s/file-%06u.dat", paths->churn_dir, iteration % opts->churn_files) >= (int)sizeof(file_path)) { stats->errors++; return; } stats->operations++; fd = open(file_path, O_RDONLY | O_CLOEXEC); if (fd < 0) { stats->errors++; return; } if (read(fd, &byte, sizeof(byte)) < 0) stats->errors++; close(fd); } /* * run_one_iteration - run the selected workload once. * @opts: run options. * @paths: generated input paths. * @stats: leaf-local counters. * @iteration: loop iteration. * Returns nothing. */ static void run_one_iteration(const struct stress_options *opts, const struct stress_paths *paths, struct leaf_stats *stats, unsigned int iteration) { switch (opts->workload) { case WORKLOAD_EXEC_OPEN: do_exec_open(opts, stats, iteration); break; case WORKLOAD_FORK_EXEC: do_fork_exec(opts, stats, iteration); break; case WORKLOAD_INTERPRETER: do_interpreter(opts, paths, stats); break; case WORKLOAD_NOSHEBANG: do_noshebang(opts, paths, stats); break; case WORKLOAD_HASH: do_hash_file(paths, stats); break; case WORKLOAD_CHURN: do_cache_churn(opts, paths, stats, iteration); break; case WORKLOAD_ALL: do_exec_open(opts, stats, iteration); do_fork_exec(opts, stats, iteration); do_interpreter(opts, paths, stats); do_noshebang(opts, paths, stats); do_hash_file(paths, stats); do_cache_churn(opts, paths, stats, iteration); break; } } /* * leaf_loop - execute workload iterations in a leaf process. * @opts: run options. * @paths: generated input paths. * @shared: shared run state. * Returns only by exiting the leaf process. */ static __noreturn void leaf_loop(const struct stress_options *opts, const struct stress_paths *paths, struct stress_shared *shared) { struct leaf_stats stats = {0, 0}; unsigned int iteration = 0; while (!shared->stop) { if (opts->iterations && iteration >= opts->iterations) break; run_one_iteration(opts, paths, &stats, iteration); iteration++; if ((iteration & 0x3F) == 0) shared_add(shared, &stats); } shared_add(shared, &stats); _exit(0); } /* * wait_for_children - wait for branch children. * @children: child pid array. * @count: number of pids in the array. * Returns 0 if all children succeeded, 1 otherwise. */ static int wait_for_children(pid_t *children, unsigned int count) { unsigned int idx; int failed = 0; for (idx = 0; idx < count; idx++) { int status = 0; int waited = 0; for (;;) { if (waitpid(children[idx], &status, 0) >= 0) { waited = 1; break; } if (errno != EINTR) { failed = 1; break; } } if (!waited || !WIFEXITED(status) || WEXITSTATUS(status) != 0) failed = 1; } return failed; } /* * spawn_branch - recursively create a process subtree. * @opts: run options. * @paths: generated input paths. * @shared: shared run state. * @depth: remaining depth below this process. * Returns only by exiting the branch process. */ static __noreturn void spawn_branch( const struct stress_options *opts, const struct stress_paths *paths, struct stress_shared *shared, unsigned int depth) { pid_t *children; unsigned int idx; int failed; if (depth == 0) leaf_loop(opts, paths, shared); children = calloc(opts->fanout, sizeof(*children)); if (children == NULL) _exit(1); for (idx = 0; idx < opts->fanout; idx++) { children[idx] = fork(); if (children[idx] < 0) { shared->stop = 1; free(children); _exit(1); } if (children[idx] == 0) spawn_branch(opts, paths, shared, depth - 1); } failed = wait_for_children(children, opts->fanout); free(children); _exit(failed ? 1 : 0); } /* * estimate_leaf_count - calculate expected leaf process count. * @opts: run options. * @out: destination for estimated leaves. * Returns 0 on success, 1 on overflow. */ static int estimate_leaf_count(const struct stress_options *opts, unsigned long long *out) { unsigned long long leaves = opts->roots; unsigned int idx; for (idx = 0; idx < opts->depth; idx++) { if (opts->fanout && leaves > ULLONG_MAX / opts->fanout) return 1; leaves *= opts->fanout; } *out = leaves; return 0; } /* * start_roots - fork root processes for the stress tree. * @opts: run options. * @paths: generated input paths. * @shared: shared run state. * Returns a pid array on success, NULL on failure. */ static pid_t *start_roots(const struct stress_options *opts, const struct stress_paths *paths, struct stress_shared *shared) { pid_t *roots; unsigned int idx; roots = calloc(opts->roots, sizeof(*roots)); if (roots == NULL) return NULL; for (idx = 0; idx < opts->roots; idx++) { roots[idx] = fork(); if (roots[idx] < 0) { shared->stop = 1; return roots; } if (roots[idx] == 0) { setpgid(0, 0); spawn_branch(opts, paths, shared, opts->depth); } setpgid(roots[idx], roots[idx]); } return roots; } /* * roots_done - reap any finished roots. * @roots: pid array. * @count: number of roots. * Returns non-zero when every root has exited. */ static int roots_done(pid_t *roots, unsigned int count) { unsigned int idx; int all_done = 1; for (idx = 0; idx < count; idx++) { int status; if (roots[idx] == 0) continue; if (waitpid(roots[idx], &status, WNOHANG) == roots[idx]) roots[idx] = 0; else if (errno == ECHILD) roots[idx] = 0; else all_done = 0; } return all_done; } /* * wait_roots_until - wait for root processes until a deadline. * @roots: pid array. * @count: number of roots. * @deadline: monotonic nanosecond deadline. * * The shared stop flag tells leaf loops to stop starting new work. Waiting * here gives in-flight exec/open operations time to finish naturally before * the harness has to terminate remaining process groups. * * Returns non-zero when every root has exited. */ static int wait_roots_until(pid_t *roots, unsigned int count, unsigned long long deadline) { while (monotonic_ns() < deadline) { if (roots_done(roots, count)) return 1; usleep(10000); } return roots_done(roots, count); } /* * stop_roots - terminate unfinished root process groups. * @roots: pid array. * @count: number of roots. * @sig: signal to send. * Returns nothing. */ static void stop_roots(pid_t *roots, unsigned int count, int sig) { unsigned int idx; for (idx = 0; idx < count; idx++) { if (roots[idx] > 0) kill(-roots[idx], sig); } } /* * wait_remaining_roots - wait for all remaining root processes. * @roots: pid array. * @count: number of roots. * Returns nothing. */ static void wait_remaining_roots(pid_t *roots, unsigned int count) { unsigned int idx; for (idx = 0; idx < count; idx++) { if (roots[idx] <= 0) continue; while (waitpid(roots[idx], NULL, 0) < 0 && errno == EINTR) ; roots[idx] = 0; } } /* * run_stress_tree - run the configured process tree to completion. * @opts: run options. * @paths: generated input paths. * @shared: shared run state. * Returns 0 when the tree was started, 1 on setup failure. */ static int run_stress_tree(const struct stress_options *opts, const struct stress_paths *paths, struct stress_shared *shared) { unsigned long long start = monotonic_ns(); unsigned long long deadline = 0; pid_t *roots; if (opts->seconds) deadline = start + (unsigned long long)opts->seconds * 1000000000ULL; roots = start_roots(opts, paths, shared); if (roots == NULL) return 1; while (!shared->stop) { if (roots_done(roots, opts->roots)) break; if (deadline && monotonic_ns() >= deadline) { shared->stop = 1; break; } usleep(10000); } if (shared->stop) { if (!wait_roots_until(roots, opts->roots, monotonic_ns() + GRACEFUL_STOP_NS)) { stop_roots(roots, opts->roots, SIGTERM); if (!wait_roots_until(roots, opts->roots, monotonic_ns() + TERM_STOP_NS)) stop_roots(roots, opts->roots, SIGKILL); } } wait_remaining_roots(roots, opts->roots); free(roots); return 0; } /* * capture_free - release captured command output. * @capture: capture object to clear. * Returns nothing. */ static void capture_free(struct capture *capture) { free(capture->data); capture->data = NULL; capture->len = 0; capture->status = 0; } /* * run_capture - run a helper command and capture stdout/stderr. * @path: executable path. * @argv: argument vector. * @capture: captured output destination. * Returns 0 when the command ran and exited successfully, 1 otherwise. */ static int run_capture(const char *path, char *const argv[], struct capture *capture) { char *buf = NULL; size_t len = 0; int pipefd[2]; pid_t pid; int status = 0; memset(capture, 0, sizeof(*capture)); if (pipe(pipefd)) return 1; pid = fork(); if (pid < 0) { close(pipefd[0]); close(pipefd[1]); return 1; } if (pid == 0) { close(pipefd[0]); dup2(pipefd[1], STDOUT_FILENO); dup2(pipefd[1], STDERR_FILENO); close(pipefd[1]); execv(path, argv); _exit(127); } close(pipefd[1]); for (;;) { char tmp[4096]; ssize_t rc = read(pipefd[0], tmp, sizeof(tmp)); if (rc < 0) { if (errno == EINTR) continue; break; } if (rc == 0) break; if (len + (size_t)rc + 1 <= CAPTURE_LIMIT) { char *next = realloc(buf, len + (size_t)rc + 1); if (next == NULL) break; buf = next; memcpy(buf + len, tmp, (size_t)rc); len += (size_t)rc; buf[len] = 0; } } close(pipefd[0]); while (waitpid(pid, &status, 0) < 0 && errno == EINTR) ; if (buf == NULL) { buf = calloc(1, 1); if (buf == NULL) return 1; } capture->data = buf; capture->len = len; capture->status = status; return !WIFEXITED(status) || WEXITSTATUS(status) != 0; } /* * file_executable - test whether a path is executable. * @path: candidate path. * Returns true when it can be executed. */ static bool file_executable(const char *path) { return path && access(path, X_OK) == 0; } /* * find_cli - select the configured fapolicyd-cli for report collection. * @opts: run options. * @dst: destination path buffer. * Returns 0 when the configured path is executable, 1 otherwise. */ static int find_cli(const struct stress_options *opts, char *dst) { const char *path = opts->cli_path ? opts->cli_path : STRESS_CLI_PATH; if (!file_executable(path)) return 1; if (snprintf(dst, PATH_MAX, "%s", path) >= PATH_MAX) return 1; return 0; } /* * parse_human_u64 - parse an unsigned integer allowing commas. * @text: text to parse. * @out: parsed destination. * Returns 0 on success, 1 on parse error. */ static int parse_human_u64(const char *text, unsigned long long *out) { unsigned long long value = 0; int saw_digit = 0; while (*text && isspace((unsigned char)*text)) text++; while (*text) { if (*text == ',') { text++; continue; } if (!isdigit((unsigned char)*text)) break; value = value * 10ULL + (unsigned long long)(*text - '0'); saw_digit = 1; text++; } if (!saw_digit) return 1; *out = value; return 0; } /* * parse_u64_line - parse a numeric "name: value" line. * @data: report text. * @name: metric name with trailing colon. * @out: parsed destination. * Returns 0 on success, 1 when not found or invalid. */ static int parse_u64_line(const char *data, const char *name, unsigned long long *out) { size_t name_len = strlen(name); const char *pos = data; while ((pos = strstr(pos, name)) != NULL) { if ((pos == data || pos[-1] == '\n') && parse_human_u64(pos + name_len, out) == 0) return 0; pos += name_len; } return 1; } /* * parse_word_line - parse the first word after a "name: value" line. * @data: report text. * @name: metric name with trailing colon. * @out: destination buffer. * @out_len: destination buffer size. * Returns 0 on success, 1 when not found or invalid. */ static int parse_word_line(const char *data, const char *name, char *out, size_t out_len) { size_t name_len = strlen(name); const char *pos = data; size_t idx = 0; while ((pos = strstr(pos, name)) != NULL) { if (pos != data && pos[-1] != '\n') { pos += name_len; continue; } pos += name_len; while (*pos && isspace((unsigned char)*pos)) pos++; while (*pos && !isspace((unsigned char)*pos) && idx + 1 < out_len) out[idx++] = *pos++; out[idx] = 0; return idx ? 0 : 1; } return 1; } /* * parse_text_line - parse all text after a "name: value" line. * @data: report text. * @name: metric name with trailing colon. * @out: destination buffer. * @out_len: destination buffer size. * Returns 0 on success, 1 when not found or invalid. */ static int parse_text_line(const char *data, const char *name, char *out, size_t out_len) { size_t name_len = strlen(name); const char *pos = data; size_t idx = 0; while ((pos = strstr(pos, name)) != NULL) { if (pos != data && pos[-1] != '\n') { pos += name_len; continue; } pos += name_len; while (*pos && isspace((unsigned char)*pos)) pos++; while (*pos && *pos != '\n' && idx + 1 < out_len) out[idx++] = *pos++; while (idx > 0 && isspace((unsigned char)out[idx - 1])) idx--; out[idx] = 0; return idx ? 0 : 1; } return 1; } /* * parse_double_line - parse a floating point "name: value" line. * @data: report text. * @name: metric name with trailing colon. * @out: parsed destination. * Returns 0 on success, 1 when not found or invalid. */ static int parse_double_line(const char *data, const char *name, double *out) { size_t name_len = strlen(name); const char *pos = data; char *end = NULL; pos = strstr(data, name); if (pos == NULL) return 1; pos += name_len; while (*pos && isspace((unsigned char)*pos)) pos++; errno = 0; *out = strtod(pos, &end); if (errno || end == pos) return 1; return 0; } /* * trim_space - remove leading and trailing whitespace in a line field. * @text: field text. * Returns the first non-space character in the trimmed field. */ static char *trim_space(char *text) { char *end; while (isspace((unsigned char)*text)) text++; if (*text == 0) return text; end = text + strlen(text) - 1; while (end > text && isspace((unsigned char)*end)) { *end = 0; end--; } return text; } /* * parse_config_assignment - split one fapolicyd.conf assignment. * @line: mutable configuration line. * @name: destination for the trimmed option name. * @value: destination for the trimmed option value. * Returns 0 when an assignment was found, 1 for blank or non-assignment lines. */ static int parse_config_assignment(char *line, char **name, char **value) { char *comment; char *equals; comment = strchr(line, '#'); if (comment) *comment = 0; equals = strchr(line, '='); if (equals == NULL) return 1; *equals = 0; *name = trim_space(line); *value = trim_space(equals + 1); return **name && **value ? 0 : 1; } /* * copy_config_word - copy the first whitespace-delimited config value. * @dst: destination buffer. * @src: source value. * @dst_len: destination buffer size. * Returns nothing. */ static void copy_config_word(char *dst, const char *src, size_t dst_len) { size_t idx = 0; while (*src && !isspace((unsigned char)*src) && idx + 1 < dst_len) dst[idx++] = *src++; dst[idx] = 0; } /* * parse_config_snapshot - extract reset-sensitive fapolicyd.conf settings. * @config: destination config snapshot. * @line: mutable configuration line. * Returns nothing. */ static void parse_config_snapshot(struct daemon_config_snapshot *config, char *line) { unsigned int value; char *name; char *setting; if (parse_config_assignment(line, &name, &setting)) return; if (strcmp(name, "report_interval") == 0) { if (parse_uint(setting, &value) == 0) { config->report_interval = value; config->have_report_interval = 1; } } else if (strcmp(name, "reset_strategy") == 0) { copy_config_word(config->reset_strategy, setting, sizeof(config->reset_strategy)); config->have_reset_strategy = config->reset_strategy[0] != 0; } } /* * read_daemon_config_snapshot - read config settings that affect counters. * @config: destination config snapshot. * @verbose: non-zero prints config read failures. * Returns 0 when the config was read, 1 otherwise. */ static int read_daemon_config_snapshot(struct daemon_config_snapshot *config, int verbose) { char line[8192]; FILE *file; int fd; memset(config, 0, sizeof(*config)); fd = open(CONFIG_FILE, O_RDONLY | O_NOFOLLOW); if (fd < 0) { if (verbose && errno != ENOENT) fprintf(stderr, "cannot read %s: %s\n", CONFIG_FILE, strerror(errno)); return 1; } file = fdopen(fd, "r"); if (file == NULL) { if (verbose) fprintf(stderr, "cannot read %s: %s\n", CONFIG_FILE, strerror(errno)); close(fd); return 1; } while (fgets(line, sizeof(line), file)) parse_config_snapshot(config, line); fclose(file); return 0; } /* * copy_token_until - copy text until a delimiter or line end. * @dst: destination buffer. * @src: source pointer. * @delim: delimiter to stop at. * Returns nothing. */ static void copy_token_until(char *dst, const char *src, char delim) { size_t idx = 0; while (*src && *src != delim && *src != '\n' && idx + 1 < 32) { dst[idx++] = *src++; } dst[idx] = 0; } /* * parse_daemon_metrics - merge daemon report metrics used by this harness. * @data: fapolicyd state and metrics report text. * @metrics: metric snapshot to update. * Returns nothing. */ static void parse_daemon_metrics(const char *data, struct daemon_metrics *metrics) { metrics->present = 1; parse_u64_line(data, "Inter-thread max queue depth:", &metrics->queue_max_depth); parse_u64_line(data, "Subject deferred events:", &metrics->subject_defer_current); parse_u64_line(data, "Subject defer max depth:", &metrics->subject_defer_max_depth); parse_u64_line(data, "Subject defer fallbacks:", &metrics->subject_defer_fallbacks); parse_text_line(data, "Subject defer oldest age:", metrics->subject_defer_oldest_age, sizeof(metrics->subject_defer_oldest_age)); parse_u64_line(data, "Early subject cache evictions:", &metrics->early_subject_evictions); parse_u64_line(data, "Subject BUILDING tracer evictions:", &metrics->subject_tracer_evictions); parse_u64_line(data, "Subject BUILDING stale evictions:", &metrics->subject_stale_evictions); parse_u64_line(data, "Subject collisions:", &metrics->subject_collisions); parse_u64_line(data, "Subject evictions:", &metrics->subject_evictions); parse_u64_line(data, "Object collisions:", &metrics->object_collisions); parse_u64_line(data, "Object evictions:", &metrics->object_evictions); parse_u64_line(data, "Allowed accesses:", &metrics->allowed); parse_u64_line(data, "Denied accesses:", &metrics->denied); parse_u64_line(data, "Kernel queue overflow:", &metrics->kernel_overflow); parse_u64_line(data, "Reply errors:", &metrics->reply_errors); if (parse_word_line(data, "Timing collection mode:", metrics->timing_mode, sizeof(metrics->timing_mode)) == 0) metrics->have_timing_mode = 1; if (parse_word_line(data, "reset_strategy:", metrics->reset_strategy, sizeof(metrics->reset_strategy)) == 0) metrics->have_reset_strategy = 1; } /* * parse_timing_metrics - extract timing metrics used by this harness. * @data: fapolicyd timing report text. * @metrics: metric snapshot to fill. * Returns nothing. */ static void parse_timing_metrics(const char *data, struct timing_metrics *metrics) { const char *pos; memset(metrics, 0, sizeof(*metrics)); metrics->present = 1; parse_u64_line(data, "Max queue depth:", &metrics->max_queue_depth); parse_u64_line(data, "Decisions:", &metrics->decisions); parse_double_line(data, "Throughput:", &metrics->throughput); parse_double_line(data, "Active decision rate:", &metrics->active_rate); pos = strstr(data, "Overall decision latency:"); if (pos) { const char *avg = strstr(pos, " avg "); const char *p95 = strstr(pos, "p95 bucket "); if (avg) { avg += strlen(" avg "); copy_token_until(metrics->avg_latency, avg, ','); pos = strstr(avg, "max "); if (pos) { pos += strlen("max "); copy_token_until(metrics->max_latency, pos, '\n'); } } if (p95) { p95 += strlen("p95 bucket "); copy_token_until(metrics->p95_latency, p95, ','); } } } /* * collect_status - ask fapolicyd-cli for state and metrics and parse them. * @cli_path: fapolicyd-cli executable. * @metrics: destination metrics. * @verbose: non-zero prints failures. * Returns 0 on success, 1 on failure. */ static int collect_status(const char *cli_path, struct daemon_metrics *metrics, int verbose) { struct capture capture; char *const state_argv[] = {(char *)cli_path, "--check-status", NULL}; char *const metrics_argv[] = {(char *)cli_path, "--check-metrics", NULL}; int rc; memset(metrics, 0, sizeof(*metrics)); rc = run_capture(cli_path, state_argv, &capture); if (rc) { if (verbose) fprintf(stderr, "status capture failed: %s\n", capture.data ? capture.data : "no output"); capture_free(&capture); return 1; } parse_daemon_metrics(capture.data, metrics); capture_free(&capture); rc = run_capture(cli_path, metrics_argv, &capture); if (rc) { if (verbose) fprintf(stderr, "metrics capture failed: %s\n", capture.data ? capture.data : "no output"); capture_free(&capture); return 1; } parse_daemon_metrics(capture.data, metrics); capture_free(&capture); return 0; } /* * validate_privileged_options - report options that need elevated privilege. * @opts: run options. * Returns 0 when the run may continue, 1 for a hard privilege error. */ static int validate_privileged_options(const struct stress_options *opts) { if (geteuid() == 0) return 0; if (opts->collect_timing) { fprintf(stderr, "--timing requires root or equivalent privilege\n"); return 1; } if (opts->collect_status) fprintf(stderr, "--status may require root; use --no-status for an " "unprivileged workload-only run\n"); return 0; } /* * ensure_timing_ready - verify active daemon timing configuration. * @cli_path: fapolicyd-cli executable. * @status: existing status snapshot, or NULL. * @verbose: non-zero prints status capture failures. * Returns 0 when manual timing can be requested, 1 otherwise. */ static int ensure_timing_ready(const char *cli_path, const struct daemon_metrics *status, int verbose) { struct daemon_metrics tmp; if (status == NULL || !status->present) { if (collect_status(cli_path, &tmp, verbose)) { fprintf(stderr, "--timing requires an active daemon status " "report to verify timing_collection=manual\n"); return 1; } status = &tmp; } if (!status->have_timing_mode) { fprintf(stderr, "--timing requires a daemon status report with " "Timing collection mode\n"); return 1; } if (strcmp(status->timing_mode, "manual") != 0) { fprintf(stderr, "--timing requires timing_collection=manual in the " "active daemon configuration; current mode is %s\n", status->timing_mode); return 1; } return 0; } /* * warn_interval_reset_hazard - warn when interval reports may reset counters. * @opts: run options. * @status: active daemon status snapshot, or NULL. * Returns nothing. */ static void warn_interval_reset_hazard(const struct stress_options *opts, const struct daemon_metrics *status) { struct daemon_config_snapshot config; const char *reset_strategy = NULL; if (read_daemon_config_snapshot(&config, opts->verbose)) return; if (!config.have_report_interval || config.report_interval == 0) return; if (status && status->have_reset_strategy) reset_strategy = status->reset_strategy; else if (config.have_reset_strategy) reset_strategy = config.reset_strategy; if (reset_strategy == NULL || strcmp(reset_strategy, "auto") != 0) return; fprintf(stderr, "warning: reset_strategy=auto with report_interval=%u can " "reset daemon counters during this run; stress deltas and " "post-run state reports may undercount workload activity\n", config.report_interval); } /* * timing_start - request manual decision timing start. * @cli_path: fapolicyd-cli executable. * @verbose: non-zero prints failures. * Returns 0 on success, 1 on failure. */ static int timing_start(const char *cli_path, int verbose) { struct capture capture; char *const argv[] = {(char *)cli_path, "--timing-start", NULL}; int rc = run_capture(cli_path, argv, &capture); if (rc && verbose) fprintf(stderr, "timing start failed: %s\n", capture.data ? capture.data : "no output"); capture_free(&capture); return rc; } /* * timing_stop - request manual decision timing stop and parse report. * @cli_path: fapolicyd-cli executable. * @metrics: destination timing metrics. * @verbose: non-zero prints failures. * Returns 0 on success, 1 on failure. */ static int timing_stop(const char *cli_path, struct timing_metrics *metrics, int verbose) { struct capture capture; char *const argv[] = {(char *)cli_path, "--timing-stop", NULL}; int rc = run_capture(cli_path, argv, &capture); if (rc) { if (verbose) fprintf(stderr, "timing stop failed: %s\n", capture.data ? capture.data : "no output"); capture_free(&capture); return 1; } parse_timing_metrics(capture.data, metrics); capture_free(&capture); return 0; } /* * metric_delta - calculate an unsigned monotonic metric delta. * @after: value after the run. * @before: value before the run. * Returns after-before when monotonic, otherwise after. */ static unsigned long long metric_delta(unsigned long long after, unsigned long long before) { return after >= before ? after - before : after; } /* * print_metric_delta - print one before/after metric line. * @name: printable metric name. * @before: value before the run. * @after: value after the run. * Returns nothing. */ static void print_metric_delta(const char *name, unsigned long long before, unsigned long long after) { printf("%s: before=%llu after=%llu delta=%llu\n", name, before, after, metric_delta(after, before)); } /* * print_status_summary - print daemon status metrics for the run. * @before: metrics before the run. * @after: metrics after the run. * Returns nothing. */ static void print_status_summary(const struct daemon_metrics *before, const struct daemon_metrics *after) { if (!before->present || !after->present) { printf("Daemon status: not observed\n"); return; } printf("\nDaemon status deltas:\n"); printf("Inter-thread max queue depth: before=%llu after=%llu\n", before->queue_max_depth, after->queue_max_depth); printf("Subject deferred events: before=%llu after=%llu\n", before->subject_defer_current, after->subject_defer_current); printf("Subject defer max depth: before=%llu after=%llu\n", before->subject_defer_max_depth, after->subject_defer_max_depth); print_metric_delta("Subject defer fallbacks", before->subject_defer_fallbacks, after->subject_defer_fallbacks); printf("Subject defer oldest age: before=%s after=%s\n", before->subject_defer_oldest_age[0] ? before->subject_defer_oldest_age : "0 ns", after->subject_defer_oldest_age[0] ? after->subject_defer_oldest_age : "0 ns"); print_metric_delta("Early subject cache evictions", before->early_subject_evictions, after->early_subject_evictions); print_metric_delta("Subject BUILDING tracer evictions", before->subject_tracer_evictions, after->subject_tracer_evictions); print_metric_delta("Subject BUILDING stale evictions", before->subject_stale_evictions, after->subject_stale_evictions); print_metric_delta("Subject collisions", before->subject_collisions, after->subject_collisions); print_metric_delta("Subject evictions", before->subject_evictions, after->subject_evictions); print_metric_delta("Object collisions", before->object_collisions, after->object_collisions); print_metric_delta("Object evictions", before->object_evictions, after->object_evictions); print_metric_delta("Allowed accesses", before->allowed, after->allowed); print_metric_delta("Denied accesses", before->denied, after->denied); print_metric_delta("Kernel queue overflows", before->kernel_overflow, after->kernel_overflow); print_metric_delta("Reply errors", before->reply_errors, after->reply_errors); } /* * print_timing_summary - print parsed decision timing metrics. * @timing: timing metrics to print. * Returns nothing. */ static void print_timing_summary(const struct timing_metrics *timing) { if (!timing->present) { printf("\nDecision timing: not observed\n"); return; } printf("\nDecision timing:\n"); printf("Full report: %s\n", TIMING_REPORT); printf("Decisions: %llu\n", timing->decisions); printf("Max queue depth during timing: %llu\n", timing->max_queue_depth); if (timing->throughput) printf("Timed throughput: %.1f decisions/sec\n", timing->throughput); if (timing->active_rate) printf("Active decision rate: %.1f decisions/sec\n", timing->active_rate); if (timing->avg_latency[0] || timing->max_latency[0] || timing->p95_latency[0]) printf("Decision latency: avg=%s max=%s p95_bucket=%s\n", timing->avg_latency[0] ? timing->avg_latency : "n/a", timing->max_latency[0] ? timing->max_latency : "n/a", timing->p95_latency[0] ? timing->p95_latency : "n/a"); } /* * print_run_header - print selected workload configuration. * @opts: run options. * @leaves: estimated leaf process count. * @paths: generated path object. * Returns nothing. */ static void print_run_header(const struct stress_options *opts, unsigned long long leaves, const struct stress_paths *paths) { printf("fapolicyd stress harness\n"); printf("workload: %s\n", workload_name(opts->workload)); printf("roots: %u\n", opts->roots); printf("fanout: %u\n", opts->fanout); printf("depth: %u\n", opts->depth); printf("estimated leaf processes: %llu\n", leaves); printf("iterations per leaf: %u\n", opts->iterations); printf("seconds: %u\n", opts->seconds); printf("workdir: %s\n", paths->workdir); } /* * print_run_summary - print local workload throughput. * @shared: shared run counters. * @elapsed_ns: elapsed wall-clock nanoseconds. * Returns nothing. */ static void print_run_summary(const struct stress_shared *shared, unsigned long long elapsed_ns) { double seconds = elapsed_ns ? (double)elapsed_ns / 1000000000.0 : 0.0; double throughput = seconds ? (double)shared->operations / seconds : 0.0; printf("\nWorkload summary:\n"); printf("wall_seconds: %.3f\n", seconds); printf("operations: %llu\n", shared->operations); printf("errors: %llu\n", shared->errors); printf("throughput_ops_per_sec: %.1f\n", throughput); } /* * main - program entry point. * @argc: argument count. * @argv: argument vector. * Returns 0 when the run completed without workload errors, non-zero on * setup or workload failure. */ int main(int argc, char **argv) { struct stress_options opts; struct stress_paths paths; struct stress_shared *shared; struct daemon_metrics before_status; struct daemon_metrics after_status; struct timing_metrics timing; unsigned long long leaves = 0; unsigned long long start_ns; unsigned long long end_ns; char cli_path[PATH_MAX] = ""; const char *expected_cli; int cli_available = 0; int arg_rc; int rc = 0; set_defaults(&opts); arg_rc = parse_args(&opts, argc, argv); if (arg_rc == 2) return 0; if (arg_rc) { usage(argv[0]); return 2; } if (validate_privileged_options(&opts)) return 2; if (opts.commands.count == 0 && add_default_commands(&opts.commands)) { fprintf(stderr, "no executable command targets found\n"); return 1; } if (find_shell(&opts)) { fprintf(stderr, "no executable shell found\n"); return 1; } if (estimate_leaf_count(&opts, &leaves)) { fprintf(stderr, "process tree size overflow\n"); return 1; } if (setup_paths(&opts, &paths)) { fprintf(stderr, "failed to create stress inputs: %s\n", strerror(errno)); cleanup_paths(&opts, &paths); return 1; } shared = mmap(NULL, sizeof(*shared), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if (shared == MAP_FAILED) { fprintf(stderr, "failed to create shared counters: %s\n", strerror(errno)); cleanup_paths(&opts, &paths); return 1; } memset(shared, 0, sizeof(*shared)); memset(&before_status, 0, sizeof(before_status)); memset(&after_status, 0, sizeof(after_status)); memset(&timing, 0, sizeof(timing)); expected_cli = opts.cli_path ? opts.cli_path : STRESS_CLI_PATH; cli_available = find_cli(&opts, cli_path) == 0; if (opts.collect_timing && !cli_available) { fprintf(stderr, "--timing requires fapolicyd-cli at expected path %s\n", expected_cli); rc = 1; goto out; } if (opts.collect_status && cli_available) collect_status(cli_path, &before_status, opts.verbose); warn_interval_reset_hazard(&opts, &before_status); if (opts.collect_timing && ensure_timing_ready(cli_path, &before_status, opts.verbose)) { rc = 1; goto out; } if (opts.collect_timing && timing_start(cli_path, opts.verbose)) { fprintf(stderr, "failed to start daemon decision timing\n"); rc = 1; goto out; } else if (opts.collect_status && !cli_available) fprintf(stderr, "fapolicyd-cli is not available at expected path %s; " "daemon metrics disabled\n", expected_cli); if (install_signal_handlers(shared)) { fprintf(stderr, "failed to install signal handlers\n"); rc = 1; goto out; } print_run_header(&opts, leaves, &paths); start_ns = monotonic_ns(); if (run_stress_tree(&opts, &paths, shared)) { fprintf(stderr, "failed to start stress tree\n"); rc = 1; } end_ns = monotonic_ns(); if (opts.collect_timing && cli_available) timing_stop(cli_path, &timing, opts.verbose); if (opts.collect_status && cli_available) collect_status(cli_path, &after_status, opts.verbose); print_run_summary(shared, end_ns - start_ns); if (opts.collect_status) print_status_summary(&before_status, &after_status); if (opts.collect_timing) print_timing_summary(&timing); if (shared->errors) rc = 1; out: munmap(shared, sizeof(*shared)); cleanup_paths(&opts, &paths); return rc; } fapolicyd-2.0/src/tests/stress/scripts/000077500000000000000000000000001524071644000202735ustar00rootroot00000000000000fapolicyd-2.0/src/tests/stress/scripts/with-shebang.sh000077500000000000000000000002031524071644000232050ustar00rootroot00000000000000#!/bin/sh data=${1:?missing data path} i=0 while [ "$i" -lt 4 ]; do cat "$data" >/dev/null || exit 1 i=$((i + 1)) done exit 0 fapolicyd-2.0/src/tests/stress/scripts/without-shebang000077500000000000000000000001611524071644000233270ustar00rootroot00000000000000data=${1:?missing data path} i=0 while [ "$i" -lt 4 ]; do test -r "$data" || exit 1 i=$((i + 1)) done exit 0 fapolicyd-2.0/src/tests/test-stubs.c000066400000000000000000000011011524071644000175330ustar00rootroot00000000000000/* * test-stubs.c - provide globals needed by libfapolicyd for unit tests * * This file supplies minimal definitions of globals referenced by the * library so that standalone tests can link without pulling in the daemon * or CLI entry points. */ #include #include "conf.h" atomic_bool stop; atomic_bool run_stats; atomic_uint signal_report_requests; atomic_uint signal_report_intent; atomic_uint signal_report_reset_requests; atomic_int signal_report_reset_request_pid; atomic_int signal_report_reset_request_uid; unsigned int debug_mode; conf_t config; fapolicyd-2.0/src/tests/trustdb_format_test.c000066400000000000000000000024161524071644000215260ustar00rootroot00000000000000// Copyright 2024 Red Hat // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include "fapolicyd-backend.h" int main(void) { const char *digest = "68879112e7d8a66c61178c409b07d1233270bcf2375d2ea029ca68f3552846563426b625f946c478c37b910373c44a0b89c08b9897885e9b135b11a6db604550"; char data[TRUSTDB_DATA_BUFSZ]; char parsed_digest[FILE_DIGEST_STRING_MAX]; unsigned int tsource; trustdb_size_t size; trustdb_size_t expected_size = UINT64_C(4294967296); off_t native_size; int written; written = snprintf(data, sizeof(data), DATA_FORMAT, SRC_RPM, expected_size, digest); if (written < 0 || written >= (int)sizeof(data)) return 1; if (sscanf(data, DATA_FORMAT_IN, &tsource, &size, parsed_digest) != 3) return 1; if (size != expected_size || strcmp(digest, parsed_digest)) return 1; /* Large-file off_t builds must retain a size above 32-bit range. */ if (sizeof(off_t) >= sizeof(expected_size)) { if (trustdb_size_to_off_t(size, &native_size) || (trustdb_size_t)native_size != size) return 1; } else if (trustdb_size_to_off_t(size, &native_size) == 0) return 1; if (trustdb_size_from_signed(-1, &size) == 0 || trustdb_size_to_off_t(UINT64_MAX, &native_size) == 0) return 1; return 0; } fapolicyd-2.0/src/tests/trustdb_lmdb_test.c000066400000000000000000001730221524071644000211560ustar00rootroot00000000000000// Copyright 2026 Red Hat // SPDX-License-Identifier: GPL-2.0-or-later #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include "database.h" #include "database-internal.h" #include "decision-config.h" #include "event.h" #include "failure-action.h" #include "fapolicyd-backend.h" #define CONCURRENT_READERS 32 #define CONCURRENT_ITERATIONS 64 #define RELOAD_READERS 8 #define CHECK(cond, code, msg) \ do { \ if (!(cond)) \ return check_failed(__func__, __LINE__, #cond, \ code, msg); \ } while (0) struct concurrent_lookup { const char *path; atomic_bool *start; int failed; }; struct reload_lookup { const char *path; atomic_bool *start; atomic_bool *stop; int failed; }; struct incremental_update { const char *path; const char *hash; trustdb_size_t size; int rc; }; struct utilization_report { conf_t *config; FILE *output; pthread_barrier_t *start; }; /* * check_failed - report enough context to diagnose hidden Automake logs. * @func: Test function containing the failed check. * @line: Source line of the failed check. * @expr: Failed expression text. * @code: Exit code returned by the check. * @msg: Human-readable failure message. * * Returns @code after writing failure context to stderr. */ static int check_failed(const char *func, int line, const char *expr, int code, const char *msg) { int saved_errno = errno; fprintf(stderr, "%s\n", msg); fprintf(stderr, " test: %s\n", func); fprintf(stderr, " check: %s:%d: %s\n", __FILE__, line, expr); if (saved_errno) fprintf(stderr, " last errno: %d (%s)\n", saved_errno, strerror(saved_errno)); return code; } static int remove_lmdb_files(const char *dir) { char path[512]; int rc; snprintf(path, sizeof(path), "%s/data.mdb", dir); rc = unlink(path); if (rc == -1 && errno != ENOENT) fprintf(stderr, "failed to remove %s: %s\n", path, strerror(errno)); snprintf(path, sizeof(path), "%s/lock.mdb", dir); rc = unlink(path); if (rc == -1 && errno != ENOENT) fprintf(stderr, "failed to remove %s: %s\n", path, strerror(errno)); rc = rmdir(dir); if (rc == -1) fprintf(stderr, "failed to remove %s: %s\n", dir, strerror(errno)); return rc; } /* * remove_lmdb_lock_file - remove only the LMDB lock file. * @dir: LMDB environment directory. * * Returns 0 on success or when lock.mdb is already absent, and 1 on error. */ static int remove_lmdb_lock_file(const char *dir) { char path[512]; snprintf(path, sizeof(path), "%s/lock.mdb", dir); if (unlink(path) == -1 && errno != ENOENT) { fprintf(stderr, "failed to remove %s: %s\n", path, strerror(errno)); return 1; } return 0; } /* * lmdb_lock_file_mode - read lock.mdb's permission bits. * @dir: LMDB environment directory. * @mode: receives the low permission bits from st_mode. * * Returns 0 on success and 1 on error. */ static int lmdb_lock_file_mode(const char *dir, mode_t *mode) { char path[512]; struct stat sb; snprintf(path, sizeof(path), "%s/lock.mdb", dir); if (stat(path, &sb)) { fprintf(stderr, "failed to stat %s: %s\n", path, strerror(errno)); return 1; } *mode = sb.st_mode & 0777; return 0; } /* * make_memfd_input - Build an in-memory backend snapshot payload. * @text: Newline-delimited trust records. * * Returns an fd positioned at offset 0 on success, or -1 on failure. */ static int make_memfd_input(const char *text) { int fd; size_t len = strlen(text); fd = memfd_create("trustdb-test", 0); if (fd == -1) return -1; if (write(fd, text, len) != (ssize_t)len) { close(fd); return -1; } if (lseek(fd, 0, SEEK_SET) == -1) { close(fd); return -1; } return fd; } /* * build_long_path - Produce a deterministic long path string. * @suffix: Distinguishing suffix appended after a shared long prefix. * * Returns newly allocated path string on success, NULL on allocation error. */ static char *build_long_path(const char *suffix) { size_t prefix_len = 700; size_t suffix_len = strlen(suffix); size_t len = 5 + prefix_len + 1 + suffix_len; char *path = malloc(len + 1); if (path == NULL) return NULL; memcpy(path, "/tmp/", 5); memset(path + 5, 'p', prefix_len); path[5 + prefix_len] = '/'; memcpy(path + 5 + prefix_len + 1, suffix, suffix_len); path[len] = '\0'; return path; } static int import_records(const char *payload, long *entries) { int fd = make_memfd_input(payload); int rc; if (fd == -1) return 1; rc = do_memfd_update(fd, entries); close(fd); return rc; } static int publish_records(conf_t *cfg, const char *payload) { int fd = make_memfd_input(payload); int rc; if (fd == -1) return 1; rc = database_publish_memfd_for_tests(fd, cfg); close(fd); return rc; } static int publish_startup_records(conf_t *cfg, const char *payload) { int fd = make_memfd_input(payload); int rc; if (fd == -1) return 1; rc = database_publish_startup_memfd_for_tests(fd, cfg); close(fd); return rc; } static int drop_candidate_records(const char *payload) { int fd = make_memfd_input(payload); int rc; if (fd == -1) return 1; rc = database_drop_candidate_after_import_for_tests(fd); close(fd); return rc; } /* * write_all - write a complete test payload to an open descriptor. * @fd: descriptor to write. * @contents: NUL-terminated test payload. * * Returns 0 after writing all bytes, or 1 on write failure. */ static int write_all(int fd, const char *contents) { size_t len = strlen(contents); size_t done = 0; while (done < len) { ssize_t written = write(fd, contents + done, len - done); if (written == -1) { if (errno == EINTR) continue; return 1; } if (written == 0) return 1; done += written; } return 0; } /* * write_contents - replace the contents of an open test file. * @fd: descriptor to rewrite and rewind. * @contents: NUL-terminated test payload. * * Returns 0 on success, or 1 on truncate, seek, or write failure. */ static int write_contents(int fd, const char *contents) { if (ftruncate(fd, 0) == -1) return 1; if (lseek(fd, 0, SEEK_SET) == -1) return 1; if (write_all(fd, contents)) return 1; if (lseek(fd, 0, SEEK_SET) == -1) return 1; return 0; } /* * create_test_file - create a temporary file with known contents. * @path: caller buffer receiving the created path. * @path_size: size of @path. * @contents: NUL-terminated test payload. * * Returns an open descriptor on success, or -1 on failure. */ static int create_test_file(char *path, size_t path_size, const char *contents) { char template[] = "/tmp/fapolicyd-trustdb-file-XXXXXX"; int fd; int saved_errno; fd = mkstemp(template); if (fd == -1) return -1; if (strlen(template) + 1 > path_size) { saved_errno = ENAMETOOLONG; close(fd); unlink(template); errno = saved_errno; return -1; } if (write_contents(fd, contents)) { saved_errno = errno; close(fd); unlink(template); errno = saved_errno; return -1; } strcpy(path, template); return fd; } /* * replace_test_file - atomically replace a path while old fds stay open. * @path: existing path to replace with a new inode. * @contents: NUL-terminated replacement payload. * * Returns 0 on success, or 1 on failure. */ static int replace_test_file(const char *path, const char *contents) { char template[] = "/tmp/fapolicyd-trustdb-replacement-XXXXXX"; int fd; int saved_errno; fd = mkstemp(template); if (fd == -1) return 1; if (write_contents(fd, contents)) goto fail; if (close(fd) == -1) { fd = -1; goto fail; } fd = -1; if (rename(template, path) == -1) goto fail; return 0; fail: saved_errno = errno; if (fd != -1) close(fd); unlink(template); errno = saved_errno; return 1; } /* * describe_test_file_hash - collect file metadata and selected digest. * @fd: descriptor to describe. * @info: receives allocated file metadata. * @hash: receives allocated digest text. * @alg: digest algorithm to calculate. * * Returns 0 on success, or 1 on stat/hash failure. */ static int describe_test_file_hash(int fd, struct file_info **info, char **hash, file_hash_alg_t alg) { *info = stat_file_entry(fd); if (*info == NULL) return 1; *hash = get_hash_from_fd2(fd, (*info)->size, alg); if (*hash == NULL) { free(*info); *info = NULL; return 1; } return 0; } /* * describe_test_file - collect file metadata and SHA256 digest for trust. * @fd: descriptor to describe. * @info: receives allocated file metadata. * @hash: receives allocated SHA256 digest text. * * Returns 0 on success, or 1 on stat/hash failure. */ static int describe_test_file(int fd, struct file_info **info, char **hash) { return describe_test_file_hash(fd, info, hash, FILE_HASH_ALG_SHA256); } /* * read_database_metrics_report - capture trust DB metrics text. * @buf: output buffer for report text. * @size: size of @buf. * @reset: non-zero resets metrics after snapshotting. * * Returns 0 on success and 1 on failure. */ static int read_database_metrics_report(char *buf, size_t size, int reset) { FILE *report; size_t used; report = tmpfile(); if (report == NULL) return 1; database_metrics_report_reset(report, reset); fflush(report); rewind(report); used = fread(buf, 1, size - 1, report); fclose(report); buf[used] = '\0'; return 0; } /* * read_database_utilization_text - capture trust DB status text. * @cfg: configuration passed to the utilization report. * @buf: output buffer for report text. * @size: size of @buf. * * Returns 0 on success and 1 on failure. */ static int read_database_utilization_text(conf_t *cfg, char *buf, size_t size) { FILE *report; size_t used; report = tmpfile(); if (report == NULL) return 1; database_utilization_report(report, cfg); fflush(report); rewind(report); used = fread(buf, 1, size - 1, report); fclose(report); buf[used] = '\0'; return 0; } /* * concurrent_lookup_worker - repeatedly read one trust DB key. * @arg: struct concurrent_lookup pointer. * * Returns NULL after recording any lookup failure in @arg. */ static void *concurrent_lookup_worker(void *arg) { struct concurrent_lookup *lookup = arg; while (!atomic_load_explicit(lookup->start, memory_order_acquire)) ; for (unsigned int i = 0; i < CONCURRENT_ITERATIONS; i++) { if (check_trust_database(lookup->path, NULL, -1) != 1) { lookup->failed = 1; break; } } return NULL; } /* * reload_lookup_worker - read one key until a publish storm finishes. * @arg: struct reload_lookup pointer. * * Returns NULL after recording any lookup failure in @arg. */ static void *reload_lookup_worker(void *arg) { struct reload_lookup *lookup = arg; while (!atomic_load_explicit(lookup->start, memory_order_acquire)) ; while (!atomic_load_explicit(lookup->stop, memory_order_acquire)) { if (check_trust_database(lookup->path, NULL, -1) != 1) { lookup->failed = 1; break; } } return NULL; } /* * incremental_update_worker - store one package-manager FIFO record. * @arg: struct incremental_update describing the record and result. * * Returns NULL after database_store_update_record() completes. */ static void *incremental_update_worker(void *arg) { struct incremental_update *update = arg; update->rc = database_store_update_record(update->path, update->size, update->hash); return NULL; } /* * utilization_report_worker - collect sizing data on a worker thread. * @arg: struct utilization_report containing output and start barrier. * * Returns NULL after the report completes. */ static void *utilization_report_worker(void *arg) { struct utilization_report *report = arg; pthread_barrier_wait(report->start); database_utilization_report(report->output, report->config); return NULL; } /* * timed_join - join a test worker before a short real-time deadline. * @thread: worker to join. * @seconds: number of seconds allowed for completion. * * Returns pthread_timedjoin_np()'s status. */ static int timed_join(pthread_t thread, time_t seconds) { struct timespec deadline; if (clock_gettime(CLOCK_REALTIME, &deadline)) return errno; deadline.tv_sec += seconds; return pthread_timedjoin_np(thread, NULL, &deadline); } static int with_temp_db(char *tmpdir, size_t tmpdir_sz, conf_t *cfg) { char template[] = "/tmp/fapolicyd-lmdb-XXXXXX"; char *dir = mkdtemp(template); int rc; if (dir == NULL) { fprintf(stderr, "mkdtemp failed for %s: %s\n", template, strerror(errno)); return 1; } if (strlen(dir) + 1 > tmpdir_sz) { fprintf(stderr, "temporary LMDB path too long: %s\n", dir); return 1; } strcpy(tmpdir, dir); memset(cfg, 0, sizeof(*cfg)); cfg->db_max_size = 16; cfg->integrity = IN_NONE; /* * Some subtests intentionally start CONCURRENT_READERS lookup threads. * Size the LMDB reader table for that pressure so success does not * depend on whether the scheduler overlaps more than the default * one-worker reader budget. */ cfg->decision_threads = CONCURRENT_READERS; rc = database_set_location(tmpdir, NULL); if (rc) { fprintf(stderr, "database_set_location(%s) failed: %d\n", tmpdir, rc); return 1; } rc = database_open_for_tests(cfg); if (rc) fprintf(stderr, "database_open_for_tests(%s) failed: %d\n", tmpdir, rc); return rc; } static int test_data_format_round_trip(void) { const char *digest = "68879112e7d8a66c61178c409b07d1233270bcf2375d2ea029ca68f355284656" "3426b625f946c478c37b910373c44a0b89c08b9897885e9b135b11a6db604550"; char data[TRUSTDB_DATA_BUFSZ]; char parsed_digest[FILE_DIGEST_STRING_MAX]; unsigned int tsource; trustdb_size_t size; int written; written = snprintf(data, sizeof(data), DATA_FORMAT, SRC_RPM, (trustdb_size_t)9400, digest); CHECK(written >= 0 && written < (int)sizeof(data), 10, "[ERROR:10] DATA_FORMAT output truncated"); CHECK(sscanf(data, DATA_FORMAT_IN, &tsource, &size, parsed_digest) == 3, 11, "[ERROR:11] DATA_FORMAT_IN parse failed"); CHECK(strcmp(digest, parsed_digest) == 0, 12, "[ERROR:12] digest mismatch after round trip"); return 0; } /* * test_lmdb_large_size_import - verify the stored size is not tied to size_t. * * A 32-bit build with large-file support has a 64-bit off_t but a 32-bit * size_t. Import must retain the 4 GiB value on that ABI. A genuinely * narrow off_t cannot compare such a record, so the parser must reject it. * * Returns 0 on success or a test-specific error code. */ static int test_lmdb_large_size_import(void) { conf_t cfg; char dir[128]; char payload[256]; long entries = 0; int rc; const char *path = "/usr/bin/large-size"; const char *digest = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 13, "[ERROR:13] failed to open large-size LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path, SRC_FILE_DB, UINT64_C(4294967296), digest); rc = import_records(payload, &entries); if (sizeof(off_t) >= sizeof(trustdb_size_t)) { CHECK(rc == 0 && entries == 1, 14, "[ERROR:14] large-size record import failed"); CHECK(check_trust_database(path, NULL, -1) == 1, 15, "[ERROR:15] large-size record lookup failed"); } else { CHECK(rc != 0, 16, "[ERROR:16] narrow off_t accepted large-size record"); } database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 17, "[ERROR:17] large-size LMDB cleanup failed"); return 0; } static int test_lmdb_short_path_round_trip(void) { conf_t cfg; char dir[128]; long entries = 0; int rc; const char *path = "/usr/bin/short-path"; const char *digest = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; char payload[256]; walkdb_entry_t *entry; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 20, "[ERROR:20] failed to open temporary LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path, SRC_FILE_DB, (trustdb_size_t)1234, digest); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 21, "[ERROR:21] short-path record import failed"); CHECK(check_trust_database(path, NULL, -1) == 1, 22, "[ERROR:22] short-path lookup failed"); database_close_for_tests(); CHECK(walk_database_start(&cfg) == WALK_DATABASE_SUCCESS, 24, "[ERROR:24] short-path walker failed"); entry = walk_database_get_entry(); CHECK(entry != NULL && entry->path_is_hashed == 0, 25, "[ERROR:25] direct path was classified as hashed"); CHECK(entry->path.mv_size == strlen(path), 26, "[ERROR:26] direct path key length changed"); CHECK(memcmp(entry->path.mv_data, path, entry->path.mv_size) == 0, 27, "[ERROR:27] direct path key changed"); walk_database_finish(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 23, "[ERROR:23] short-path cleanup failed"); return 0; } static int test_lmdb_long_path_round_trip(void) { conf_t cfg; char dir[128]; long entries = 0; int rc; char payload[2048]; char *path = build_long_path("long-round-trip"); const char *digest = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; CHECK(path != NULL, 30, "[ERROR:30] failed to allocate long path"); rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 31, "[ERROR:31] failed to open temporary LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path, SRC_FILE_DB, (trustdb_size_t)2048, digest); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 32, "[ERROR:32] long-path record import failed"); CHECK(check_trust_database(path, NULL, -1) == 1, 33, "[ERROR:33] long-path lookup failed"); free(path); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 34, "[ERROR:34] long-path cleanup failed"); return 0; } static int test_lmdb_long_path_shared_prefix_no_collision(void) { conf_t cfg; char dir[128]; long entries = 0; int rc, walk_rc; int count = 1; char *path_a = build_long_path("shared-suffix-a"); char *path_b = build_long_path("shared-suffix-b"); char payload[4096]; walkdb_entry_t *entry; const char *digest_a = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; const char *digest_b = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; CHECK(path_a && path_b, 40, "[ERROR:40] failed to allocate shared-prefix long paths"); rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 41, "[ERROR:41] failed to open temporary LMDB"); /* * Security regression guard: these paths intentionally share a very long * prefix and diverge only after LMDB key size limits. If lookup-side code * hashes a truncated prefix instead of the full path length, one of these * lookups will fail or collide. */ snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n" "%s " DATA_FORMAT "\n", path_a, SRC_FILE_DB, (trustdb_size_t)3000, digest_a, path_b, SRC_FILE_DB, (trustdb_size_t)3001, digest_b); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 2, 42, "[ERROR:42] shared-prefix record import failed"); CHECK(check_trust_database(path_a, NULL, -1) == 1, 43, "[ERROR:43] long-path A lookup failed"); CHECK(check_trust_database(path_b, NULL, -1) == 1, 44, "[ERROR:44] long-path B lookup failed"); database_close_for_tests(); CHECK(walk_database_start(&cfg) == 0, 45, "[ERROR:45] walk_database_start failed"); CHECK(database_walk_reader_slots_for_tests() > 0, 49, "[ERROR:49] walker did not register an LMDB reader"); entry = walk_database_get_entry(); CHECK(entry != NULL && entry->path_is_hashed, 46, "[ERROR:46] long path key was not classified as hashed"); while ((walk_rc = walk_database_next()) == WALK_DATABASE_NEXT) { entry = walk_database_get_entry(); CHECK(entry->path_is_hashed, 50, "[ERROR:50] long path key was not classified as hashed"); count++; } CHECK(walk_rc == WALK_DATABASE_DONE, 51, "[ERROR:51] walker ended with an LMDB error"); walk_database_finish(); CHECK(count == 2, 47, "[ERROR:47] walker did not see two entries"); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 48, "[ERROR:48] shared-prefix cleanup failed"); free(path_a); free(path_b); return 0; } /* * test_lmdb_readonly_probe_does_not_break_live_env - Guard LMDB mutexes. * * Returns 0 when a read-only probe environment can be opened and closed * without breaking the existing writable trust database handle. */ static int test_lmdb_readonly_probe_does_not_break_live_env(void) { conf_t cfg; char dir[128]; long entries = 0; int rc; MDB_env *probe = NULL; const char *digest_a = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; const char *digest_b = "1111111111111111111111111111111111111111111111111111111111111111"; char payload[512]; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 60, "[ERROR:60] failed to open temporary LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", "/usr/bin/probe-a", SRC_FILE_DB, (trustdb_size_t)123, digest_a); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 61, "[ERROR:61] initial record import failed"); rc = mdb_env_create(&probe); CHECK(rc == 0, 62, "[ERROR:62] failed to create probe environment"); rc = mdb_env_set_maxdbs(probe, 2); CHECK(rc == 0, 63, "[ERROR:63] failed to size probe environment"); rc = mdb_env_open(probe, dir, MDB_RDONLY|MDB_NOLOCK, 0); CHECK(rc == 0, 64, "[ERROR:64] failed to open probe environment"); mdb_env_close(probe); probe = NULL; entries = 0; snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", "/usr/bin/probe-b", SRC_FILE_DB, (trustdb_size_t)456, digest_b); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 65, "[ERROR:65] live environment import failed after probe close"); CHECK(check_trust_database("/usr/bin/probe-b", NULL, -1) == 1, 66, "[ERROR:66] post-probe lookup failed"); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 67, "[ERROR:67] probe cleanup failed"); return 0; } /* * test_lmdb_readonly_lookup_keeps_dbi_valid - Guard CLI read-only lookups. * * Returns 0 when the DBI opened during read-only lookup setup remains usable * by later lookup transactions. */ static int test_lmdb_readonly_lookup_keeps_dbi_valid(void) { conf_t cfg; char dir[128]; long entries = 0; int rc; const char *trusted_path = "/usr/bin/readonly-dbi"; const char *digest = "abababababababababababababababababababababababababababababababab"; char payload[512]; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 250, "[ERROR:250] failed to open temporary LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", trusted_path, SRC_FILE_DB, (trustdb_size_t)789, digest); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 251, "[ERROR:251] readonly lookup record import failed"); database_close_for_tests(); rc = database_readonly_lookup_start(); CHECK(rc == 0, 252, "[ERROR:252] readonly lookup start failed"); CHECK(check_trust_database_readonly(trusted_path, NULL, -1) == 1, 253, "[ERROR:253] readonly lookup could not read trusted path"); database_readonly_lookup_finish(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 254, "[ERROR:254] readonly lookup cleanup failed"); return 0; } /* * test_lmdb_readonly_helpers_create_lock_with_trust_mode - Guard lock mode. * * Returns 0 when read-only CLI helpers recreate a missing lock.mdb with the * normal trust DB permissions. */ static int test_lmdb_readonly_helpers_create_lock_with_trust_mode(void) { conf_t cfg; char dir[128]; long entries = 0; int rc; mode_t mode; mode_t old_umask; const char *trusted_path = "/usr/bin/readonly-lock-mode"; const char *digest = "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd"; char payload[512]; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 255, "[ERROR:255] failed to open temporary LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", trusted_path, SRC_FILE_DB, (trustdb_size_t)321, digest); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 256, "[ERROR:256] readonly lock-mode import failed"); database_close_for_tests(); CHECK(remove_lmdb_lock_file(dir) == 0, 257, "[ERROR:257] failed to remove lock before readonly lookup"); old_umask = umask(0); rc = database_readonly_lookup_start(); umask(old_umask); CHECK(rc == 0, 258, "[ERROR:258] readonly lookup start failed"); database_readonly_lookup_finish(); CHECK(lmdb_lock_file_mode(dir, &mode) == 0, 259, "[ERROR:259] failed to stat readonly lookup lock"); CHECK(mode == 0660, 260, "[ERROR:260] readonly lookup recreated lock with wrong mode"); CHECK(remove_lmdb_lock_file(dir) == 0, 261, "[ERROR:261] failed to remove lock before database walk"); old_umask = umask(0); rc = walk_database_start(&cfg); umask(old_umask); CHECK(rc == WALK_DATABASE_SUCCESS, 262, "[ERROR:262] walk database start failed"); walk_database_finish(); CHECK(lmdb_lock_file_mode(dir, &mode) == 0, 263, "[ERROR:263] failed to stat walk lock"); CHECK(mode == 0660, 264, "[ERROR:264] walk recreated lock with wrong mode"); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 265, "[ERROR:265] readonly lock-mode cleanup failed"); return 0; } static int test_lmdb_chunked_import(void) { const unsigned int record_count = 4105; const char *digest = "2222222222222222222222222222222222222222222222222222222222222222"; conf_t cfg; char dir[128]; char first_path[64]; char last_path[64]; char *payload = NULL; size_t payload_size = 0; FILE *stream; long entries = 0; int rc; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 90, "[ERROR:90] failed to open temporary LMDB"); stream = open_memstream(&payload, &payload_size); CHECK(stream != NULL, 91, "[ERROR:91] failed to allocate chunked payload"); for (unsigned int i = 0; i < record_count; i++) { fprintf(stream, "/usr/bin/chunked-%04u " DATA_FORMAT "\n", i, SRC_FILE_DB, (trustdb_size_t)(9000 + i), digest); } fclose(stream); rc = import_records(payload, &entries); free(payload); CHECK(rc == 0 && entries == (long)record_count, 92, "[ERROR:92] chunked record import failed"); snprintf(first_path, sizeof(first_path), "/usr/bin/chunked-%04u", 0); snprintf(last_path, sizeof(last_path), "/usr/bin/chunked-%04u", record_count - 1); CHECK(check_trust_database(first_path, NULL, -1) == 1, 93, "[ERROR:93] first chunked lookup failed"); CHECK(check_trust_database(last_path, NULL, -1) == 1, 94, "[ERROR:94] last chunked lookup failed"); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 95, "[ERROR:95] chunked cleanup failed"); return 0; } static int test_lmdb_long_path_negative_lookup(void) { conf_t cfg; char dir[128]; long entries = 0; int rc; char *path_a = build_long_path("negative-a"); char *path_b = build_long_path("negative-b"); char payload[2048]; const char *digest = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; CHECK(path_a && path_b, 50, "[ERROR:50] failed to allocate negative-lookup paths"); rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 51, "[ERROR:51] failed to open temporary LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path_a, SRC_FILE_DB, (trustdb_size_t)4100, digest); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 52, "[ERROR:52] negative-lookup record import failed"); CHECK(check_trust_database(path_b, NULL, -1) == 0, 53, "[ERROR:53] non-existent long path unexpectedly found"); free(path_a); free(path_b); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 54, "[ERROR:54] negative-lookup cleanup failed"); return 0; } /* * test_lmdb_concurrent_read_handles - exercise per-call LMDB readers. * * Returns 0 when concurrent trust lookups all succeed and lookup metrics are * reported/reset correctly. */ static int test_lmdb_concurrent_read_handles(void) { conf_t cfg; char dir[128]; long entries = 0; int rc; const char *path = "/usr/bin/concurrent-reader"; const char *digest = "9999999999999999999999999999999999999999999999999999999999999999"; char payload[256]; char report[512]; char expected[64]; pthread_t readers[CONCURRENT_READERS]; struct concurrent_lookup lookup[CONCURRENT_READERS]; atomic_bool start = false; unsigned long expected_lookups = CONCURRENT_READERS * CONCURRENT_ITERATIONS; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 70, "[ERROR:70] failed to open temporary LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path, SRC_FILE_DB, (trustdb_size_t)555, digest); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 71, "[ERROR:71] concurrent record import failed"); CHECK(read_database_metrics_report(report, sizeof(report), 1) == 0, 72, "[ERROR:72] metrics reset report failed"); for (unsigned int i = 0; i < CONCURRENT_READERS; i++) { lookup[i].path = path; lookup[i].start = &start; lookup[i].failed = 0; rc = pthread_create(&readers[i], NULL, concurrent_lookup_worker, &lookup[i]); CHECK(rc == 0, 73, "[ERROR:73] failed to create lookup reader"); } atomic_store_explicit(&start, true, memory_order_release); for (unsigned int i = 0; i < CONCURRENT_READERS; i++) { rc = pthread_join(readers[i], NULL); CHECK(rc == 0, 74, "[ERROR:74] failed to join lookup reader"); CHECK(lookup[i].failed == 0, 75, "[ERROR:75] concurrent lookup failed"); } CHECK(read_database_metrics_report(report, sizeof(report), 1) == 0, 76, "[ERROR:76] metrics report failed"); snprintf(expected, sizeof(expected), "Trust DB lookups: %lu", expected_lookups); CHECK(strstr(report, expected) != NULL, 77, "[ERROR:77] lookup metrics count mismatch"); CHECK(strstr(report, "Trust DB reader slots full: 0") != NULL, 78, "[ERROR:78] reader slots full metric changed"); CHECK(strstr(report, "Trust DB lookup average") == NULL, 79, "[ERROR:79] lookup average latency metric in metrics report"); CHECK(strstr(report, "Trust DB lookup max") == NULL, 80, "[ERROR:80] lookup max latency metric in metrics report"); CHECK(read_database_metrics_report(report, sizeof(report), 0) == 0, 81, "[ERROR:81] post-reset metrics report failed"); CHECK(strstr(report, "Trust DB lookups: 0") != NULL, 82, "[ERROR:82] lookup metrics reset failed"); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 83, "[ERROR:83] concurrent cleanup failed"); return 0; } static int test_lmdb_nested_read_transaction(void) { conf_t cfg; char dir[128]; long entries = 0; int rc; const char *path = "/usr/bin/nested-reader"; const char *digest = "8989898989898989898989898989898989898989898989898989898989898989"; char payload[256]; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 154, "[ERROR:154] failed to open temporary LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path, SRC_FILE_DB, (trustdb_size_t)556, digest); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 155, "[ERROR:155] nested record import failed"); CHECK(database_nested_lookup_for_tests(path) == 1, 156, "[ERROR:156] nested trust lookup failed"); CHECK(check_trust_database(path, NULL, -1) == 1, 157, "[ERROR:157] post-nested trust lookup failed"); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 158, "[ERROR:158] nested cleanup failed"); return 0; } static int test_lmdb_failed_candidate_preserves_generation(void) { conf_t cfg; char dir[128]; long entries = 0; int rc; database_generation_test_report_t before, after; const char *path_a = "/usr/bin/preserved-a"; const char *path_b = "/usr/bin/unpublished-b"; const char *digest_a = "abababababababababababababababababababababababababababababababab"; const char *digest_b = "babababababababababababababababababababababababababababababababa"; char payload[512]; unsigned long flush_generation; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 100, "[ERROR:100] failed to open temporary LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path_a, SRC_FILE_DB, (trustdb_size_t)100, digest_a); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 101, "[ERROR:101] preserved record import failed"); CHECK(database_generation_report_for_tests(&before) == 0, 102, "[ERROR:102] generation report failed"); flush_generation = object_cache_flush_generation_snapshot(); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path_b, SRC_FILE_DB, (trustdb_size_t)200, digest_b); rc = drop_candidate_records(payload); CHECK(rc == 0, 103, "[ERROR:103] candidate import/drop failed"); CHECK(database_generation_report_for_tests(&after) == 0, 104, "[ERROR:104] post-drop generation report failed"); CHECK(after.generation == before.generation, 105, "[ERROR:105] failed candidate changed active generation"); CHECK(check_trust_database(path_a, NULL, -1) == 1, 106, "[ERROR:106] preserved generation lookup failed"); CHECK(check_trust_database(path_b, NULL, -1) == 0, 107, "[ERROR:107] dropped candidate became visible"); CHECK(object_cache_flush_generation_snapshot() == flush_generation, 108, "[ERROR:108] failed candidate requested cache flush"); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 109, "[ERROR:109] failed-candidate cleanup failed"); return 0; } static int test_lmdb_reload_failure_preserves_generation(void) { conf_t cfg; char dir[128]; long entries = 0; int rc; unsigned long before_failures, after_failures; database_generation_test_report_t before, after; const char *path = "/usr/bin/reload-preserved"; const char *digest = "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd"; char payload[256]; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 210, "[ERROR:210] failed to open temporary LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path, SRC_FILE_DB, (trustdb_size_t)300, digest); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 211, "[ERROR:211] reload-preserve record import failed"); CHECK(database_generation_report_for_tests(&before) == 0, 212, "[ERROR:212] generation report failed before reload failure"); cfg.trust = "unsupported-test-backend"; before_failures = failure_action_count(FAILURE_REASON_TRUST_RELOAD_FAILURE); rc = database_reload_for_tests(&cfg); after_failures = failure_action_count(FAILURE_REASON_TRUST_RELOAD_FAILURE); CHECK(rc != 0, 213, "[ERROR:213] reload failure unexpectedly passed"); CHECK(after_failures == before_failures + 1, 214, "[ERROR:214] reload failure was not recorded"); CHECK(database_generation_report_for_tests(&after) == 0, 215, "[ERROR:215] generation report failed after reload failure"); CHECK(after.generation == before.generation, 216, "[ERROR:216] failed reload changed active generation"); CHECK(check_trust_database(path, NULL, -1) == 1, 217, "[ERROR:217] failed reload lost previous trust DB"); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 218, "[ERROR:218] reload-failure cleanup failed"); return 0; } /* * test_lmdb_incremental_update_keeps_duplicate_hashes - package update window. * * A package transaction can replace a trusted executable and then run the new * file from a scriptlet before the next full trust DB rebuild publishes. The * per-file update path must add the new hash to the active generation without * dropping the old hash that existing open executables may still need. * * Returns 0 when both old and new file contents are trusted after an * incremental active update. */ static int test_lmdb_incremental_update_keeps_duplicate_hashes(void) { const char old_contents[] = "shadow-utils-useradd-old\n"; const char new_contents[] = "shadow-utils-useradd-new\n"; conf_t cfg; char dir[128]; char path[128]; char payload[512]; struct file_info *old_info = NULL; struct file_info *new_info = NULL; char *old_hash = NULL; char *new_hash = NULL; long entries = 0; int old_fd = -1; int new_fd = -1; int rc; old_fd = create_test_file(path, sizeof(path), old_contents); CHECK(old_fd != -1, 230, "[ERROR:230] failed to create old package file"); rc = describe_test_file(old_fd, &old_info, &old_hash); CHECK(rc == 0, 231, "[ERROR:231] failed to hash old package file"); rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 232, "[ERROR:232] failed to open temporary LMDB"); cfg.integrity = IN_SHA256; rc = decision_config_publish(&cfg); CHECK(rc == 0, 233, "[ERROR:233] failed to publish SHA256 integrity config"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path, SRC_RPM, (trustdb_size_t)old_info->size, old_hash); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 234, "[ERROR:234] old package record import failed"); CHECK(check_trust_database(path, old_info, old_fd) == 1, 235, "[ERROR:235] old package file was not trusted"); rc = replace_test_file(path, new_contents); CHECK(rc == 0, 236, "[ERROR:236] failed to replace package file"); new_fd = open(path, O_RDONLY|O_CLOEXEC); CHECK(new_fd != -1, 237, "[ERROR:237] failed to open replacement package file"); rc = describe_test_file(new_fd, &new_info, &new_hash); CHECK(rc == 0, 238, "[ERROR:238] failed to hash replacement package file"); CHECK(old_info->size == new_info->size, 239, "[ERROR:239] replacement test contents changed size"); CHECK(strcmp(old_hash, new_hash) != 0, 240, "[ERROR:240] replacement test contents did not change hash"); CHECK(check_trust_database(path, new_info, new_fd) == 0, 241, "[ERROR:241] replacement file trusted before incremental update"); rc = database_store_update_record(path, (trustdb_size_t)new_info->size, new_hash); CHECK(rc == 0, 242, "[ERROR:242] incremental update failed"); CHECK(check_trust_database(path, new_info, new_fd) == 1, 243, "[ERROR:243] replacement file was not trusted after update"); CHECK(check_trust_database(path, old_info, old_fd) == 1, 244, "[ERROR:244] old duplicate hash was not retained"); close(new_fd); close(old_fd); unlink(path); free(new_hash); free(old_hash); free(new_info); free(old_info); decision_config_destroy(); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 245, "[ERROR:245] incremental-duplicate cleanup failed"); return 0; } /* * test_lmdb_shared_environment_ownership - verify live environment locking. * * Incremental LMDB writes must coexist with decision readers while state * sizing reports must wait for exclusive close/swap ownership. Final reports * must also tolerate the database and update lock already being closed. * * Returns 0 when all three lifetime rules hold. */ static int test_lmdb_shared_environment_ownership(void) { const char *digest = "abababababababababababababababababababababababababababababababab"; struct incremental_update update = { .path = "/usr/bin/shared-update-lock", .hash = digest, .size = 4096, .rc = 1, }; struct utilization_report utilization; pthread_barrier_t report_start; pthread_t thread; conf_t cfg; char dir[128]; char report[512]; int join_rc; int cleanup_rc = 0; int rc; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 266, "[ERROR:266] shared-lock LMDB setup failed"); database_update_read_lock(); rc = pthread_create(&thread, NULL, incremental_update_worker, &update); if (rc) database_update_read_unlock(); CHECK(rc == 0, 267, "[ERROR:267] incremental writer create failed"); join_rc = timed_join(thread, 2); database_update_read_unlock(); if (join_rc) cleanup_rc = pthread_join(thread, NULL); CHECK(join_rc == 0, 268, "[ERROR:268] incremental writer blocked a shared DB reader"); CHECK(cleanup_rc == 0 && update.rc == 0, 269, "[ERROR:269] shared-lock incremental writer failed"); utilization.config = &cfg; utilization.output = tmpfile(); CHECK(utilization.output != NULL, 270, "[ERROR:270] utilization report output setup failed"); rc = pthread_barrier_init(&report_start, NULL, 2); CHECK(rc == 0, 271, "[ERROR:271] utilization report barrier setup failed"); utilization.start = &report_start; lock_update_thread(); rc = pthread_create(&thread, NULL, utilization_report_worker, &utilization); if (rc) unlock_update_thread(); CHECK(rc == 0, 272, "[ERROR:272] utilization reporter create failed"); pthread_barrier_wait(&report_start); join_rc = timed_join(thread, 1); unlock_update_thread(); if (join_rc) cleanup_rc = pthread_join(thread, NULL); pthread_barrier_destroy(&report_start); fclose(utilization.output); CHECK(join_rc == ETIMEDOUT, 273, "[ERROR:273] utilization report bypassed exclusive DB ownership"); CHECK(cleanup_rc == 0, 274, "[ERROR:274] utilization reporter cleanup failed"); database_close_for_tests(); CHECK(read_database_utilization_text(&cfg, report, sizeof(report)) == 0, 275, "[ERROR:275] utilization report failed after DB close"); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 276, "[ERROR:276] shared-lock LMDB cleanup failed"); return 0; } /* * test_lmdb_rpm_sha256_only_rejects_stale_sha1 - enforce lookup hash floor. * * RPM compatibility mode must continue trusting SHA1 RPM records for older * third-party repositories. Once rpm_sha256_only is enabled, even an already * imported stale SHA1 record must not be trusted by SHA256 integrity lookups. * * Returns 0 when the same SHA1 record is accepted only in compatibility mode. */ static int test_lmdb_rpm_sha256_only_rejects_stale_sha1(void) { const char contents[] = "third-party-sha1-rpm-payload\n"; conf_t cfg; char dir[128]; char path[128]; char payload[512]; struct file_info *info = NULL; char *sha1_hash = NULL; char *sha256_hash = NULL; long entries = 0; int fd = -1; int rc; fd = create_test_file(path, sizeof(path), contents); CHECK(fd != -1, 246, "[ERROR:246] failed to create SHA1 RPM file"); rc = describe_test_file_hash(fd, &info, &sha1_hash, FILE_HASH_ALG_SHA1); CHECK(rc == 0, 247, "[ERROR:247] failed to SHA1 hash RPM file"); sha256_hash = get_hash_from_fd2(fd, info->size, FILE_HASH_ALG_SHA256); CHECK(sha256_hash != NULL, 255, "[ERROR:255] failed to SHA256 hash RPM file"); rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 248, "[ERROR:248] failed to open temporary LMDB"); cfg.integrity = IN_SHA256; cfg.rpm_sha256_only = 0; rc = decision_config_publish(&cfg); CHECK(rc == 0, 249, "[ERROR:249] failed to publish SHA1-compatible config"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path, SRC_RPM, (trustdb_size_t)info->size, sha1_hash); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 250, "[ERROR:250] SHA1 RPM record import failed"); CHECK(check_trust_database(path, info, fd) == 1, 251, "[ERROR:251] SHA1 RPM record not trusted in compatibility mode"); cfg.rpm_sha256_only = 1; rc = decision_config_publish(&cfg); CHECK(rc == 0, 252, "[ERROR:252] failed to publish SHA1-strict config"); CHECK(check_trust_database(path, info, fd) == 0, 253, "[ERROR:253] stale SHA1 RPM record trusted in strict mode"); rc = database_store_update_record(path, (trustdb_size_t)info->size, sha256_hash); CHECK(rc == 0, 256, "[ERROR:256] SHA256 duplicate update failed"); CHECK(check_trust_database(path, info, fd) == 1, 257, "[ERROR:257] SHA256 duplicate not trusted in strict mode"); close(fd); unlink(path); free(sha256_hash); free(sha1_hash); free(info); decision_config_destroy(); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 254, "[ERROR:254] SHA1 RPM cleanup failed"); return 0; } static int test_lmdb_autosize_generation_reload_target(void) { unsigned int target; target = database_autosize_target_mb_for_tests( /*active_pages*/14277, /*allocated_pages*/14283, /*map_pages*/21760, /*page_size*/4096); CHECK(target >= 140, 110, "[ERROR:110] autosize target ignored reload headroom"); CHECK(target > 85, 111, "[ERROR:111] autosize target kept undersized map"); return 0; } static int test_lmdb_autosize_retry_uses_actual_map(void) { unsigned int retry; retry = database_autosize_retry_mb_for_tests( /*old_mb*/8, /*active_pages*/256, /*map_pages*/25600, /*page_size*/4096); CHECK(retry >= 125, 134, "[ERROR:134] autosize retry ignored actual LMDB map size"); CHECK(retry > 8, 135, "[ERROR:135] autosize retry did not grow configured size"); return 0; } static int test_lmdb_manual_resize_report_is_gated(void) { conf_t cfg; char dir[128]; char report[2048]; char *payload; size_t used = 0; long entries = 0; int rc; const unsigned int records = 12000; const char *digest = "9191919191919191919191919191919191919191919191919191919191919191"; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 112, "[ERROR:112] failed to open temporary LMDB"); payload = calloc(records, 192); CHECK(payload != NULL, 113, "[ERROR:113] resize report payload allocation failed"); for (unsigned int i = 0; i < records; i++) { int written = snprintf(payload + used, records * 192 - used, "/usr/bin/resize-report-%04u " DATA_FORMAT "\n", i, SRC_FILE_DB, (trustdb_size_t)(1000 + i), digest); CHECK(written > 0 && (size_t)written < records * 192 - used, 114, "[ERROR:114] resize report payload truncated"); used += written; } rc = import_records(payload, &entries); free(payload); CHECK(rc == 0 && entries == records, 115, "[ERROR:115] resize report import failed"); /* * Simulate a manual configuration that is below the computed reload * target without reopening the test map. The report is about the * administrator's numeric db_max_size, not LMDB's current file size. */ cfg.db_max_size = 1; cfg.do_auto_db_sizing = false; CHECK(read_database_utilization_text(&cfg, report, sizeof(report)) == 0, 116, "[ERROR:116] manual resize report failed"); CHECK(strstr(report, "Trust database resize recommended: yes") != NULL, 117, "[ERROR:117] manual resize recommendation missing"); CHECK(strstr(report, "Trust database resize target:") != NULL, 118, "[ERROR:118] manual resize target missing"); cfg.do_auto_db_sizing = true; CHECK(read_database_utilization_text(&cfg, report, sizeof(report)) == 0, 119, "[ERROR:119] auto resize report failed"); CHECK(strstr(report, "Trust database resize recommended:") == NULL, 132, "[ERROR:132] resize recommendation shown in auto mode"); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 133, "[ERROR:133] resize report cleanup failed"); return 0; } static int test_lmdb_startup_generation_resets(void) { conf_t cfg; char dir[128]; long entries = 0; int rc; database_generation_test_report_t report; const char *path_a = "/usr/bin/startup-generation-a"; const char *path_b = "/usr/bin/startup-generation-b"; const char *path_c = "/usr/bin/startup-generation-c"; const char *path_d = "/usr/bin/startup-generation-d"; const char *digest = "edededededededededededededededededededededededededededededededed"; char payload[512]; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 160, "[ERROR:160] failed to open temporary LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path_a, SRC_FILE_DB, (trustdb_size_t)100, digest); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 161, "[ERROR:161] startup generation initial import failed"); CHECK(database_generation_report_for_tests(&report) == 0, 162, "[ERROR:162] startup generation initial report failed"); CHECK(report.generation == 1, 163, "[ERROR:163] initial generation did not start at one"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path_b, SRC_FILE_DB, (trustdb_size_t)200, digest); rc = publish_records(&cfg, payload); CHECK(rc == 0, 164, "[ERROR:164] runtime generation publish failed"); CHECK(database_generation_report_for_tests(&report) == 0, 165, "[ERROR:165] runtime generation report failed"); CHECK(report.generation == 2, 166, "[ERROR:166] runtime generation did not advance"); database_close_for_tests(); rc = database_open_for_tests(&cfg); CHECK(rc == 0, 167, "[ERROR:167] failed to reopen LMDB"); CHECK(database_generation_report_for_tests(&report) == 0, 168, "[ERROR:168] reopened generation report failed"); CHECK(report.generation == 1, 169, "[ERROR:169] persisted generation did not reset on startup"); CHECK(check_trust_database(path_b, NULL, -1) == 1, 170, "[ERROR:170] reopened generation lookup failed"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path_c, SRC_FILE_DB, (trustdb_size_t)300, digest); rc = publish_startup_records(&cfg, payload); CHECK(rc == 0, 171, "[ERROR:171] startup rebuild publish failed"); CHECK(database_generation_report_for_tests(&report) == 0, 172, "[ERROR:172] startup rebuild report failed"); CHECK(report.generation == 1, 173, "[ERROR:173] startup rebuild did not reset generation"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path_d, SRC_FILE_DB, (trustdb_size_t)400, digest); rc = publish_records(&cfg, payload); CHECK(rc == 0, 174, "[ERROR:174] post-startup runtime publish failed"); CHECK(database_generation_report_for_tests(&report) == 0, 175, "[ERROR:175] post-startup runtime report failed"); CHECK(report.generation == 2, 176, "[ERROR:176] post-startup runtime generation mismatch"); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 177, "[ERROR:177] startup generation cleanup failed"); return 0; } static int test_lmdb_offline_compaction_swaps_environment(void) { conf_t cfg; char dir[128]; long entries = 0; int rc; database_generation_test_report_t before, after; const char *path_a = "/usr/bin/offline-compact-a"; const char *path_b = "/usr/bin/offline-compact-b"; const char *digest_a = "1212121212121212121212121212121212121212121212121212121212121212"; const char *digest_b = "3434343434343434343434343434343434343434343434343434343434343434"; char payload[512]; unsigned long flush_generation; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 180, "[ERROR:180] failed to open temporary LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path_a, SRC_FILE_DB, (trustdb_size_t)500, digest_a); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 181, "[ERROR:181] offline initial import failed"); CHECK(database_generation_report_for_tests(&before) == 0, 182, "[ERROR:182] offline initial report failed"); CHECK(before.generation == 1 && before.lmdb_generation == 1, 183, "[ERROR:183] initial LMDB generation mismatch"); flush_generation = object_cache_flush_generation_snapshot(); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path_b, SRC_FILE_DB, (trustdb_size_t)600, digest_b); { int fd = make_memfd_input(payload); CHECK(fd != -1, 184, "[ERROR:184] offline compaction memfd failed"); rc = database_compact_memfd_for_tests(fd, &cfg); close(fd); } CHECK(rc == 0, 185, "[ERROR:185] offline compaction failed"); CHECK(database_generation_report_for_tests(&after) == 0, 186, "[ERROR:186] offline compaction report failed"); CHECK(after.generation == before.generation + 1, 187, "[ERROR:187] trust generation did not advance"); CHECK(after.lmdb_generation == before.lmdb_generation + 1, 188, "[ERROR:188] LMDB environment generation did not advance"); CHECK(check_trust_database(path_a, NULL, -1) == 0, 189, "[ERROR:189] old environment record survived replacement"); CHECK(check_trust_database(path_b, NULL, -1) == 1, 190, "[ERROR:190] rebuilt environment record missing"); CHECK(object_cache_flush_generation_snapshot() > flush_generation, 191, "[ERROR:191] offline compaction did not request cache flush"); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 192, "[ERROR:192] offline compaction cleanup failed"); return 0; } static int test_lmdb_offline_compaction_preserves_busy_environment(void) { conf_t cfg; char dir[128]; long entries = 0; int rc; void *held; database_generation_test_report_t before, after; const char *path_a = "/usr/bin/offline-preserve-a"; const char *path_b = "/usr/bin/offline-preserve-b"; const char *digest = "5656565656565656565656565656565656565656565656565656565656565656"; char payload[512]; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 193, "[ERROR:193] failed to open temporary LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path_a, SRC_FILE_DB, (trustdb_size_t)700, digest); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 194, "[ERROR:194] preserve initial import failed"); CHECK(database_generation_report_for_tests(&before) == 0, 195, "[ERROR:195] preserve initial report failed"); held = database_generation_hold_for_tests(); CHECK(held != NULL, 196, "[ERROR:196] preserve generation hold failed"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path_b, SRC_FILE_DB, (trustdb_size_t)800, digest); { int fd = make_memfd_input(payload); CHECK(fd != -1, 197, "[ERROR:197] preserve compaction memfd failed"); rc = database_compact_memfd_for_tests(fd, &cfg); close(fd); } CHECK(rc != 0, 198, "[ERROR:198] compaction unexpectedly swapped busy environment"); database_generation_release_for_tests(held); CHECK(database_generation_report_for_tests(&after) == 0, 199, "[ERROR:199] preserve after report failed"); CHECK(after.generation == before.generation, 200, "[ERROR:200] failed compaction changed trust generation"); CHECK(after.lmdb_generation == before.lmdb_generation, 201, "[ERROR:201] failed compaction changed LMDB generation"); CHECK(check_trust_database(path_a, NULL, -1) == 1, 202, "[ERROR:202] old record missing after failed compaction"); CHECK(check_trust_database(path_b, NULL, -1) == 0, 203, "[ERROR:203] failed compaction published new record"); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 204, "[ERROR:204] preserve compaction cleanup failed"); return 0; } static int test_lmdb_held_reader_delays_reclamation(void) { conf_t cfg; char dir[128]; long entries = 0; int rc; void *held; database_generation_test_report_t report; const char *path_a = "/usr/bin/held-reader-a"; const char *path_b = "/usr/bin/held-reader-b"; const char *digest_a = "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd"; const char *digest_b = "dcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdc"; char payload[512]; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 120, "[ERROR:120] failed to open temporary LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path_a, SRC_FILE_DB, (trustdb_size_t)300, digest_a); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 121, "[ERROR:121] held-reader initial import failed"); held = database_generation_hold_for_tests(); CHECK(held != NULL, 122, "[ERROR:122] failed to hold generation"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", path_b, SRC_FILE_DB, (trustdb_size_t)400, digest_b); rc = publish_records(&cfg, payload); CHECK(rc == 0, 123, "[ERROR:123] held-reader publish failed"); CHECK(database_generation_report_for_tests(&report) == 0, 124, "[ERROR:124] held-reader report failed"); CHECK(report.retired_count == 1, 125, "[ERROR:125] held reader did not delay retired generation"); database_reclaim_generations_for_tests(); CHECK(database_generation_report_for_tests(&report) == 0, 126, "[ERROR:126] held-reader reclaim report failed"); CHECK(report.retired_count == 1, 127, "[ERROR:127] reclaimed generation while reader was held"); database_generation_release_for_tests(held); database_reclaim_generations_for_tests(); CHECK(database_generation_report_for_tests(&report) == 0, 128, "[ERROR:128] post-release report failed"); CHECK(report.retired_count == 0, 129, "[ERROR:129] released generation was not reclaimed"); CHECK(check_trust_database(path_b, NULL, -1) == 1, 130, "[ERROR:130] published generation lookup failed"); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 131, "[ERROR:131] held-reader cleanup failed"); return 0; } static int test_lmdb_concurrent_publish_storm(void) { conf_t cfg; char dir[128]; long entries = 0; int rc; const char *stable_path = "/usr/bin/reload-stable"; const char *last_path = "/usr/bin/reload-storm-5"; const char *digest = "efefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef"; char payload[512]; pthread_t readers[RELOAD_READERS]; struct reload_lookup lookup[RELOAD_READERS]; atomic_bool start = false; atomic_bool stop_readers = false; database_generation_test_report_t before, after; unsigned long flush_generation; rc = with_temp_db(dir, sizeof(dir), &cfg); CHECK(rc == 0, 140, "[ERROR:140] failed to open temporary LMDB"); snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n", stable_path, SRC_FILE_DB, (trustdb_size_t)500, digest); rc = import_records(payload, &entries); CHECK(rc == 0 && entries == 1, 141, "[ERROR:141] reload-storm initial import failed"); CHECK(database_generation_report_for_tests(&before) == 0, 142, "[ERROR:142] reload-storm initial report failed"); for (unsigned int i = 0; i < RELOAD_READERS; i++) { lookup[i].path = stable_path; lookup[i].start = &start; lookup[i].stop = &stop_readers; lookup[i].failed = 0; rc = pthread_create(&readers[i], NULL, reload_lookup_worker, &lookup[i]); CHECK(rc == 0, 143, "[ERROR:143] failed to create reload reader"); } flush_generation = object_cache_flush_generation_snapshot(); atomic_store_explicit(&start, true, memory_order_release); for (unsigned int i = 0; i < 6; i++) { snprintf(payload, sizeof(payload), "%s " DATA_FORMAT "\n" "/usr/bin/reload-storm-%u " DATA_FORMAT "\n", stable_path, SRC_FILE_DB, (trustdb_size_t)(600 + i), digest, i, SRC_FILE_DB, (trustdb_size_t)(700 + i), digest); rc = publish_records(&cfg, payload); CHECK(rc == 0, 144, "[ERROR:144] reload-storm publish failed"); CHECK(object_cache_flush_generation_snapshot() > flush_generation, 145, "[ERROR:145] publish did not request cache flush"); flush_generation = object_cache_flush_generation_snapshot(); } atomic_store_explicit(&stop_readers, true, memory_order_release); for (unsigned int i = 0; i < RELOAD_READERS; i++) { rc = pthread_join(readers[i], NULL); CHECK(rc == 0, 146, "[ERROR:146] failed to join reload reader"); CHECK(lookup[i].failed == 0, 147, "[ERROR:147] concurrent lookup failed during publish"); } database_reclaim_generations_for_tests(); CHECK(database_generation_report_for_tests(&after) == 0, 148, "[ERROR:148] reload-storm final report failed"); CHECK(after.generation == before.generation + 6, 149, "[ERROR:149] reload-storm generation count mismatch"); CHECK(after.retired_count == 0, 150, "[ERROR:150] reload-storm retired generations leaked"); CHECK(check_trust_database(stable_path, NULL, -1) == 1, 151, "[ERROR:151] stable path missing after reload storm"); CHECK(check_trust_database(last_path, NULL, -1) == 1, 152, "[ERROR:152] last reload-storm path missing"); database_close_for_tests(); database_set_location(NULL, NULL); CHECK(remove_lmdb_files(dir) == 0, 153, "[ERROR:153] reload-storm cleanup failed"); return 0; } int main(void) { int rc; rc = test_data_format_round_trip(); if (rc) return rc; rc = test_lmdb_large_size_import(); if (rc) return rc; rc = test_lmdb_short_path_round_trip(); if (rc) return rc; rc = test_lmdb_long_path_round_trip(); if (rc) return rc; rc = test_lmdb_long_path_shared_prefix_no_collision(); if (rc) return rc; rc = test_lmdb_long_path_negative_lookup(); if (rc) return rc; rc = test_lmdb_readonly_probe_does_not_break_live_env(); if (rc) return rc; rc = test_lmdb_readonly_lookup_keeps_dbi_valid(); if (rc) return rc; rc = test_lmdb_readonly_helpers_create_lock_with_trust_mode(); if (rc) return rc; rc = test_lmdb_chunked_import(); if (rc) return rc; rc = test_lmdb_concurrent_read_handles(); if (rc) return rc; rc = test_lmdb_nested_read_transaction(); if (rc) return rc; rc = test_lmdb_failed_candidate_preserves_generation(); if (rc) return rc; rc = test_lmdb_reload_failure_preserves_generation(); if (rc) return rc; rc = test_lmdb_incremental_update_keeps_duplicate_hashes(); if (rc) return rc; rc = test_lmdb_shared_environment_ownership(); if (rc) return rc; rc = test_lmdb_rpm_sha256_only_rejects_stale_sha1(); if (rc) return rc; rc = test_lmdb_autosize_generation_reload_target(); if (rc) return rc; rc = test_lmdb_autosize_retry_uses_actual_map(); if (rc) return rc; rc = test_lmdb_manual_resize_report_is_gated(); if (rc) return rc; rc = test_lmdb_startup_generation_resets(); if (rc) return rc; rc = test_lmdb_offline_compaction_swaps_environment(); if (rc) return rc; rc = test_lmdb_offline_compaction_preserves_busy_environment(); if (rc) return rc; rc = test_lmdb_held_reader_delays_reclamation(); if (rc) return rc; rc = test_lmdb_concurrent_publish_storm(); if (rc) return rc; return 0; } fapolicyd-2.0/src/tests/uid_proc_test.c000066400000000000000000000037401524071644000202740ustar00rootroot00000000000000#include #include #include #include #include #include #include "attr-sets.h" #include "process.h" /* * require_uid - ensure a collected attribute set contains a specific UID * @set: attribute set generated by read_proc_status() * @uid: numeric user identifier expected in the set * @label: description for error reporting when the lookup fails */ static void require_uid(attr_sets_entry_t *set, unsigned int uid, const char *label) { if (!attr_set_check_int(set, (int64_t)uid)) error(1, 0, "%s uid %u not found", label, uid); } /* * main - validate UID collection helper records credential variants * * Return: 0 when all expected user IDs are reported, or terminate via error() * if any lookup fails during the exercise. */ int main(void) { struct proc_status_info info = { .ppid = -1, .uid = NULL, .groups = NULL, .comm = NULL }; attr_sets_entry_t *uids; FILE *status; char buf[4096]; int saw_uid_line = 0; if (read_proc_status(getpid(), PROC_STAT_UID, &info) != 0) error(1, 0, "Unable to obtain uid set"); uids = info.uid; info.uid = NULL; if (!uids) error(1, 0, "Unable to obtain uid set"); status = fopen("/proc/self/status", "rt"); if (!status) error(1, errno, "fopen /proc/self/status"); while (fgets(buf, sizeof(buf), status)) { if (memcmp(buf, "Uid:", 4) == 0) { unsigned int real_uid = 0, eff_uid = 0; unsigned int saved_uid = 0, fs_uid = 0; int fields = sscanf(buf, "Uid: %u %u %u %u", &real_uid, &eff_uid, &saved_uid, &fs_uid); if (fields >= 1) require_uid(uids, real_uid, "Real"); if (fields >= 2) require_uid(uids, eff_uid, "Effective"); // if (fields >= 3) // require_uid(uids, saved_uid, "Saved"); if (fields >= 4) require_uid(uids, fs_uid, "Filesystem"); saw_uid_line = 1; break; } } fclose(status); if (!saw_uid_line) error(1, 0, "Uid line not found in /proc/self/status"); attr_set_destroy(uids); return 0; }