pax_global_header00006660000000000000000000000064152033664460014523gustar00rootroot0000000000000052 comment=5c2715819639fee0681b8c7a015dc4656a3cf4d7 linux-application-whitelisting-fapolicyd-e086a8a/000077500000000000000000000000001520336644600222615ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/.editorconfig000066400000000000000000000007201520336644600247350ustar00rootroot00000000000000 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 linux-application-whitelisting-fapolicyd-e086a8a/.fmf/000077500000000000000000000000001520336644600231075ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/.fmf/version000066400000000000000000000000021520336644600245070ustar00rootroot000000000000001 linux-application-whitelisting-fapolicyd-e086a8a/.github/000077500000000000000000000000001520336644600236215ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/.github/workflows/000077500000000000000000000000001520336644600256565ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/.github/workflows/.rawhide-fedora-build000066400000000000000000000014261520336644600316400ustar00rootroot00000000000000name: 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 linux-application-whitelisting-fapolicyd-e086a8a/.github/workflows/almalinux9.yml000066400000000000000000000016041520336644600304650ustar00rootroot00000000000000name: 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 linux-application-whitelisting-fapolicyd-e086a8a/.github/workflows/centosstream10.yml000066400000000000000000000016331520336644600312540ustar00rootroot00000000000000name: 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 linux-application-whitelisting-fapolicyd-e086a8a/.github/workflows/rockylinux9.yml000066400000000000000000000016211520336644600307010ustar00rootroot00000000000000name: 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 linux-application-whitelisting-fapolicyd-e086a8a/.github/workflows/stable-fedora-build.yml000066400000000000000000000014501520336644600322060ustar00rootroot00000000000000name: 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 linux-application-whitelisting-fapolicyd-e086a8a/.github/workflows/ubuntu22.yml000066400000000000000000000020761520336644600300740ustar00rootroot00000000000000name: 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 linux-application-whitelisting-fapolicyd-e086a8a/.gitignore000066400000000000000000000017451520336644600242600ustar00rootroot00000000000000# 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 linux-application-whitelisting-fapolicyd-e086a8a/.packit.yaml000066400000000000000000000033321520336644600244770ustar00rootroot00000000000000specfile_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 linux-application-whitelisting-fapolicyd-e086a8a/AGENTS.md000066400000000000000000000071061520336644600235700ustar00rootroot00000000000000# Repository Guidelines This project contains the code for the File Access Policy Deamon (fapolicyd). The repository uses autotools and has optional self-tests. Follow the instructions below when making changes. ## Building 1. Bootstrap and configure the build. The README shows an example: ``` cd fapolicyd autoreconf -f --install ./configure --with-audit --with-rpm --with-deb --disable-shared make ``` 2. Tests can be run with `make check` as described in INSTALL: ``` 2. Type 'make' to compile the package. 3. Optionally, type 'make check' to run any self-tests that come with the package, generally using the just-built uninstalled binaries. 3. Installation (`make install`) is typically performed only after successful tests. ## Project Structure for Navigation - `/src`: This is where the code that makes up fapolicyd and fapolicy-cli are located - `/library`: This is where the common code between fapolicyd and the cli app is located - `/daemon`: This is where the daemon code for fapolicyd is located - `/cli`: This is where we find the code for the command line helper application. - `/dnf`: This holds the code for fapolicyd-dnf-plugin.py - `/deb`: This holds information about building for Debian - `/init`: This holds the code related to initializing the daemon and loading rules - `/docs`: This holds all of the man pages - `/rules.d`: This holds access control rules ## Code Style Contributions should follow the Linux Kernel coding style: ``` So, if you would like to test it and report issues or even contribute code feel free to do so. But please discuss the contribution first to ensure that its acceptable. This project uses the Linux Kernel Style Guideline. Please follow it if you wish to contribute. ``` In practice this means: - Indent with tabs (8 spaces per tab). - Keep lines within ~80 columns. - Place braces and other formatting as in the kernel style. However, if the basic block is a 1 liner, do not use curly braces for it. - Add a comment before any new function describing it, input variables, and return codes. - Comments within a function may be C++ style. - Do not do any whitespace adustment of existing code. - Keep existing function and variable names. ## Commit Messages - Use a concise one-line summary followed by a blank line and additional details if needed (similar to existing commits). ## Special Files The `rules.d` directory contains groups of access control rules intended for `fagenrules` and should remain organized as documented: ``` This group of rules are meant to be used with the fagenrules program. The fagenrules program expects rules to be located in /etc/fapolicy/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 ``` When editing rule files, keep them in the correct group and preserve the intended ordering. ## Summary - Build with `autoreconf`, `configure`, and `make`. - Run `make check` to execute the self-tests. - Follow Linux Kernel coding style (tabs, 80 columns). - Keep commit messages short and descriptive. - Always add comments to explain new code. - Maintain rule file organization as described in `rules.d/README-rules`. These guidelines should help future contributors and automated tools work consistently within the fapolicyd repository. linux-application-whitelisting-fapolicyd-e086a8a/AUTHORS000066400000000000000000000001131520336644600233240ustar00rootroot00000000000000This program was created and maintained by Steve Grubb linux-application-whitelisting-fapolicyd-e086a8a/BUILD.md000066400000000000000000000106331520336644600234450ustar00rootroot00000000000000BUILDING ======== 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/ ``` linux-application-whitelisting-fapolicyd-e086a8a/CI/000077500000000000000000000000001520336644600225545ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/CI/ci-tests.fmf000066400000000000000000000002541520336644600250020ustar00rootroot00000000000000discover: how: fmf url: https://github.com/linux-application-whitelisting/fapolicyd-tests.git filter: component:fapolicyd & tag:CI-Tier-1 execute: how: tmt linux-application-whitelisting-fapolicyd-e086a8a/COPYING000066400000000000000000001045131520336644600233200ustar00rootroot00000000000000 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 . linux-application-whitelisting-fapolicyd-e086a8a/ChangeLog000066400000000000000000000364431520336644600240450ustar00rootroot000000000000001.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 linux-application-whitelisting-fapolicyd-e086a8a/IMA-setup-RHEL8-example.md000066400000000000000000000107251520336644600266250ustar00rootroot00000000000000# 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 ``` linux-application-whitelisting-fapolicyd-e086a8a/INSTALL.tmp000066400000000000000000000001411520336644600241050ustar00rootroot00000000000000Installation Instructions ************************* See README.md for build, install, testing. linux-application-whitelisting-fapolicyd-e086a8a/Makefile.am000066400000000000000000000010541520336644600243150ustar00rootroot00000000000000 # 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 linux-application-whitelisting-fapolicyd-e086a8a/NEWS000066400000000000000000000000001520336644600227460ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/README.md000066400000000000000000001017541520336644600235500ustar00rootroot00000000000000File 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. 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. 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. 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. 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. 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 files matching the %languages macro (interpreter-consumable content), reports findings, and returns non-zero when suspicious files are found 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. The database has very fast performance because it uses the kernel virtual memory system to put the whole database in memory. If the database is sized wrongly, then fapolicyd will reserve too much memory. Don't worry too much about this. The kernel is very smart and doesn't actually allocate the memory unless its used. However, we'd like to get it right sized. Starting with the 0.9.3 version of fapolicyd, statistics about the database is output when the program shuts down. On my system, it looks like this: ``` Database max pages: 9728 Database pages in use: 7314 (75%) ``` This also correlates to the following setting in the fapolicyd.conf file: ``` db_max_size = 38 ``` This size is in megabytes. So, if you take that and multiply by 1024 * 1024, we get 39845888. A page of memory is defined as 4096. So, if we divide max_size by the page size, we get 9728 which matches the setting. Each entry in the lmdb database is 512 bytes. So, for each 4k page, we can have data on 8 trusted files. An ideal size for the database is for the statistics to come up around 75% in case you decide to install new software some day. The formula is ``` (db_max_size x percentage in use) / desired percentage = new db_max_size ``` So, working with example numbers, suppose max_size is 160 and it says it was 68% occupied. That is wasting a little space. Putting the numbers in the formula, we get (160 x 68) / 75 = 145. If you have an embedded system and are not using rpm. But instead use the file trust source and you have a list of files, then your calculation is very different. Suppose for the sake of discussion, you have 317 files that are trusted. We take that number and divide by 8. We'll round that up to 40. Take that number and multiply by 100 and divide by 75. We come up with 53.33. So, let's call it 54. This is how many pages is needed. Turning that into real memory, it's 216K. One megabyte is the smallest allocation, so you would set ``` db_max_size = 1 ``` 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. Starting with 1.1, fapolicyd-cli includes some diagnostic 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. | | --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. | | --check-rules [path] | 1.5 | Validate rule syntax without loading the rules; use --lint for policy-shape warnings. | 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 linux-application-whitelisting-fapolicyd-e086a8a/RELEASE_PROCESS.md000066400000000000000000000027051520336644600250650ustar00rootroot00000000000000# 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` linux-application-whitelisting-fapolicyd-e086a8a/TODO000066400000000000000000000003341520336644600227510ustar00rootroot00000000000000Userspace ========= * Allow rules to express paths using globbing (fnmatch) Improve reconfigure via SIGHUP to update configuration Consider adding rule testing to cli (uid, pgm, file) Support other packaging manifests linux-application-whitelisting-fapolicyd-e086a8a/autogen.sh000077500000000000000000000002141520336644600242570ustar00rootroot00000000000000#! /bin/sh set -x -e # --no-recursive is available only in recent autoconf versions autoreconf -fv --install cp INSTALL.tmp INSTALL || true linux-application-whitelisting-fapolicyd-e086a8a/configure.ac000066400000000000000000000174501520336644600245560ustar00rootroot00000000000000AC_REVISION($Revision: 1.3 $)dnl AC_INIT([fapolicyd],[1.5]) 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) 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([gettid]) 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 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 " linux-application-whitelisting-fapolicyd-e086a8a/deb/000077500000000000000000000000001520336644600230135ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/deb/README.Debian000066400000000000000000000005411520336644600250540ustar00rootroot00000000000000fapolicyd 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. linux-application-whitelisting-fapolicyd-e086a8a/deb/build_deb.sh000077500000000000000000000005511520336644600252640ustar00rootroot00000000000000#! /bin/bash cd .. make dist cd deb cp ../fapolicyd-*.tar.gz . tar zxvf fapolicyd-*.tar.gz cd fapolicyd-*/ # Ugly work around for INSTALL.tmp # Need to figure out proper fix. mv INSTALL INSTALL.tmp cd .. tar zcvf fapolicyd-*.tar.gz fapolicyd-*/ cd fapolicyd-*/ debmake cp ../rules debian/ cp ../postinst debian/ cp ../README.Debian debian/ debuild cd .. linux-application-whitelisting-fapolicyd-e086a8a/deb/postinst000077500000000000000000000005411520336644600246240ustar00rootroot00000000000000#! /bin/sh 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/ linux-application-whitelisting-fapolicyd-e086a8a/deb/rules000077500000000000000000000003571520336644600241000ustar00rootroot00000000000000#!/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 linux-application-whitelisting-fapolicyd-e086a8a/dnf/000077500000000000000000000000001520336644600230305ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/dnf/fapolicyd-dnf-plugin.py000066400000000000000000000016731520336644600274240ustar00rootroot00000000000000#!/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() linux-application-whitelisting-fapolicyd-e086a8a/doc/000077500000000000000000000000001520336644600230265ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/doc/Makefile.am000066400000000000000000000021351520336644600250630ustar00rootroot00000000000000# Makefile.am -- # Copyright 2016,2018 Red Hat Inc., Durham, North Carolina. # 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) 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 linux-application-whitelisting-fapolicyd-e086a8a/doc/fagenrules.8000066400000000000000000000021521520336644600252520ustar00rootroot00000000000000.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 only copied to \fI/etc/fapolicyd/compiled.rules\fP, if it differs. .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). linux-application-whitelisting-fapolicyd-e086a8a/doc/fapolicyd-cli.8000066400000000000000000000234231520336644600256420ustar00rootroot00000000000000.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 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 and .B Ruleset generation headers so the counter window and active policy 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 \-\-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 the \fB%languages\fP macro. Any matches are reported and a summary per mount is displayed. A non-zero status is returned when suspicious files are found so automated workflows can gate changes. .TP .B \-\-check-rules [path] Parse the rules file and validate its syntax 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. A zero exit status means the file is valid. .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. .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 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. .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 \-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 that does not pass inspection. .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 linux-application-whitelisting-fapolicyd-e086a8a/doc/fapolicyd-filter.conf.5000066400000000000000000000076341520336644600273070ustar00rootroot00000000000000.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 linux-application-whitelisting-fapolicyd-e086a8a/doc/fapolicyd-perf-test.8000066400000000000000000000022131520336644600267760ustar00rootroot00000000000000.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 linux-application-whitelisting-fapolicyd-e086a8a/doc/fapolicyd.8000066400000000000000000000114651520336644600251000ustar00rootroot00000000000000.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 ] .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 \-\-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. 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 linux-application-whitelisting-fapolicyd-e086a8a/doc/fapolicyd.conf.5000066400000000000000000000333621520336644600260210ustar00rootroot00000000000000.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 it's value. .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 100 megabytes. The special value "auto" tells the daemon to size the trust database based on current utilization whenever it starts or rebuilds the database. Auto sizing targets roughly 75% usage, grows when use exceeds 85% or shrinks when it falls under 65%, and increases the size by 25% and retries if a rebuild runs out of space mid-flight. .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. .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. .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 the daemon to work only with SHA256 and larger hashes. This is useful on the 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. If set to 0 the daemon stores SHA1/MD5 in trustdb 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. On the other hand the integrity set to SHA256 or IMA will never consider a file with SHA1 in trustdb as trusted. 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. .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 \f[C] fapolicyd-cli --check-ignore_mounts[=MOUNT] \f[R] .fi to verify the mount exists, confirm \fBnoexec\fR, and scan for files matching the \fB%languages\fR macro. The command reports findings 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 linux-application-whitelisting-fapolicyd-e086a8a/doc/fapolicyd.metrics.5000066400000000000000000000133331520336644600265360ustar00rootroot00000000000000.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. .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 Ruleset generation The active ruleset generation that the counters and rule hit table apply to. .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 max queue depth The highest internal event queue depth observed since the last metrics reset. .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. .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. .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. .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). linux-application-whitelisting-fapolicyd-e086a8a/doc/fapolicyd.rules.5000066400000000000000000000264011520336644600262220ustar00rootroot00000000000000.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. Daemons have a value of -1. The given value may 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. Globbing is not supported. 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 obj_trust with execute permission when writing rules. .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 obbject. 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. Globbing is not supported. 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 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 .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), and .BR fapolicyd.conf (5) .SH AUTHOR Steve Grubb linux-application-whitelisting-fapolicyd-e086a8a/doc/fapolicyd.state.5000066400000000000000000000166431520336644600262170ustar00rootroot00000000000000.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. .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 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. .TP .B Ruleset generation The current in-memory ruleset generation. This value increments each time a fully validated ruleset is published by the daemon. .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 CPU cores The number of online processor cores reported by the system. .TP .B q_size The configured size of the internal event queue. .TP .B Subject defer array size The number of preallocated entries available for subject-slot deferral. .TP .B Subject cache size The configured number of entries in the subject cache. .TP .B Object cache size The configured number of entries in the object cache. .TP .B Trust database max pages The configured maximum LMDB page count for the trust database. .SS Resource utilization .TP .B Trust database pages in use The number and percentage of LMDB pages currently used by the trust database. .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 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 (observe) 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 Ruleset generation , .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 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). linux-application-whitelisting-fapolicyd-e086a8a/doc/fapolicyd.timing.5000066400000000000000000000410771520336644600263650ustar00rootroot00000000000000.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 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, long-term 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 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). linux-application-whitelisting-fapolicyd-e086a8a/doc/fapolicyd.trust.5000066400000000000000000000032101520336644600262420ustar00rootroot00000000000000.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 line has to contain three columns and space is a valid separator. The first column contains full path to the file, the second is size of the file in bytes and the third is valid sha256 hash. .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 linux-application-whitelisting-fapolicyd-e086a8a/doc/rpm-filter.conf.5000066400000000000000000000006571520336644600261310ustar00rootroot00000000000000.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 linux-application-whitelisting-fapolicyd-e086a8a/fapolicyd.spec000066400000000000000000000235721520336644600251200ustar00rootroot00000000000000#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: 1.5 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 %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) %{_bindir}/%{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(660,root,%{name}) /run/%{name}/%{name}.fifo %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 * Wed May 20 2026 Petr Lautrbach - 1.5-1 - New release linux-application-whitelisting-fapolicyd-e086a8a/init/000077500000000000000000000000001520336644600232245ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/init/Makefile.am000066400000000000000000000012421520336644600252570ustar00rootroot00000000000000EXTRA_DIST = \ fapolicyd.service \ 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) dist_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} ${MAGIC}: $(EXTRA_DIST) file -C -m ${top_srcdir}/init/fapolicyd-magic linux-application-whitelisting-fapolicyd-e086a8a/init/fagenrules000066400000000000000000000070171520336644600253070ustar00rootroot00000000000000#!/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 # 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 SourceRulesDir=/etc/fapolicyd/rules.d TmpRules=$(mktemp /tmp/farules.XXXXXXXX) ASuffix="prev" OnlyCheck=0 LoadRules=0 RETVAL=0 usage="Usage: $0 [--check|--load]" # Delete the interim file on faults trap 'rm -f ${TmpRules}; exit 1' HUP INT QUIT PIPE TERM try_load() { pid=$(pidof fapolicyd) if [ $LoadRules -eq 1 ] && [ "x$pid" != "x" ] ; 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}" rm -f "${TmpRules}" try_load exit 1 fi files=$(ls ${SourceRulesDir} 2>/dev/null | wc -w) if [ "$files" = "0" ] ; then echo "No rules in ${SourceRulesDir}" rm -f "${TmpRules}" # 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" rm -f "${TmpRules}" 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 echo "## This file is automatically generated from $SourceRulesDir" >> "${TmpRules}" for rules in $(/bin/ls -1v ${SourceRulesDir} | grep "\.rules$") ; do cat ${SourceRulesDir}/"${rules}" && echo done | awk ' BEGIN { rest = 0; } { if (length($0) < 1) { next; } if (match($0, "^\\s*#")) { next; } rules[rest++] = $0; } END { for (i = 0; i < rest; i++) { printf "%s\n", rules[i]; } }' >> "${TmpRules}" fapolicyd-cli --check-rules "${TmpRules}" err=$? if [ $err -ne 0 ]; then echo "Rules file content:" cat -n "${TmpRules}" rm -f "${TmpRules}" exit $err fi # If the same then quit cmp -s "${TmpRules}" ${DestinationFile} > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "$0: No change" rm -f "${TmpRules}" try_load exit $RETVAL elif [ $OnlyCheck -eq 1 ] ; then echo "$0: Rules have changed and should be updated" rm -f "${TmpRules}" exit 0 fi # Otherwise we install the new file if [ -f ${DestinationFile} ]; then cp ${DestinationFile} ${DestinationFile}.${ASuffix} fi # We copy the file so that it gets the right selinux label cp "${TmpRules}" ${DestinationFile} chmod 0644 ${DestinationFile} chgrp fapolicyd ${DestinationFile} # Restore context on MLS system. # /tmp is SystemLow & fapolicyd.rules is SystemHigh if [ -x /usr/sbin/restorecon ] ; then /usr/sbin/restorecon -F ${DestinationFile} fi rm -f "${TmpRules}" try_load exit $RETVAL linux-application-whitelisting-fapolicyd-e086a8a/init/fapolicyd-filter.conf000066400000000000000000000042231520336644600273310ustar00rootroot00000000000000# --------------------------------------------------------------------------- # 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 linux-application-whitelisting-fapolicyd-e086a8a/init/fapolicyd-magic000066400000000000000000000013661520336644600262050ustar00rootroot000000000000000 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 linux-application-whitelisting-fapolicyd-e086a8a/init/fapolicyd-tmpfiles.conf000066400000000000000000000001651520336644600276700ustar00rootroot00000000000000d /run/fapolicyd 0770 root fapolicyd - d /var/lib/fapolicyd 770 root fapolicyd - Z /etc/fapolicyd - root fapolicyd - linux-application-whitelisting-fapolicyd-e086a8a/init/fapolicyd.bash_completion000066400000000000000000000112431520336644600302670ustar00rootroot00000000000000# fapolicyd-cli (8) completion -*- shell-script -*- _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 --check-watch_fs --check-ignore_mounts \ --delete-db --dump-db \ --file --filter --ftype --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 -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 COMPREPLY=( $(compgen -f -- ${cur}) ) fi return 0 fi if [[ -z ${subcmd} ]]; then 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 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 COMPREPLY=($(compgen -W "${file_opts}" -- "${cur}")) [[ ${COMPREPLY-} == *= ]] && compopt -o nospace return 0 fi fi case $prev in --ftype|-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 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 COMPREPLY=( $(compgen -d -- ${cur}) ) return 0 fi ;; --file|-f) COMPREPLY=($(compgen -W 'add delete update' -- "$cur")) return 0 ;; add|update|delete) 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 COMPREPLY=( $(compgen -f -- ${cur}) ) fi return 0 ;; --check-rules) if [[ ${cur} == -* ]]; then 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 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 COMPREPLY=( $(compgen -f -- ${cur}) ) fi return 0 ;; esac if [[ $cur == -* ]]; then 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 COMPREPLY=( $(compgen -f -- ${mount_cur}) ) COMPREPLY=( ${COMPREPLY/#/--mounts=} ) fi return 0 fi if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) [[ ${COMPREPLY-} == *= ]] && compopt -o nospace return 0 fi } complete -F _fapolicydcli fapolicyd-cli complete -F _fapolicyd fapolicyd # ex: filetype=sh linux-application-whitelisting-fapolicyd-e086a8a/init/fapolicyd.conf000066400000000000000000000011541520336644600260460ustar00rootroot00000000000000# # 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 uid = fapolicyd gid = fapolicyd do_stat_report = 1 detailed_report = 1 db_max_size = 100 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 linux-application-whitelisting-fapolicyd-e086a8a/init/fapolicyd.service000066400000000000000000000014341520336644600265620ustar00rootroot00000000000000# 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=forking RuntimeDirectory=fapolicyd PIDFile=/run/fapolicyd.pid ExecStartPre=/usr/sbin/fagenrules ExecStart=/usr/sbin/fapolicyd Restart=on-abnormal [Install] WantedBy=multi-user.target linux-application-whitelisting-fapolicyd-e086a8a/init/fapolicyd.trust000066400000000000000000000003451520336644600263030ustar00rootroot00000000000000# AUTOGENERATED FILE VERSION 2 # This file contains a list of trusted files # # FULL PATH SIZE SHA256 # /home/user/my-ls 157984 61a9960bf7d255a85811f4afcac51067b8f2e4c75e21cf4f2af95319d4ed1b87 linux-application-whitelisting-fapolicyd-e086a8a/m4/000077500000000000000000000000001520336644600226015ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/m4/dyn_linker.m4000066400000000000000000000005571520336644600252100ustar00rootroot00000000000000AC_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]) ]) linux-application-whitelisting-fapolicyd-e086a8a/rules.d/000077500000000000000000000000001520336644600236355ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/rules.d/10-languages.rules000066400000000000000000000007351520336644600271020ustar00rootroot00000000000000# 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 linux-application-whitelisting-fapolicyd-e086a8a/rules.d/20-dracut.rules000066400000000000000000000002031520336644600264050ustar00rootroot00000000000000# 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 linux-application-whitelisting-fapolicyd-e086a8a/rules.d/21-updaters.rules000066400000000000000000000002741520336644600267630ustar00rootroot00000000000000# 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 linux-application-whitelisting-fapolicyd-e086a8a/rules.d/22-buildroot.rules000066400000000000000000000017411520336644600271400ustar00rootroot00000000000000# 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 linux-application-whitelisting-fapolicyd-e086a8a/rules.d/30-patterns.rules000066400000000000000000000003411520336644600267670ustar00rootroot00000000000000# 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 linux-application-whitelisting-fapolicyd-e086a8a/rules.d/40-bad-elf.rules000066400000000000000000000001451520336644600264240ustar00rootroot00000000000000# Do not allow malformed ELF even if trusted deny_audit perm=any all : ftype=application/x-bad-elf linux-application-whitelisting-fapolicyd-e086a8a/rules.d/41-shared-obj.rules000066400000000000000000000003701520336644600271510ustar00rootroot00000000000000# 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 linux-application-whitelisting-fapolicyd-e086a8a/rules.d/42-trusted-elf.rules000066400000000000000000000001071520336644600273700ustar00rootroot00000000000000# Allow trusted programs to execute allow perm=execute all : trust=1 linux-application-whitelisting-fapolicyd-e086a8a/rules.d/43-known-elf.rules000066400000000000000000000006171520336644600270410ustar00rootroot00000000000000# 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 linux-application-whitelisting-fapolicyd-e086a8a/rules.d/70-trusted-lang.rules000066400000000000000000000002171520336644600275460ustar00rootroot00000000000000# Allow any program to open trusted language files allow perm=open all : ftype=%languages trust=1 deny_audit perm=any all : ftype=%languages linux-application-whitelisting-fapolicyd-e086a8a/rules.d/71-known-python.rules000066400000000000000000000004211520336644600276060ustar00rootroot00000000000000# 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 linux-application-whitelisting-fapolicyd-e086a8a/rules.d/72-shell.rules000066400000000000000000000001401520336644600262410ustar00rootroot00000000000000# Allow all shell script execution and sourcing allow perm=any all : ftype=text/x-shellscript linux-application-whitelisting-fapolicyd-e086a8a/rules.d/73-known-perl.rules000066400000000000000000000001741520336644600272360ustar00rootroot00000000000000# Only allow system perl files allow perm=any all : ftype=text/x-perl trust=1 deny_audit perm=any all : ftype=text/x-perl linux-application-whitelisting-fapolicyd-e086a8a/rules.d/74-known-ocaml.rules000066400000000000000000000002371520336644600273700ustar00rootroot00000000000000# 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 linux-application-whitelisting-fapolicyd-e086a8a/rules.d/75-known-php.rules000066400000000000000000000001711520336644600270620ustar00rootroot00000000000000# Only allow system PHP files allow perm=any all : ftype=text/x-php trust=1 deny_audit perm=any all : ftype=text/x-php linux-application-whitelisting-fapolicyd-e086a8a/rules.d/76-known-ruby.rules000066400000000000000000000001741520336644600272600ustar00rootroot00000000000000# Only allow system ruby files allow perm=any all : ftype=text/x-ruby trust=1 deny_audit perm=any all : ftype=text/x-ruby linux-application-whitelisting-fapolicyd-e086a8a/rules.d/77-known-lua.rules000066400000000000000000000001711520336644600270560ustar00rootroot00000000000000# Only allow system lua files allow perm=any all : ftype=text/x-lua trust=1 deny_audit perm=any all : ftype=text/x-lua linux-application-whitelisting-fapolicyd-e086a8a/rules.d/78-known-wine.rules000066400000000000000000000002771520336644600272470ustar00rootroot00000000000000# 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 linux-application-whitelisting-fapolicyd-e086a8a/rules.d/90-deny-execute.rules000066400000000000000000000001141520336644600275320ustar00rootroot00000000000000# Deny execution for anything untrusted deny_audit perm=execute all : all linux-application-whitelisting-fapolicyd-e086a8a/rules.d/91-deny-lang.rules000066400000000000000000000001321520336644600270120ustar00rootroot00000000000000# Deny all languages not explicitly enabled deny_audit perm=open all : ftype=%languages linux-application-whitelisting-fapolicyd-e086a8a/rules.d/95-allow-open.rules000066400000000000000000000001051520336644600272150ustar00rootroot00000000000000# Allow everything else to open any file allow perm=open all : all linux-application-whitelisting-fapolicyd-e086a8a/rules.d/Makefile.am000066400000000000000000000025731520336644600257000ustar00rootroot00000000000000# 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-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) linux-application-whitelisting-fapolicyd-e086a8a/rules.d/README-rules000066400000000000000000000027221520336644600256500ustar00rootroot00000000000000This 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 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 linux-application-whitelisting-fapolicyd-e086a8a/src/000077500000000000000000000000001520336644600230505ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/src/Makefile.am000066400000000000000000000067661520336644600251230ustar00rootroot00000000000000SUBDIRS = 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 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 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/daemon-config.c \ library/daemon-config.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 bin_PROGRAMS = fapolicyd-rpm-loader fapolicyd_rpm_loader_SOURCES = \ handler/fapolicyd-rpm-loader.c fapolicyd_CFLAGS += -DBINARYDIR='"$(bindir)"' 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 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/decision-defer.c \ daemon/decision-defer.h \ daemon/fanotify-fs-error.c \ daemon/fanotify-fs-error.h \ daemon/fapolicyd.c \ daemon/mounts.c \ daemon/mounts.h \ daemon/notify.c \ daemon/notify.h \ daemon/state-report.c \ daemon/state-report.h fapolicyd_cli_SOURCES = \ 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 linux-application-whitelisting-fapolicyd-e086a8a/src/cli/000077500000000000000000000000001520336644600236175ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/src/cli/fapolicyd-cli.c000066400000000000000000001042001520336644600264770ustar00rootroot00000000000000/* * 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 "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" 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" "--check-watch_fs Check watch_fs against currently mounted file systems\n" "--check-ignore_mounts [path] Scan ignored mounts for executable 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" "-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 }, {"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'}, {"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) { free_daemon_config(&config); memset(&config, 0, sizeof(config)); } typedef enum _reload_code { DB, RULES} 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; 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; } mdb_env_set_maxdbs(env, 2); rc = mdb_env_open(env, DB_DIR, MDB_RDONLY|MDB_NOLOCK, 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; } rc = mdb_dbi_open(txn, DB_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_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 txn_abort; } rc = mdb_cursor_get(cursor, &key, &val, MDB_FIRST); if (rc) { fprintf(stderr, "mdb_cursor_get failed, error %d %s\n", rc, mdb_strerror(rc)); exit_rc = CLI_EXIT_DB_ERROR; goto txn_abort; } do { char *path = NULL, *data = NULL, sha[FILE_DIGEST_STRING_MAX]; unsigned int tsource; 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 %zu %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; mdb_cursor_close(cursor); 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) { 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 (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(*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; 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; } } ssize_t ret = 0; char str[32] = {0}; if (code == DB) { snprintf(str, 32, "%c\n", RELOAD_TRUSTDB_COMMAND); ret = write(fd, "1\n", strlen(str)); } else if (code == RULES) { snprintf(str, 32, "%c\n", RELOAD_RULES_COMMAND); ret = write(fd, "3\n", strlen(str)); } if (ret == -1) { fprintf(stderr,"Write: %s -> %s\n", fifo_path, strerror(errno)); 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; 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 { if (fd_fgets_r(st, buf, sizeof(buf), fd)) { 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); 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 (found == 0) printf("Nothing appears missing\n"); if (alloc_err) return CLI_EXIT_INTERNAL; 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 found = 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); int rc = walk_database_start(&config); reset_config(); if (rc) return CLI_EXIT_DB_ERROR; do { unsigned int tsource; off_t size; char sha[FILE_DIGEST_STRING_MAX]; char path[448]; char data[TRUSTDB_DATA_BUFSZ]; // Get the entry and format it for use. walkdb_entry_t *entry = walk_database_get_entry(); snprintf(path, sizeof(path), "%.*s", (int) entry->path.mv_size, (char *) entry->path.mv_data); snprintf(data, sizeof(data), "%.*s", (int) entry->data.mv_size, (char *) entry->data.mv_data); if (sscanf(data, DATA_FORMAT_IN, &tsource, &size, sha) != 3) { fprintf(stderr, "%s data entry is corrupted\n", path); continue; } if (verify_file(path, size, sha, tsource)) found = 1; } while (walk_database_next()); walk_database_finish(); if (found == 0) puts("No problems found"); return CLI_EXIT_SUCCESS; } 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 check_file(const char *fpath, const struct stat *sb, int typeflag_unused __attribute__ ((unused)), struct FTW *s_unused __attribute__ ((unused))) { int ret = FTW_CONTINUE; if (S_ISREG(sb->st_mode) == 0) return ret; int fd = open(fpath, O_RDONLY|O_CLOEXEC); if (fd >= 0) { struct file_info info; info.size = sb->st_size; if (check_trust_database(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; 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); init_database(&config); char *path = strdup(env_path); ptr = strtok_r(path, ":", &saved); while (ptr) { if (is_link(ptr)) goto next; nftw(ptr, check_file, 1024, FTW_PHYS); next: ptr = strtok_r(NULL, ":", &saved); } stop = 1; // Need this to terminate update thread free(path); close_database(); reset_config(); if (path_found == 0) puts("No problems found"); 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'; } /* * 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]; if (fd_fgets_r(st, pid_buf, sizeof(pid_buf), pidfd)) { 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; } 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); do { char buf[80]; if (fd_fgets_r(st, buf, sizeof(buf), rpt_fd)) write(1, buf, strlen(buf)); } while (!fd_fgets_eof_r(st)); 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) unlink(TIMING_REPORT); 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 int main(int argc, char * const argv[]) { int opt, option_index, rc = CLI_EXIT_GENERIC; int orig_argc = argc, arg_count = 0; char *args[orig_argc+1]; for (int i = 0; i < orig_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; 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: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; rc = do_delete_db(); break; case 'D': if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; rc = do_dump_db(); break; case 'f': if (lint_rules) goto args_err; if (arg_count > 7) goto args_err; // 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': if (lint_rules) goto args_err; if (arg_count > 3) goto args_err; rc = do_ftype(optarg); break; case 'l': if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; rc = do_list(); break; case 'u': if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; rc = do_reload(DB); break; case 'r': if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; 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; 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; return check_watch_fs(); break; case 3: // --check-trustdb if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; return check_trustdb(); break; case 4: // --check-status if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; 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; return check_path(); break; case 14: // --check-metrics if (lint_rules) goto args_err; if (arg_count > 2) goto args_err; return do_status_report(REPORT_INTENT_METRICS, 0); 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; 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; 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; 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; 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; 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; 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/cli/file-cli.c000066400000000000000000000150151520336644600254510ustar00rootroot00000000000000/* * 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) 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); return CLI_EXIT_USAGE; } char *dest = fname ? fapolicyd_strcat(TRUST_DIR_PATH, fname) : TRUST_FILE_PATH; if (dest == NULL) 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/cli/file-cli.h000066400000000000000000000070111520336644600254530ustar00rootroot00000000000000/* * 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 linux-application-whitelisting-fapolicyd-e086a8a/src/cli/ignore-mounts.c000066400000000000000000000314431520336644600265760ustar00rootroot00000000000000/* * 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 "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; struct mount_scan_state { const avl_tree_t *languages; unsigned long *count; int had_error; }; static struct mount_scan_state scan_state; /* * 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; } 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 and 1 on allocation failure. */ static int insert_language_mime(avl_tree_t *languages, const char *mime) { struct language_entry *entry; avl_t *ret; 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 = (struct language_entry *)languages->root; avl_remove(languages, &entry->avl); free(entry->mime); free(entry); } } /* * 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. * Returns 0 on success and 1 on failure. */ static int load_language_mimes(avl_tree_t *languages, const char **source_path) { FILE *fp; char *line = NULL; size_t len = 0; int rc = 1, found = 0; *source_path = LANGUAGE_RULES_FILE; fp = fopen(*source_path, "rm"); if (fp == NULL) { *source_path = RULES_FILE; fp = fopen(*source_path, "rm"); if (fp == NULL) return 1; } while (getline(&line, &len, fp) != -1) { char *entry = fapolicyd_strtrim(line); if (strncmp(entry, "%languages=", 11) == 0) { char *value = entry + 11; char *tmp = strdup(value); char *ptr, *saved; if (tmp == NULL) goto done; ptr = strtok_r(tmp, ",", &saved); while (ptr) { char *mime = fapolicyd_strtrim(ptr); if (*mime) { if (insert_language_mime(languages, mime)) { free(tmp); free_language_mimes(languages); goto done; } } ptr = strtok_r(NULL, ",", &saved); } free(tmp); found = 1; break; } } if (found) rc = 0; done: free(line); fclose(fp); return rc; } /* * 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_file - nftw callback that records suspicious files. * @fpath: path of the file being inspected. * @sb: stat buffer describing the file. * @typeflag_unused: unused nftw type flag. * @ftwbuf_unused: unused nftw traversal metadata. * Returns FTW_CONTINUE so the walk keeps running. */ static int inspect_mount_file(const char *fpath, const struct stat *sb, int typeflag_unused __attribute__ ((unused)), struct FTW *ftwbuf_unused __attribute__ ((unused))) { int fd; struct file_info info; char buf[128]; char *mime; /* Only evaluate regular files discovered during the walk. */ if (S_ISREG(sb->st_mode) == 0) return FTW_CONTINUE; /* Open the file and collect metadata for libmagic. */ fd = open(fpath, O_RDONLY|O_CLOEXEC); if (fd < 0) { fprintf(stderr, "Unable to open %s (%s)\n", fpath, strerror(errno)); scan_state.had_error = 1; return FTW_CONTINUE; } memset(&info, 0, sizeof(info)); info.device = sb->st_dev; info.inode = sb->st_ino; info.mode = sb->st_mode; info.size = sb->st_size; info.time = sb->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; } /* Look up the MIME in the %languages tree and report matches. */ struct language_entry key = { .mime = buf, }; if (avl_search(scan_state.languages, &key.avl)) { if (verbose) printf("%s: %s\n", fpath, buf); if (scan_state.count) (*scan_state.count)++; } return FTW_CONTINUE; } /* * scan_mount_entry - scan a single ignore_mounts entry for suspicious files. * @mount: entry from config.ignore_mounts. * @suspicious_total: aggregate counter 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, unsigned long *suspicious_total, int override) { char resolved[PATH_MAX]; char *rpath; unsigned long mount_count = 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)); printf("Summary for %s: 0 suspicious file(s) (scan skipped)\n", mount); return CLI_EXIT_PATH_CONFIG; } if (stat(rpath, &sb)) { fprintf(stderr, "%s does not exist\n", rpath); printf("Summary for %s: 0 suspicious file(s) (scan skipped)\n", rpath); return CLI_EXIT_PATH_CONFIG; } if (S_ISDIR(sb.st_mode) == 0) { fprintf(stderr, "%s is not a directory\n", rpath); printf("Summary for %s: 0 suspicious file(s) (scan skipped)\n", 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.count = &mount_count; scan_state.had_error = 0; if (nftw(rpath, inspect_mount_file, 1024, FTW_PHYS)) { fprintf(stderr, "Unable to scan %s (%s)\n", rpath, strerror(errno)); printf("Summary for %s: 0 suspicious file(s) (scan skipped)\n", rpath); rc = CLI_EXIT_IO; } else scanned = 1; if (scan_state.had_error) rc = CLI_EXIT_IO; if (scanned) { printf("Summary for %s: %lu suspicious file(s)\n", rpath, mount_count); *suspicious_total += mount_count; } scan_state.count = 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 suspicious files are found, CLI_EXIT_GENERIC * when suspicious files 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; unsigned long suspicious_total = 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 suspicious MIME matches. */ for (list_item_t *lptr = mounts.first; lptr; lptr = lptr->next) { int scan_rc = scan_mount_entry(lptr->index, &suspicious_total, override ? 1 : 0); if (scan_rc) { errors = 1; if (rc == CLI_EXIT_SUCCESS) rc = scan_rc; } } if (errors == 0 && suspicious_total == 0) rc = CLI_EXIT_SUCCESS; finish: if (file_ready) file_close(); list_empty(&mounts); free_language_mimes(&languages); scan_state.languages = NULL; scan_state.count = NULL; scan_state.had_error = 0; reset_ignore_mounts_config(); if (suspicious_total > 0) return CLI_EXIT_GENERIC; if (errors) return rc; return rc; } linux-application-whitelisting-fapolicyd-e086a8a/src/cli/ignore-mounts.h000066400000000000000000000007061520336644600266010ustar00rootroot00000000000000/* * 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 linux-application-whitelisting-fapolicyd-e086a8a/src/cli/rule-lint.c000066400000000000000000000162361520336644600257060ustar00rootroot00000000000000/* * 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 "rules.h" #include "rule-lint.h" /* * 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; } /* * lint_rules_policy - emit policy-shape warnings for default-allow gaps. * @rules: parsed rule list to inspect. * 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 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_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 (!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\n"); warnings = 1; } if (!has_languages) { fprintf(stderr, "Policy lint warning: %%languages is not " "defined; programmatic ftype coverage cannot be checked\n"); warnings = 1; } else if (!first_lang_deny) { fprintf(stderr, "Policy lint warning: programmatic opens can " "fall through; no broad %%languages open deny found\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 on rule " "%u can shadow programmatic-content denies\n", first_open_allow->num + 1); warnings = 1; } if (warnings == 0) printf("Policy lint found no warnings\n"); return warnings ? CLI_EXIT_GENERIC : CLI_EXIT_SUCCESS; } /* * default_rules_path - select the rules file used when no path is supplied. * @path: selected path is returned here. * Returns CLI_EXIT_SUCCESS when a single candidate was selected. */ static int default_rules_path(const char **path) { int old_rules = access(OLD_RULES_FILE, F_OK) == 0; int compiled_rules = access(RULES_FILE, F_OK) == 0; if (old_rules && compiled_rules) { fprintf(stderr, "Error - old and new rules file detected. " "Delete one or the other.\n"); return CLI_EXIT_PATH_CONFIG; } if (old_rules) *path = OLD_RULES_FILE; else *path = RULES_FILE; 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; unsigned int cnt; set_message_mode(MSG_STDERR, DBG_NO); if (path == NULL) { rc = default_rules_path(&path); if (rc) return rc; } f = fopen(path, "r"); if (f == NULL) { fprintf(stderr, "Cannot open rules file %s (%s)\n", path, strerror(errno)); return CLI_EXIT_IO; } 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); } else rc = CLI_EXIT_SUCCESS; rules_clear(&temp_rules); return rc; } linux-application-whitelisting-fapolicyd-e086a8a/src/cli/rule-lint.h000066400000000000000000000007151520336644600257060ustar00rootroot00000000000000/* * 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 linux-application-whitelisting-fapolicyd-e086a8a/src/daemon/000077500000000000000000000000001520336644600243135ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/src/daemon/decision-defer.c000066400000000000000000000332431520336644600273440ustar00rootroot00000000000000/* * 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 * thread 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 thread 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 thread owns this array. No producer writes to it, and no other * thread pops from it. That keeps the implementation simple and makes 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 "gcc-attributes.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; }; static void format_age(uint64_t age_ns, char *buf, size_t buf_size) __attr_access ((__write_only__, 2, 3)); /* * 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; if (defer == NULL) { errno = EINVAL; return -1; } memset(defer, 0, sizeof(*defer)); 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) 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; free(defer->entries); 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. * * 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; if (defer == NULL || event == NULL) { errno = EINVAL; return -1; } if (defer->current == defer->capacity) { errno = ENOSPC; return -1; } 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; return 0; } errno = ENOSPC; return -1; } /* * 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) { return pop_entry(defer, oldest_entry(defer, slot), event); } /* * 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; if (defer == NULL || match == NULL || defer->current == 0) return 0; 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; } return pop_entry(defer, oldest, event); } /* * 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) { return pop_entry(defer, oldest_entry(defer, DECISION_EVENT_NO_SLOT), event); } /* * 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) defer->fallbacks++; } /* * 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; } /* * format_age - convert a nanosecond age into compact human-readable text. * @age_ns: age in nanoseconds. * @buf: destination buffer. * @buf_size: destination size. * Returns nothing. */ static void format_age(uint64_t age_ns, char *buf, size_t buf_size) { if (buf == NULL || buf_size == 0) return; if (age_ns == 0) snprintf(buf, buf_size, "0ns"); else if (age_ns < 1000ULL) snprintf(buf, buf_size, "%lluns", (unsigned long long)age_ns); else if (age_ns < 1000000ULL) snprintf(buf, buf_size, "%.3fus", (double)age_ns / 1000.0); else if (age_ns < 1000000000ULL) snprintf(buf, buf_size, "%.3fms", (double)age_ns / 1000000.0); else snprintf(buf, buf_size, "%.3fs", (double)age_ns / 1000000000.0); } /* * 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) 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; } } /* * 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, "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; format_age(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); } linux-application-whitelisting-fapolicyd-e086a8a/src/daemon/decision-defer.h000066400000000000000000000045051520336644600273500ustar00rootroot00000000000000/* * 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 "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 { 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); 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); #endif linux-application-whitelisting-fapolicyd-e086a8a/src/daemon/fanotify-fs-error.c000066400000000000000000000436651520336644600300510ustar00rootroot00000000000000/* * 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) { 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); } } linux-application-whitelisting-fapolicyd-e086a8a/src/daemon/fanotify-fs-error.h000066400000000000000000000016171520336644600300450ustar00rootroot00000000000000/* * 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); #endif linux-application-whitelisting-fapolicyd-e086a8a/src/daemon/fapolicyd.c000066400000000000000000001057331520336644600264420ustar00rootroot00000000000000/* * fapolicyd.c - Main file for the program * Copyright (c) 2016,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 */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include /* umask */ #include #include #include #include /* PATH_MAX */ #include #include #ifndef HAVE_GETTID #include #endif #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-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" // Global program variables unsigned int debug_mode = 0; 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; // Reconfiguration static atomic_bool reconfig_running = false; // Mount handling static atomic_bool mounts_running = false; static void usage(void) NORETURN; #ifndef HAVE_GETTID pid_t gettid(void) { return syscall(SYS_gettid); } #endif 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; #ifndef USE_RPM 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 nothing. Exits on failure when the list is missing. */ static void init_fs_list(const char *watch_fs) { if (watch_fs == NULL) { msg(LOG_ERR, "File systems to watch is empty"); exit(1); } avl_init(&filesystems.index, cmp_fs); // Now parse up list and push into avl char *ptr, *saved, *tmp = strdup(watch_fs); if (tmp == NULL) { msg(LOG_ERR, "Cannot duplicate watch_fs list"); return; } ptr = strtok_r(tmp, ",", &saved); while (ptr) { new_filesystem(&filesystems, ptr); ptr = strtok_r(NULL, ",", &saved); } free(tmp); } static void term_handler(int sig __attribute__((unused))) { stop = true; nudge_queue(); } static void coredump_handler(int sig) { if (getpid() == gettid()) { unmark_fanotify(m); unlink_fifo(); signal(sig, SIG_DFL); kill(getpid(), sig); } else { /* * Fatal signals are usually delivered to the thread generating * them, if this is not main thread, raised the signal again to * handle it there, then wait forever to die. */ kill(getpid(), sig); for (;;) pause(); } } 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; } __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 thread restore timing-owned queue metrics. if (new_config.timing_collection == TIMING_COLLECTION_OFF) nudge_queue(); } if (new_config.integrity != config.integrity) { set_integrity_mode(new_config.integrity); config.integrity = new_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) { 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, 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 thread'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 (reload_configuration()) msg(LOG_WARNING, "Continuing with previous configuration settings"); 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"); 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 before * taking the rule lock, builds a complete policy snapshot while the current * policy remains active, validates rule parsing and syslog format parsing, * and publishes the new snapshot only after full success. 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 static int write_pid_file(void) { int pidfd, len; char val[16]; len = snprintf(val, sizeof(val), "%u\n", getpid()); if (len <= 0) { msg(LOG_ERR, "Pid error (%s)", strerror(errno)); return 1; } pidfd = open(pidfile, O_CREAT | O_TRUNC | O_NOFOLLOW | O_WRONLY, 0644); if (pidfd < 0) { msg(LOG_ERR, "Unable to create pidfile (%s)", strerror(errno)); return 1; } if (write(pidfd, val, (unsigned int)len) != len) { msg(LOG_ERR, "Unable to write pidfile (%s)", strerror(errno)); close(pidfd); return 1; } close(pidfd); return 0; } 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'; field++; break; } // terminate the field and increment s[len] = '\0'; s += len + 1; field++; } return (*point && *type) ? 0 : 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 no value. */ static void handle_mounts(int fd) { char buf[PATH_MAX * 2]; char *point, *type; pthread_mutex_lock(&mlist_lock); if (m == NULL) { m = malloc(sizeof(mlist)); mlist_create(m); } // Rewind the descriptor lseek(fd, 0, SEEK_SET); fd_fgets_state_t *st = fd_fgets_init(); if (!st) { pthread_mutex_unlock(&mlist_lock); return; } mlist_mark_all_deleted(m); do { int 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; } unescape_shell(point, strlen(point)); // Is this one that we care about? if (check_mount_entry(point, type)) { // Can we find it in the old list? if (mlist_find(m, point)) { // Mark no change m->cur->status = MNT_NO_CHANGE; } else mlist_append(m, point); } } else if (rc < 0) // Some kind of error - stop break; } while (!fd_fgets_eof_r(st)); fd_fgets_destroy(st); // update marks fanotify_update(m); pthread_mutex_unlock(&mlist_lock); } /* * 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); handle_mounts(fd); 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 usage(void) { fprintf(stderr, "Usage: fapolicyd [--debug|--debug-deny] [--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 char *ptr = lookup_integrity(config.integrity); 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; strategy = __atomic_load_n(&config.reset_strategy, __ATOMIC_RELAXED); reset_ptr = lookup_reset_strategy(strategy); failure_action_snapshot(&failures, 0); getDecisionMetrics(&decisions); mode.permissive = __atomic_load_n(&config.permissive, __ATOMIC_RELAXED); mode.integrity = ptr; mode.reset_strategy = reset_ptr; mode.ruleset_generation = decisions.ruleset_generation; 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, "CPU cores: %ld\n", sysconf(_SC_NPROCESSORS_ONLN)); fprintf(f, "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); 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_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); if (shutdown) 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); 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); } /* * 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); if (!shutdown) fputs("\n", f); do_metrics_report_reset(f, reset); if (shutdown) fputs("\n", f); } int already_running(void) { fd_fgets_state_t *st = fd_fgets_init(); if (!st) return 1; int pidfd = open(pidfile, O_RDONLY); if (pidfd >= 0) { char pid_buf[16]; if (fd_fgets_r(st, pid_buf, sizeof(pid_buf), pidfd)) { int pid; char exe_buf[80], my_path[80]; // Get our path if (get_program_from_pid(getpid(), sizeof(exe_buf), my_path) == NULL) goto err_out; // shouldn't happen, but be safe // convert pidfile to integer errno = 0; pid = strtoul(pid_buf, NULL, 10); if (errno) goto err_out; // shouldn't happen, but be safe // verify it really is fapolicyd if (get_program_from_pid(pid, sizeof(exe_buf), exe_buf) == NULL) goto good; //if pid doesn't exist, we're OK // If the path doesn't have fapolicyd in it, we're OK if (strstr(exe_buf, "fapolicyd") == NULL) goto good; if (strcmp(exe_buf, my_path) == 0) goto err_out; // if the same, we need to exit // one last sanity check in case path is unexpected // for example: /sbin/fapolicyd & /home/test/fapolicyd if (pid != getpid()) goto err_out; good: fd_fgets_destroy(st); close(pidfd); unlink(pidfile); return 0; } else msg(LOG_ERR, "fapolicyd pid file found but unreadable"); err_out: // At this point, we have a pid file, let's just assume it's alive // because if 2 are running, it deadlocks the machine fd_fgets_destroy(st); close(pidfd); return 1; } fd_fgets_destroy(st); return 0; // pid file doesn't exist, we're good to go } int main(int argc, const char *argv[]) { struct pollfd pfd[3]; struct sigaction sa; struct rlimit limit; nfds_t pfd_count = 2; 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], "--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) { // nop; debug flags already set } else { msg(LOG_ERR, "unknown command option:%s\n", argv[i]); free_daemon_config(&config); usage(); } } if (already_running()) { msg(LOG_ERR, "fapolicyd is already running"); exit(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 >= 16384) limit.rlim_cur = limit.rlim_max; else limit.rlim_max = limit.rlim_cur = 16384; 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)) exit(1); if (!debug_mode) { if (become_daemon() < 0) { msg(LOG_ERR, "Exiting due to failure daemonizing"); exit(1); } set_message_mode(MSG_SYSLOG, DBG_NO); openlog("fapolicyd", LOG_PID, LOG_DAEMON); } 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)) { msg(LOG_ERR, "Cannot set exit function"); exit(1); } // Setup filesystem to watch list init_ignore_mounts(config.ignore_mounts); init_fs_list(config.watch_fs); // Write the pid file for the init system write_pid_file(); // Set strict umask (void) umask( 0117 ); if (preconstruct_fifo(&config)) { unlink(pidfile); 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(); // 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); unlink(pidfile); 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); unlink(pidfile); 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); unlink(pidfile); 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); unlink(pidfile); exit(1); } pfd[0].events = POLLPRI; handle_mounts(pfd[0].fd); pfd[1].fd = init_fanotify(&config, m); pfd[1].events = POLLIN; #ifdef FAPOLICYD_ENABLE_FANOTIFY_FS_ERROR pfd[2].fd = fanotify_fs_error_fd(); if (pfd[2].fd >= 0) { pfd[2].events = POLLIN; pfd_count = 3; } #endif msg(LOG_INFO, "Starting to listen for events"); while (!stop) { int rc; if (hup) { hup = false; msg(LOG_DEBUG, "Got SIGHUP"); maybe_start_reconfigure_thread(); } rc = poll(pfd, pfd_count, -1); #ifdef DEBUG msg(LOG_DEBUG, "Main poll interrupted"); #endif if (rc < 0) { if (errno == EINTR) continue; else { stop = true; nudge_queue(); close_database(); msg(LOG_ERR, "Poll error (%s)\n", strerror(errno)); exit(1); } } else if (rc > 0) { if (pfd[1].revents & POLLIN) { handle_events(); } if (pfd_count > 2 && pfd[2].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 } } msg(LOG_INFO, "shutting down..."); wait_for_mounts_thread(); shutdown_fanotify(m); close(pfd[0].fd); file_close(); close_database(); #ifdef HAVE_MALLINFO2 close_memory_report(); #endif unlink(pidfile); // 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); free_daemon_config(&config); return 0; } linux-application-whitelisting-fapolicyd-e086a8a/src/daemon/mounts.c000066400000000000000000000051771520336644600260160ustar00rootroot00000000000000/* * 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 (p) { newnode = malloc(sizeof(mnode)); if (newnode == NULL) return 1; newnode->path = strdup(p); 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; } 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/daemon/mounts.h000066400000000000000000000026721520336644600260200ustar00rootroot00000000000000/* * 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_clear(mlist *m); #endif linux-application-whitelisting-fapolicyd-e086a8a/src/daemon/notify.c000066400000000000000000000744741520336644600260070ustar00rootroot00000000000000/* * notify.c - functions handle recieving and enqueuing events * 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 */ #include "config.h" /* Needed to get O_LARGEFILE definition */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "attr-lookup-metrics.h" #include "conf.h" #include "decision-defer.h" #include "decision-timing.h" #include "failure-action.h" #include "fanotify-fs-error.h" #include "policy.h" #include "event.h" #include "escape.h" #include "message.h" #include "queue.h" #include "mounts.h" #include "state-report.h" #define FANOTIFY_BUFFER_SIZE 8192 #define KERNEL_OVERFLOW_LOG_INTERVAL 60 #define DEFER_RECHECK_INTERVAL_SEC 1 // External variables extern atomic_bool stop, run_stats; extern conf_t config; // Local variables static pid_t our_pid; static struct queue *q = NULL; static struct queue_metrics last_queue_metrics; static struct decision_defer_queue defer_queue; static struct decision_defer_metrics last_defer_metrics; static pthread_t decision_thread; static pthread_t deadmans_switch_thread; static atomic_bool alive = true; static int fd = -1; static int rpt_timer_fd = -1; static uint64_t mask; static unsigned int mark_flag; static unsigned int rpt_interval; static struct message_rate_limit kernel_queue_overflow_log = MESSAGE_RATE_LIMIT_INIT(KERNEL_OVERFLOW_LOG_INTERVAL); // Local functions static void *decision_thread_main(void *arg); static void *deadmans_switch_thread_main(void *arg); static void dispatch_decision_event(decision_event_t *event, int *rpt_is_stale); static void fanotify_failure_action(failure_reason_t reason); static unsigned int release_ready_deferred_events(int *rpt_is_stale); static unsigned int shutdown_deferred_events(void); static unsigned int shutdown_queued_events(void); static unsigned int timing_queue_depth_reset(void *ctx); static unsigned int timing_queue_depth_restore(void *ctx, unsigned int saved); void fanotify_queue_report_reset(FILE *f, int reset); void nudge_queue(void); /* * 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; } /* * 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(*list) && *list != ',') return 1; list++; } return 0; } int init_fanotify(const conf_t *conf, mlist *m) { const char *path; int ignore_mounts_enabled; // Get inter-thread queue ready q = q_open(conf->q_size); if (q == NULL) { msg(LOG_ERR, "Failed setting up queue (%s)", strerror(errno)); exit(1); } q_metrics_snapshot(q, &last_queue_metrics); if (decision_defer_init(&defer_queue, conf->subj_cache_size)) { msg(LOG_ERR, "Failed setting up subject defer array (%s)", strerror(errno)); q_close(q); q = NULL; exit(1); } decision_defer_metrics_snapshot_reset(&defer_queue, &last_defer_metrics, 0); decision_timing_set_queue_depth_hooks(timing_queue_depth_reset, timing_queue_depth_restore, q); 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_defer_destroy(&defer_queue); q_close(q); q = NULL; exit(1); } // Start decision thread so its ready when first event comes rpt_interval = conf->report_interval; int rc = pthread_create(&decision_thread, NULL, decision_thread_main, NULL); if (rc) { msg(LOG_ERR, "Failed to create decision thread (%s)", strerror(rc)); close(fd); decision_defer_destroy(&defer_queue); q_close(q); exit(1); } rc = pthread_create(&deadmans_switch_thread, NULL, deadmans_switch_thread_main, NULL); if (rc) { msg(LOG_ERR, "Failed to create deadman's switch thread (%s)", strerror(rc)); atomic_store(&stop, true); q_shutdown(q); pthread_join(decision_thread, NULL); if (rpt_timer_fd != -1) close(rpt_timer_fd); close(fd); decision_defer_destroy(&defer_queue); q_close(q); 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; } 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 } void unmark_fanotify(mlist *m) { if (m == NULL) return; const char *path = mlist_first(m); // Stop the flow of events while (path) { char *escaped_path = NULL; const char *safe_path = escape_path_for_log(path, &escaped_path); if (fanotify_mark(fd, FAN_MARK_FLUSH | mark_flag, 0, -1, path) == -1) msg(LOG_ERR, "Failed flushing path %s (%s)", safe_path, strerror(errno)); fanotify_fs_error_unmark(path); free(escaped_path); path = mlist_next(m); } } void shutdown_fanotify(mlist *m) { unmark_fanotify(m); // End the thread q_shutdown(q); pthread_join(decision_thread, NULL); pthread_join(deadmans_switch_thread, NULL); // Clean up q_metrics_snapshot(q, &last_queue_metrics); decision_defer_metrics_snapshot_reset(&defer_queue, &last_defer_metrics, 0); decision_timing_set_queue_depth_hooks(NULL, NULL, NULL); decision_defer_destroy(&defer_queue); q_close(q); q = NULL; close(rpt_timer_fd); fanotify_fs_error_close(); close(fd); // Report results msg(LOG_DEBUG, "Allowed accesses: %lu", getAllowed()); msg(LOG_DEBUG, "Denied accesses: %lu", getDenied()); } void nudge_queue(void) { q_shutdown(q); } /* * timing_queue_depth_reset - reset timing run max queue depth. * @ctx: queue pointer. * Returns the max depth value saved before reset. */ static unsigned int timing_queue_depth_reset(void *ctx) { return q_max_depth_snapshot_reset(ctx); } /* * timing_queue_depth_restore - snapshot timing run queue depth and restore. * @ctx: queue pointer. * @saved: max depth value saved before timing reset. * Returns the max depth observed during the timing run. */ static unsigned int timing_queue_depth_restore(void *ctx, unsigned int saved) { return q_max_depth_snapshot_restore(ctx, saved); } /* * fanotify_queue_report - write fanotify queue metrics. * @f: output stream. * Returns nothing. */ void fanotify_queue_report(FILE *f) { fanotify_queue_report_reset(f, 0); } /* * 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) { if (f == NULL) return; if (q) { struct queue_metrics metrics; struct decision_defer_metrics defer_metrics; q_metrics_snapshot_reset(q, &metrics, reset); q_metrics_report(f, &metrics); decision_defer_metrics_snapshot_reset(&defer_queue, &defer_metrics, reset); decision_defer_metrics_report(f, &defer_metrics); } else { q_metrics_report(f, &last_queue_metrics); decision_defer_metrics_report(f, &last_defer_metrics); } } /* * fanotify_defer_config_report - write defer capacity sized at startup. * @f: report stream. * Returns nothing. */ void fanotify_defer_config_report(FILE *f) { struct decision_defer_metrics metrics; if (f == NULL) return; if (q) decision_defer_metrics_snapshot_reset(&defer_queue, &metrics, 0); else metrics = last_defer_metrics; decision_defer_config_report(f, &metrics); } /* * fanotify_defer_fallback_report - write defer fallback health indicator. * @f: report stream. * Returns nothing. */ void fanotify_defer_fallback_report(FILE *f) { struct decision_defer_metrics metrics; if (f == NULL) return; if (q) decision_defer_metrics_snapshot_reset(&defer_queue, &metrics, 0); else metrics = last_defer_metrics; decision_defer_fallback_report(f, &metrics); } /* * fanotify_defer_age_report - write oldest deferred event age. * @f: report stream. * Returns nothing. */ void fanotify_defer_age_report(FILE *f) { struct decision_defer_metrics metrics; if (f == NULL) return; if (q) decision_defer_metrics_snapshot_reset(&defer_queue, &metrics, 0); else metrics = last_defer_metrics; decision_defer_age_report(f, &metrics); } /* * fanotify_defer_health_report - write defer health indicators. * @f: report stream. * Returns nothing. */ void fanotify_defer_health_report(FILE *f) { struct decision_defer_metrics metrics; if (f == NULL) return; if (q) decision_defer_metrics_snapshot_reset(&defer_queue, &metrics, 0); else metrics = last_defer_metrics; decision_defer_health_report(f, &metrics); } /* * 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); } static void *deadmans_switch_thread_main(void *arg) { 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); do { // Are you alive decision thread? The idea of triggering // on 5 is that if it's less than 5 it's still alive and // processing, although maybe running behind sometimes. // But if we are over 5, we are losing the battle. if (!atomic_load_explicit(&alive, memory_order_relaxed) && !atomic_load_explicit(&stop, memory_order_relaxed) && q_queue_length(q) > 5) { failure_action_record(FAILURE_REASON_WORKER_STALL); msg(LOG_ERR, "Deadman's switch activated...killing process"); raise(SIGKILL); } // OK, prove it again. atomic_store_explicit(&alive, false, memory_order_relaxed); sleep(3); } while (!stop); return NULL; } // disable interval reports, used on unrecoverable errors static void rpt_disable(const char *why) { rpt_interval = 0; close(rpt_timer_fd); msg(LOG_INFO, "interval reports disabled; %s", why); } // initialize interval reporting static void rpt_init(struct timespec *t) { rpt_timer_fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK); if (rpt_timer_fd == -1) { rpt_disable("timer create failure"); } else { t->tv_nsec = t->tv_sec = 0; struct itimerspec rpt_deadline = { {rpt_interval, 0}, {rpt_interval, 0} }; 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. * @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(decision_event_t *event) { attr_lookup_metrics_set_worker(0); decision_timing_decision_begin(0); decision_timing_queue_dequeued(event->enqueue_ns); make_policy_decision(event, fd, mask); decision_timing_decision_end(); } /* * dispatch_decision_event - route one dequeued event and release defers. * @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(decision_event_t *event, int *rpt_is_stale) { // 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_queue, event) == 0) { *rpt_is_stale = 1; return; } decision_defer_count_fallback(&defer_queue); } 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; atomic_store_explicit(&alive, true, memory_order_relaxed); run_decision_event(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_queue, slot, event)) return; } } /* * 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(int *rpt_is_stale) { decision_event_t event; unsigned int count = 0; while (defer_queue.current && decision_defer_pop_if(&defer_queue, deferred_event_is_ready, NULL, &event)) { dispatch_decision_event(&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 (__atomic_load_n(&config.permissive, __ATOMIC_RELAXED)) return FAN_ALLOW; return FAN_DENY; } /* * shutdown_queued_events - reply to every event left in the input queue. * * The decision thread exits its main loop as soon as stop is observed. Any * permission event that reached the inter-thread 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(void) { decision_event_t event; unsigned int count = 0; int decision = shutdown_fallback_decision(); while (q != NULL && q_queue_length(q) > 0) { if (q_dequeue(q, &event) != 1) break; reply_event(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(void) { decision_event_t event; unsigned int count = 0; int decision = shutdown_fallback_decision(); while (decision_defer_pop_any(&defer_queue, &event)) { reply_event(fd, &event.metadata, decision, NULL); count++; } return count; } #ifdef TEST_SUBJECT_DEFER /* * test_notify_queue_reset - initialize notify.c 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) { if (q != NULL) q_close(q); q = q_open(entries); return q == NULL ? -1 : 0; } /* * test_notify_queue_destroy - release notify.c queue state after unit tests. * Returns nothing. */ void test_notify_queue_destroy(void) { if (q == NULL) return; q_close(q); q = NULL; } /* * test_notify_queue_push - enqueue an event in notify.c 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(q, event); } /* * 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(); } /* * test_notify_defer_reset - initialize notify.c 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) { decision_defer_destroy(&defer_queue); return decision_defer_init(&defer_queue, subj_cache_size); } /* * test_notify_defer_destroy - release notify.c defer state after unit tests. * Returns nothing. */ void test_notify_defer_destroy(void) { decision_defer_destroy(&defer_queue); } /* * test_notify_defer_push - park an event in notify.c 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(&defer_queue, 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(); } #endif static void *decision_thread_main(void *arg) { 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); // interval reporting state int rpt_is_stale = 0; struct timespec rpt_timeout; // if an interval was configured, reports are enabled if (rpt_interval) rpt_init(&rpt_timeout); // start with a fresh report run_stats = 1; while (!stop) { int rc; decision_event_t event; /* * Apply asynchronous timing-control work on the decision * thread. 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 (rpt_interval) { errno = 0; rc = q_timed_dequeue(q, &event, &rpt_timeout); if (rc == 0) { uint64_t expired = 0; // check for timer expirations if (errno == ETIMEDOUT) { 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 || run_stats) { // 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 || run_stats) { state_report_write( state_report_reason_for_triggers( expired)); run_stats = 0; 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; } continue; } if (rc < 0) continue; } else { int timed_for_defer = 0; struct timespec defer_timeout; if (defer_queue.current && clock_gettime(CLOCK_REALTIME, &defer_timeout) == 0) { defer_timeout.tv_sec += DEFER_RECHECK_INTERVAL_SEC; errno = 0; rc = q_timed_dequeue(q, &event, &defer_timeout); timed_for_defer = 1; } else { rc = q_dequeue(q, &event); } if (rc == 0) { if (run_stats) { state_report_write(STATE_REPORT_SIGNAL); run_stats = 0; } if (timed_for_defer && errno == ETIMEDOUT) release_ready_deferred_events( &rpt_is_stale); continue; } if (rc < 0) continue; if (run_stats) { state_report_write(STATE_REPORT_SIGNAL); run_stats = 0; } } atomic_store_explicit(&alive, true, memory_order_relaxed); dispatch_decision_event(&event, &rpt_is_stale); } unsigned int queued = shutdown_queued_events(); unsigned int deferred = shutdown_deferred_events(); if (queued || deferred) msg(LOG_INFO, "Replied to %u queued and %u deferred fanotify events during shutdown", queued, deferred); msg(LOG_DEBUG, "Decision thread shutdown backlog: queued=%u deferred=%u", queued, deferred); msg(LOG_DEBUG, "Exiting decision thread"); return NULL; } /* * 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) { // 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 { decision_event_t event; decision_event_init(&event, metadata); if (q_enqueue(q, &event)) { int decision = FAN_DENY; failure_action_record( FAILURE_REASON_QUEUE_FULL); msg(LOG_ERR, "Failed to enqueue event " "for PID %d: queue is " "full, please consider " "tuning q_size if issue " "happens often", metadata->pid); if (__atomic_load_n( &config.permissive, __ATOMIC_RELAXED)) decision = FAN_ALLOW; reply_event(fd, metadata, decision, NULL); } } } 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); } } linux-application-whitelisting-fapolicyd-e086a8a/src/daemon/notify.h000066400000000000000000000031661520336644600260020ustar00rootroot00000000000000/* * 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_update(mlist *m); void unmark_fanotify(mlist *m); void shutdown_fanotify(mlist *m); void fanotify_queue_report(FILE *f); void fanotify_queue_report_reset(FILE *f, int reset); 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); void nudge_queue(void); #endif linux-application-whitelisting-fapolicyd-e086a8a/src/daemon/state-report.c000066400000000000000000000372541520336644600271230ustar00rootroot00000000000000/* * 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-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_metrics_reset_time(char *buf, size_t buf_size) __attr_access ((__write_only__, 1, 2)); /* * 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"); decision_timing_control_report(f, mode->config); decision_timing_history_report(f); fprintf(f, "Ruleset generation: %u\n", mode->ruleset_generation); } /* * 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_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) { struct tm tm; if (buf == NULL || buf_size == 0) return NULL; if (last_metrics_reset == 0) { strncpy(buf, "never", buf_size - 1); buf[buf_size - 1] = 0; return buf; } if (localtime_r(&last_metrics_reset, &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; } /* * 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; 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); fprintf(f, "Ruleset generation: %u\n", metrics.ruleset_generation); 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); } fprintf(f, "Ruleset generation: %u\n", metrics.ruleset_generation); 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 (strategy == RESET_MANUAL && 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/daemon/state-report.h000066400000000000000000000031531520336644600271170ustar00rootroot00000000000000/* * 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 "failure-action.h" #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 ruleset_generation; 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 linux-application-whitelisting-fapolicyd-e086a8a/src/handler/000077500000000000000000000000001520336644600244655ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/src/handler/fapolicyd-rpm-loader.c000066400000000000000000000067361520336644600306570ustar00rootroot00000000000000/* * fapolicy-rpm-loader.c - loader tool for fapolicyd * Copyright (c) 2025-2025 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[]) { set_message_mode(MSG_STDERR, DBG_YES); 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 */ // 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) { msg(LOG_ERR, "sendmsg failed"); exit(1); } close(sock_fd); // closes the channel; parent gets EOF close(memfd); // parent has its own refcount do_rpm_destroy_backend(); free_daemon_config(&config); return 0; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/000077500000000000000000000000001520336644600245145ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/src/library/attr-lookup-metrics.c000066400000000000000000000174101520336644600306100ustar00rootroot00000000000000/* * 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 "attr-lookup-metrics.h" #define ATTR_LOOKUP_METRICS_MAX_WORKERS 32 struct attr_lookup_counter { atomic_ullong requests; atomic_ullong lookups; }; /* * 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 __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); } /* * 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; } /* * report_subject_attrs - write all subject attribute lookup counters. * @f: output stream. * @reset: non-zero resets counters after copying. * Returns nothing. */ static void report_subject_attrs(FILE *f, int reset) { 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); struct attr_lookup_metric_snapshot snapshot; if (type == ALL_SUBJ) continue; if (attr_lookup_metrics_subject_snapshot(type, &snapshot, reset)) continue; fprintf(f, "Subject attr: %s requests=%llu lookups=%llu\n", name ? name : "unknown", snapshot.requests, snapshot.lookups); } } /* * report_object_attrs - write all object attribute lookup counters. * @f: output stream. * @reset: non-zero resets counters after copying. * Returns nothing. */ static void report_object_attrs(FILE *f, int reset) { 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); struct attr_lookup_metric_snapshot snapshot; if (type == ALL_OBJ) continue; if (attr_lookup_metrics_object_snapshot(type, &snapshot, reset)) continue; fprintf(f, "Object attr: %s requests=%llu lookups=%llu\n", name ? name : "unknown", snapshot.requests, snapshot.lookups); } } /* * 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) { if (f == NULL) return; report_subject_attrs(f, reset); report_object_attrs(f, reset); } linux-application-whitelisting-fapolicyd-e086a8a/src/library/attr-lookup-metrics.h000066400000000000000000000023251520336644600306140ustar00rootroot00000000000000/* * 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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/attr-sets.c000066400000000000000000000301241520336644600266060ustar00rootroot00000000000000/* * 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; } 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); } linux-application-whitelisting-fapolicyd-e086a8a/src/library/attr-sets.h000066400000000000000000000043531520336644600266200ustar00rootroot00000000000000/* * 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" 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; 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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/avl.c000066400000000000000000000304531520336644600254470ustar00rootroot00000000000000//#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 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/avl.h000066400000000000000000000037131520336644600254530ustar00rootroot00000000000000#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) NEVERNULL WARNUNUSED; /* 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) WARNUNUSED; /* 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 */ linux-application-whitelisting-fapolicyd-e086a8a/src/library/backend-manager.c000066400000000000000000000066761520336644600276760ustar00rootroot00000000000000/* * 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 // 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; 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; } int backend_init(const conf_t *conf) { 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/backend-manager.h000066400000000000000000000023641520336644600276710ustar00rootroot00000000000000/* * 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" 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); void backend_close(void); backend_entry* backend_get_first(void); #endif linux-application-whitelisting-fapolicyd-e086a8a/src/library/conf.h000066400000000000000000000036271520336644600256220ustar00rootroot00000000000000/* 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; uid_t uid; gid_t gid; unsigned int do_stat_report; unsigned int detailed_report; unsigned int db_max_size; bool do_audit_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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/daemon-config.c000066400000000000000000000460101520336644600273670ustar00rootroot00000000000000/* * 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 #include #include #include #include #include #include #include #include "paths.h" /* 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 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 }, {"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->uid = 0; config->gid = 0; config->do_stat_report = 1; config->detailed_report = 1; config->db_max_size = 100; config->do_audit_db_sizing = false; 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 0; } /* 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 0; } 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); } static int unsigned_int_parser(unsigned *i, const char *str, int line) { const char *ptr = str; unsigned int j; /* check that all chars are numbers */ for (j=0; ptr[j]; j++) { if (!isdigit(ptr[j])) { msg(LOG_ERR, "Value %s should only be numbers - line %d", str, line); return 1; } } /* convert to unsigned long */ errno = 0; j = strtoul(str, NULL, 10); if (errno) { msg(LOG_ERR, "Error converting string to a number (%s) - line %d", strerror(errno), line); return 1; } *i = j; 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; } static int uid_parser(const struct nv_pair *nv, int line, conf_t *config) { uid_t uid = 0; gid_t gid = 0; if (isdigit(nv->value[0])) { errno = 0; uid = strtoul(nv->value, NULL, 10); if (errno) { msg(LOG_ERR, "Error converting user value - line %d", line); return 1; } 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(nv->value[0])) { errno = 0; gid = strtoul(nv->value, NULL, 10); if (errno) { msg(LOG_ERR, "Error converting group value - line %d", line); return 1; } } 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; } 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) { // "auto" triggers utilisation‑based sizing, anything else // remains the legacy integer in MiB. if (strcmp(nv->value, "auto") == 0) { config->do_audit_db_sizing = true; config->db_max_size = get_default_db_max_size(); return 0; } return unsigned_int_parser(&(config->db_max_size), nv->value, line); } 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/daemon-config.h000066400000000000000000000022631520336644600273760ustar00rootroot00000000000000/* daemon-config.h -- * Copyright 2018-20 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" int load_daemon_config(conf_t *config); void free_daemon_config(conf_t *config); const char *lookup_integrity(unsigned value); const char *lookup_reset_strategy(unsigned value); const char *lookup_timing_collection(unsigned value); #endif linux-application-whitelisting-fapolicyd-e086a8a/src/library/database.c000066400000000000000000001644051520336644600264360ustar00rootroot00000000000000/* * 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 #include /* isspace() */ #include "database.h" #include "decision-timing.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" #include "policy.h" // Local defines enum { READ_DATA, READ_TEST_KEY, READ_DATA_DUP }; typedef enum { DB_NO_OP, ONE_FILE, RELOAD_DB, FLUSH_CACHE, RELOAD_RULES } db_ops_t; #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 // Local variables static MDB_env *env; static MDB_dbi dbi; static int dbi_init = 0; static unsigned MDB_maxkeysize; static const char *data_dir = DB_DIR; static const char *db = DB_NAME; static int update_lock_inited; static int rule_lock_inited; static int lib_symlink=0, lib64_symlink=0, bin_symlink=0, sbin_symlink=0; static struct pollfd ffd[1] = { {0, 0, 0} }; static integrity_t integrity; static atomic_bool reload_db = false; /* * 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_t update_thread; static int update_thread_created; static pthread_mutex_t update_lock; static pthread_mutex_t rule_lock; static MDB_txn *lt_txn = NULL; static MDB_cursor *lt_cursor = NULL; /* * 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 void *update_thread_main(void *arg); static int update_database(conf_t *config); static int write_db(const char *idx, size_t idx_len, const char *data) __attr_access ((__read_only__, 1, 2)) __wur; // External variables extern atomic_bool stop; extern atomic_bool needs_flush; extern atomic_bool reload_rules; 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"; } int preconstruct_fifo(const conf_t *config) { int rc; char err_buff[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, 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, 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, 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, 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, BUFFER_SIZE)); 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, BUFFER_SIZE)); unlink_fifo(); close(ffd[0].fd); return 1; } } return 0; } /* * 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; return 0; } unsigned get_default_db_max_size(void) { return DEFAULT_DB_MAX_SIZE_MB; /* 100 MiB baseline */ } /* autosize_database – compute new map size when utilisation drifts * @config: active daemon configuration, db_max_size is updated in‑place * Returns 1 when the map size was modified, 0 otherwise. On error the * function leaves db_max_size untouched and logs a single warning. */ static int autosize_database(conf_t *config) { MDB_env *tmp_env = NULL; MDB_envinfo info; MDB_stat stat; MDB_txn *txn = NULL; MDB_dbi dbi_tmp; int changed = 0; /* Open the existing env read-only without taking the shared LMDB * lockfile mutexes. autosize_database() only needs a point-in-time view * of the map, so MDB_NOLOCK avoids disturbing the live environment * during reloads. */ if (mdb_env_create(&tmp_env) || mdb_env_set_maxdbs(tmp_env, 2) || mdb_env_open(tmp_env, DB_DIR, MDB_RDONLY|MDB_NOLOCK, 0)) { msg(LOG_WARNING, "autosize: could not inspect LMDB – keeping %u MiB", config->db_max_size); if (tmp_env) mdb_env_close(tmp_env); return 0; } if (mdb_env_info(tmp_env, &info)) { msg(LOG_WARNING, "autosize: mdb_env_info failed – keeping %u MiB", config->db_max_size); mdb_env_close(tmp_env); return 0; } if (mdb_txn_begin(tmp_env, NULL, MDB_RDONLY, &txn)) { msg(LOG_WARNING, "autosize: cannot open LMDB transaction – keeping %u MiB", config->db_max_size); mdb_env_close(tmp_env); return 0; } if (mdb_dbi_open(txn, DB_NAME, 0, &dbi_tmp)) { msg(LOG_WARNING, "autosize: cannot open trust database – keeping %u MiB", config->db_max_size); mdb_txn_abort(txn); mdb_env_close(tmp_env); return 0; } if (mdb_stat(txn, dbi_tmp, &stat)) { msg(LOG_WARNING, "autosize: mdb_stat failed – keeping %u MiB", config->db_max_size); mdb_txn_abort(txn); mdb_env_close(tmp_env); return 0; } unsigned long page_sz = stat.ms_psize; /* LMDB page size */ if (!page_sz) page_sz = 4096; unsigned long used_pg = stat.ms_branch_pages + stat.ms_leaf_pages + stat.ms_overflow_pages; unsigned long max_pg = info.me_mapsize / page_sz; unsigned long util_pct = max_pg ? (100 * used_pg) / max_pg : 0; /* Empty DB or stat glitch – leave for next start‑up */ if (used_pg == 0 || util_pct == 0) { msg(LOG_INFO, "autosize: empty DB – delaying resize until populated (current %u MiB)", config->db_max_size); mdb_txn_abort(txn); mdb_env_close(tmp_env); return 0; } /* Calculate target pages for ~75 % utilization */ unsigned long target_pg = (used_pg * 100) / 75; /* Determine grow/shrink thresholds (±10 %) */ unsigned long grow_thresh = (max_pg * 85) / 100; /* >85 % */ unsigned long shrink_thresh = (max_pg * 65) / 100; /* <65 % */ if (used_pg > grow_thresh || used_pg < shrink_thresh) { /* Round to whole LMDB pages and at least +1 page */ unsigned long new_pg = target_pg + 1; size_t new_mapsize = (size_t)new_pg * (size_t)page_sz; unsigned new_mb = (new_mapsize + MEGABYTE - 1) / MEGABYTE; if (new_mb != config->db_max_size) { msg(LOG_INFO, "autosize: utilisation %lu%%, resizing map %u→%u MiB (entries=%lu)", util_pct, config->db_max_size, new_mb, stat.ms_entries); config->db_max_size = new_mb; changed = 1; } } else { msg(LOG_INFO, "autosize: utilisation %lu%% within 65‑85 %%, keeping %u MiB", util_pct, config->db_max_size); } mdb_txn_abort(txn); mdb_env_close(tmp_env); return changed; } /* 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 = old_mb + (old_mb / 4); if (new_mb <= old_mb) new_mb++; 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; msg(LOG_INFO, "autosize: trust DB full at %lu MiB – grew to %lu MiB, retrying rebuild", old_mb, new_mb); return 0; } static int init_db(const conf_t *config) { unsigned int flags = MDB_MAPASYNC|MDB_NOSYNC; #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, 2)) { /* Clean up environment on failure */ mdb_env_close(env); env = NULL; return 2; } if (mdb_env_set_mapsize(env, config->db_max_size*MEGABYTE)) { /* Clean up environment on failure */ mdb_env_close(env); env = NULL; return 3; } if (mdb_env_set_maxreaders(env, 4)) { /* Clean up environment on failure */ mdb_env_close(env); env = NULL; return 4; } int 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; } MDB_maxkeysize = mdb_env_get_maxkeysize(env); integrity = config->integrity; msg(LOG_INFO, "fapolicyd integrity is %u", integrity); lib_symlink = is_link("/lib"); lib64_symlink = is_link("/lib64"); bin_symlink = is_link("/bin"); sbin_symlink = is_link("/sbin"); return 0; } static unsigned get_pages_in_use(void); static unsigned long pages, max_pages; /* * 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; if (do_close_dbi) mdb_close(env, dbi); mdb_env_close(env); env = NULL; dbi_init = 0; lt_cursor = NULL; lt_txn = NULL; } 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; 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); } } // Now close down close_env(1); } static void check_db_size(const conf_t *config) { MDB_envinfo st; // Collect stats unsigned long size = get_pages_in_use(); if (size == 0) { msg(LOG_WARNING, "The trust database is empty"); return; } mdb_env_info(env, &st); max_pages = st.me_mapsize / size; unsigned long percent = max_pages ? (100*pages)/max_pages : 0; if (percent > 85) { if (config->do_audit_db_sizing) msg(LOG_WARNING, "Trust database at %lu%% capacity - " "map will grow automatically on next rebuild", percent); else msg(LOG_WARNING, "Trust database at %lu%% capacity - " "might want to increase db_max_size setting", percent); } else if (percent < 65) { if (config->do_audit_db_sizing) msg(LOG_WARNING, "Trust database at %lu%% capacity - " "map will shrink automatically on next rebuild", percent); else msg(LOG_WARNING, "Trust database at %lu%% capacity - " "might consider shrinking the size to save space", 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); } /* * database_utilization_report - write current trust database utilization. * @f: report stream. * Returns nothing. */ void database_utilization_report(FILE *f) { fprintf(f, "Trust database pages in use: %lu (%lu%%)\n", pages, max_pages ? ((100*pages)/max_pages) : 0); } void database_report(FILE *f) { database_config_report(f); database_utilization_report(f); } /* * A DBI has to be associated with any new txn instance. It can be * reused within the same environment unless an abort is used. Aborts * close the data base instance. */ static int open_dbi(MDB_txn *txn) { if (!dbi_init) { int rc; if ((rc = mdb_dbi_open(txn, db, MDB_CREATE|MDB_DUPSORT, &dbi))){ msg(LOG_ERR, "%s", mdb_strerror(rc)); return rc; } dbi_init = 1; } return 0; } static void abort_transaction(MDB_txn *txn) { mdb_txn_abort(txn); dbi_init = 0; } /* * 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 ----------------------------------------------------- */ errno = 0; #if SIZE_MAX >= (1ULL << 32) unsigned long long sval = strtoull(p, &end, 10); if (end == p || errno == ERANGE) return 1; *size = (off_t)sval; #else unsigned long sval = strtoul(p, &end, 10); if (end == p || errno == ERANGE) return 1; *size = (off_t)sval; #endif /* 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; } /* * 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) { MDB_val key, value; MDB_txn *txn; int rc, ret_val = 0; char *hash = NULL; if (mdb_txn_begin(env, NULL, 0, &txn)) return 1; if (open_dbi(txn)) { abort_transaction(txn); return 2; } // do_memfd_update has the length, handle_record doesn't if (idx_len == 0) idx_len = strlen(idx); if (idx_len > MDB_maxkeysize) { hash = path_to_hash(idx, idx_len); if (hash == NULL) { abort_transaction(txn); return 5; } key.mv_data = (void *)hash; key.mv_size = (SHA512_LEN * 2) + 1; } else { key.mv_data = (void *)idx; key.mv_size = idx_len; } value.mv_data = (void *)data; value.mv_size = strlen(data); if ((rc = mdb_put(txn, dbi, &key, &value, 0))) { msg(LOG_ERR, "%s", mdb_strerror(rc)); abort_transaction(txn); ret_val = (rc == MDB_MAP_FULL) ? WRITE_DB_MAP_FULL : 3; goto out; } if ((rc = mdb_txn_commit(txn))) { msg(LOG_ERR, "%s", mdb_strerror(rc)); ret_val = (rc == MDB_MAP_FULL) ? WRITE_DB_MAP_FULL : 4; goto out; } out: if (idx_len > MDB_maxkeysize) free(hash); return ret_val; } /* * The idea with this set of code is that we can set up ops once * and perform many read operations. This reduces the need to setup * a read lock every time and initial a whole transaction. It returns * a 0 on success and a 1 on error. */ static int start_long_term_read_ops(void) { int rc; if (lt_txn == NULL) { if (mdb_txn_begin(env, NULL, MDB_RDONLY, <_txn)) return 1; } if ((rc = open_dbi(lt_txn))) { msg(LOG_ERR, "open_dbi:%s", mdb_strerror(rc)); abort_transaction(lt_txn); lt_txn = NULL; return 1; } if (lt_cursor == NULL) { if ((rc = mdb_cursor_open(lt_txn, dbi, <_cursor))) { msg(LOG_ERR, "cursor_open:%s", mdb_strerror(rc)); abort_transaction(lt_txn); lt_txn = NULL; return 1; } } return 0; } /* * We are finished with read ops. Close it up. */ static void end_long_term_read_ops(void) { mdb_cursor_close(lt_cursor); lt_cursor = NULL; abort_transaction(lt_txn); lt_txn = NULL; } static unsigned get_pages_in_use(void) { MDB_stat st; int rc; if (start_long_term_read_ops()) { pages = 0; return 0; } rc = mdb_stat(lt_txn, dbi, &st); end_long_term_read_ops(); if (rc) { 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; int rc; if (start_long_term_read_ops()) return -1; rc = mdb_stat(lt_txn, dbi, &status); end_long_term_read_ops(); if (rc) return -1; return status.ms_entries; } /* * This is the long term read operation. It takes a path as input and * search for the data. It returns NULL on error or if no data found. * The returned string must be freed by the caller. */ static char *lt_read_db(const char *index, int operation, int *error) __attr_dealloc_free; static char *lt_read_db(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(lt_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(lt_cursor, &nleaves); if (nleaves <= 1) { free(hash); *error = 0; return NULL; } // is there a next duplicate? if ((rc = mdb_cursor_get(lt_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) { return strndup(db, 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 (mdb_env_stat(env, &status)) return -1; if (status.ms_entries == 0) return 1; return 0; } static int delete_all_entries_db() { int rc = 0; MDB_txn *txn; if (mdb_txn_begin(env, NULL, 0, &txn)) return 1; if (open_dbi(txn)) { abort_transaction(txn); return 2; } // 0 -> delete , 1 -> delete and close if ((rc = mdb_drop(txn, dbi, 0))) { msg(LOG_DEBUG, "mdb_drop -> %s", mdb_strerror(rc)); abort_transaction(txn); return 3; } if ((rc = mdb_txn_commit(txn))) { if (rc == MDB_MAP_FULL) msg(LOG_ERR, "db_max_size needs to be increased"); else msg(LOG_DEBUG, "mdb_txn_commit -> %s", mdb_strerror(rc)); return 4; } return 0; } /* * do_memfd_update - Populate the LMDB trust database from a backend memfd. * * Returns 0 when all records write successfully, 1 when the first non-zero * write_db error encountered during the traversal of backend items. */ int do_memfd_update(int memfd, long *entries) { int rc = 0; *entries = 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)); rc = 1; 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(buff[i])) { delim = &buff[i]; delims++; } if (delims >= MAX_DELIMS) { buff[i] = '\0'; break; } } if (delim == NULL) //bad line ? should never happen continue; // index, size, data res = write_db(buff, delim - buff, delim + 1); if (res) { msg(LOG_ERR, "Error (%d) writing key=\"%s\" data=\"%s\"", res, (const char*)buff, (const char*)delim + 1); if (rc == 0) rc = res; if (res == WRITE_DB_MAP_FULL) break; } } } while (!fd_fgets_eof_r(st) && !stop); fd_fgets_destroy(st); // calls munmap, memfd is closed by backend_close 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 entry and imports records * using do_memfd_update. Processing stops early when the global stop flag * becomes true. * * 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(int with_sync, conf_t *config) { msg(LOG_INFO, "Creating trust database"); int rc = 0; int retries = 0; for (;;) { 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(be->backend->memfd, &be->backend->entries); if (rc) msg(LOG_ERR, "Failed to import trust data from %s backend", be->backend->name); } } if (rc == WRITE_DB_MAP_FULL && config->do_audit_db_sizing && retries == 0) { if (grow_map_after_full(config) == 0 && delete_all_entries_db() == 0) { retries++; rc = 0; continue; } } break; } if (stop) return 1; // Flush everything to disk if (with_sync) mdb_env_sync(env, 1); // Check if database is getting full and warn check_db_size(config); return rc; } /* * check_data_presence - Look up an LMDB record and compare its stored data. * @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 lt_read_db are logged separately. */ static int check_data_presence(const char * index, const char * data, int * matched) { int found = 0; int error; char *read; int operation = READ_DATA; int cnt = 0; while (1) { error = 0; read = NULL; read = lt_read_db(index, operation, &error); if (error) msg(LOG_DEBUG, "Error when reading from DB!"); if (!read) break; // check strings if (strcmp(data, read) == 0) { found = 1; } free(read); 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. * @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. */ long check_from_memfd(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(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(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) { msg(LOG_INFO, "Checking if the trust database up to date"); if (start_long_term_read_ops()) 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(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++; } } end_long_term_read_ops(); 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; char err_buff[BUFFER_SIZE]; msg(LOG_INFO, "Initializing the trust database"); // update_lock is used in update_database() pthread_mutex_init(&update_lock, NULL); pthread_mutex_init(&rule_lock, NULL); update_lock_inited = 1; rule_lock_inited = 1; if (migrate_database()) return 1; /* One‑shot utilisation‑driven sizing */ if (config->do_audit_db_sizing && autosize_database(config)) msg(LOG_INFO, "autosize: map size recomputed to %u MiB", config->db_max_size); if ((rc = init_db(config))) { msg(LOG_ERR, "Cannot open the trust database, init_db() (%d)", rc); return rc; } if ((rc = backend_init(config))) { msg(LOG_ERR, "Failed to load trust data from backend (%d)", rc); close_db(0); return rc; } if ((rc = backend_load(config))) { msg(LOG_ERR, "Failed to load data from backend (%d)", rc); close_db(0); 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); if (rc) msg(LOG_ERR, "Failed updating the trust database"); } } // Conserve memory by dumping unneeded resources backend_close(); if (rc == 0) { rc = pthread_create(&update_thread, NULL, update_thread_main, config); if (rc == 0) update_thread_created = 1; else msg(LOG_ERR, "Failed to create update thread (%s)", strerror_r(rc, err_buff, sizeof(err_buff))); } return rc; } /* * This function handles the integrity check and any retries. Retries are * necessary if the system has both i686 and x86_64 packages installed. It * takes a path as input and searches for the data. It returns 0 if no * data is found or if the integrity check has failed. There is no * distinguishing which is the case since both mean you cannot trust the file. * It returns a 1 if the file is found and trustworthy. Callers have to * check the error variable before trusting it's results. */ static int read_trust_db(const char *path, int *error, struct file_info *info, int fd) { int do_integrity = 0, mode = READ_TEST_KEY; char *res; int retry = 0; char sha_xattr[FILE_DIGEST_STRING_MAX]; char calc_digest[FILE_DIGEST_STRING_MAX]; struct lmdb_record record; if (integrity != IN_NONE && info) { do_integrity = 1; mode = READ_DATA; sha_xattr[0] = 0; // Make sure we can't re-use stack value } 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 = lt_read_db(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 if (mode != READ_DATA_DUP) mode = READ_DATA_DUP; 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; file_hash_alg_t ima_alg = FILE_HASH_ALG_NONE; // read xattr only the first time if (retry == 1) rc = get_ima_hash(fd, &ima_alg, sha_xattr); if (rc) { 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 (retry == 1 && ima_alg != FILE_HASH_ALG_NONE) { /* * 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 { *error = 1; return 0; } } 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 a hash only one time if (retry == 1) { 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; } 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; } // Returns a 1 if trusted and 0 if not and -1 on error int check_trust_database(const char *path, struct file_info *info, int fd) { int retval = 0, error; int res; struct decision_timing_span lock_timing; struct decision_timing_span read_timing; struct decision_timing_span total_timing; // this function is going to be used from decision_thread that means // we need to be sure database won't change under our hands. 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); lock_update_thread(); decision_timing_stage_end(&lock_timing); decision_timing_trust_db_stage_begin(DECISION_TIMING_TRUST_DB_READ, &read_timing); if (start_long_term_read_ops()) { decision_timing_stage_end(&read_timing); unlock_update_thread(); decision_timing_stage_end(&total_timing); return -1; } res = read_trust_db(path, &error, info, fd); 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 res = read_trust_db(&path[4], &error, info, fd); if (error) retval = -1; else if (res) retval = 1; } } } end_long_term_read_ops(); decision_timing_stage_end(&read_timing); unlock_update_thread(); decision_timing_stage_end(&total_timing); return retval; } void close_database(void) { if (update_thread_created) { pthread_join(update_thread, NULL); update_thread_created = 0; } // we can close db when we are really sure update_thread does not exist close_db(1); if (update_lock_inited) { pthread_mutex_destroy(&update_lock); update_lock_inited = 0; } if (rule_lock_inited) { pthread_mutex_destroy(&rule_lock); rule_lock_inited = 0; } backend_close(); unlink_fifo(); } /* * 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) { if (!update_lock_inited) { pthread_mutex_init(&update_lock, NULL); update_lock_inited = 1; } if (!rule_lock_inited) { pthread_mutex_init(&rule_lock, NULL); rule_lock_inited = 1; } 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); if (update_lock_inited) { pthread_mutex_destroy(&update_lock); update_lock_inited = 0; } if (rule_lock_inited) { pthread_mutex_destroy(&rule_lock); rule_lock_inited = 0; } } void unlink_fifo(void) { unlink(fifo_path); } /* * Lock wrapper for update mutex */ void lock_update_thread(void) { pthread_mutex_lock(&update_lock); //msg(LOG_DEBUG, "lock_update_thread()"); } /* * Unlock wrapper for update mutex */ void unlock_update_thread(void) { pthread_mutex_unlock(&update_lock); //msg(LOG_DEBUG, "unlock_update_thread()"); } /* * set_integrity_mode - update the runtime integrity policy setting. * @mode: integrity mode that should be used for future checks. * Returns nothing. */ void set_integrity_mode(integrity_t mode) { lock_update_thread(); integrity = mode; unlock_update_thread(); msg(LOG_INFO, "fapolicyd integrity is %u", integrity); } /* * Lock wrapper for rule mutex */ 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 wrapper for rule mutex */ void unlock_rule(void) { if (!rule_lock_inited) return; pthread_mutex_unlock(&rule_lock); //msg(LOG_DEBUG, "unlock_rule()"); } /* * This function reloads updated backend db into our internal database. * It returns 0 on success and non-zero on error. */ static int update_database(conf_t *config) { int rc; 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(); if ((rc = delete_all_entries_db())) { msg(LOG_ERR, "Cannot delete database (%d)", rc); unlock_update_thread(); return rc; } if (stop) { unlock_update_thread(); return 1; } if (!stop) rc = create_database(/*with_sync*/0, config); else rc = 1; // signal that cache need to be flushed if (!stop) atomic_store_explicit(&needs_flush, true, memory_order_release); unlock_update_thread(); mdb_env_sync(env, 1); if (rc) { msg(LOG_ERR, "Failed to create the trust database (%d)", rc); close_db(1); return rc; } return 0; } /* * handle_record - Process a single update command received from the FIFO. * @buffer: Raw line of text read from the update pipe. For file updates the * buffer contains a path, file size, and SHA256 hash separated by * whitespace. * * Returns 0 after successfully storing the record, 1 when processing should * stop due to malformed data or a shutdown request. */ static int handle_record(const char * buffer) { char path[2048+1]; char hash[64+1]; size_t size; if (stop) return 1; // validating input int res = sscanf(buffer, "%2048s %zu %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; } char data[BUFFER_SIZE]; snprintf(data, BUFFER_SIZE, DATA_FORMAT, (unsigned int)SRC_UNKNOWN, size, hash); msg(LOG_DEBUG, "update_thread: Saving %s %s", path, data); lock_update_thread(); write_db(path, 0, data); unlock_update_thread(); return 0; } void set_reload_trust_database(void) { atomic_store_explicit(&reload_db, true, memory_order_release); } /* * 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); } static void do_reload_db(conf_t* config) { msg(LOG_INFO, "It looks like there was an update of the system... Syncing DB."); int rc; unsigned int old_db_max_size = config->db_max_size; backend_close(); /* One‑shot utilisation‑driven sizing */ if (config->do_audit_db_sizing && autosize_database(config)) { msg(LOG_INFO, "autosize: map size recomputed to %u MiB", config->db_max_size); /* * LMDB may unmap/remap the environment during resize. Use * the same lock that protects decision reads and rebuild * writes before touching the live map. */ if (config->db_max_size < old_db_max_size) { lock_update_thread(); close_env(0); rc = init_db(config); unlock_update_thread(); if (rc) { msg(LOG_ERR, "Cannot open the trust database, init_db() (%d)", rc); if (stop) goto out; record_trust_reload_failure(); close(ffd[0].fd); backend_close(); unlink_fifo(); exit(rc); } } 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; 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(); close_db(0); goto out; } if ((rc = backend_load(config))) { msg(LOG_ERR, "Failed to load data from backend (%d)", rc); record_trust_reload_failure(); close_db(0); goto out; } if ((rc = update_database(config))) { msg(LOG_ERR, "Cannot update trust database!"); if (stop) goto out; record_trust_reload_failure(); close(ffd[0].fd); backend_close(); unlink_fifo(); exit(rc); } msg(LOG_INFO, "Updated"); out: // Conserve memory backend_close(); } /* * 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; } lock_rule(); rc = do_reload_rules(config); if (rc) msg(LOG_ERR, "Rule reload failed; previous policy preserved"); unlock_rule(); return rc; } static void *update_thread_main(void *arg) { int rc; int flags; sigset_t sigs; char buff[BUFFER_SIZE]; char err_buff[BUFFER_SIZE]; conf_t *config = (conf_t *)arg; 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, 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, BUFFER_SIZE)); goto finalize; } ffd[0].events = POLLIN; while (!stop) { /* * 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 paths.h * (for example RELOAD_TRUSTDB_COMMAND) 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_exchange_explicit(&reload_db, false, memory_order_acq_rel)) { do_reload_db(config); } #ifdef DEBUG msg(LOG_DEBUG, "Update poll interrupted"); #endif if (rc < 0) { if (errno == EINTR) { #ifdef DEBUG msg(LOG_DEBUG, "update poll rc = EINTR"); #endif continue; } else { msg(LOG_ERR, "Update poll error (%s)", strerror_r(errno, err_buff, BUFFER_SIZE)); goto finalize; } } else if (rc == 0) { #ifdef DEBUG msg(LOG_DEBUG, "Update poll timeout expired"); #endif continue; } else { if (ffd[0].revents & POLLIN) { fd_fgets_state_t *st = fd_fgets_init(); if (st == NULL) { msg(LOG_ERR, "Failed to initialize buffered FIFO reader"); break; } do { if (stop) break; int 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'); if (end == NULL) { msg(LOG_ERR, "Too long line?"); continue; } int 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 db_ops_t values 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 (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; do_reload_db(config); } 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); // 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; atomic_store_explicit(&needs_flush, true, memory_order_release); } 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_record(buff)) continue; } } } while(!fd_fgets_eof_r(st) && !stop); fd_fgets_destroy(st); } } } finalize: close(ffd[0].fd); unlink_fifo(); return NULL; } /*********************************************************************** * 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; // Returns 0 on success and 1 on failure int walk_database_start(conf_t *config) { int rc; // Initialize the database if (init_db(config)) { printf("Cannot open the trust database\n"); return 1; } if (database_empty()) { printf("The trust database is empty - nothing to do\n"); return 1; } // Position to the first entry mdb_txn_begin(env, NULL, MDB_RDONLY, <_txn); if ((rc = open_dbi(lt_txn))) { puts(mdb_strerror(rc)); abort_transaction(lt_txn); return 1; } if ((rc = mdb_cursor_open(lt_txn, dbi, <_cursor))) { puts(mdb_strerror(rc)); abort_transaction(lt_txn); return 1; } if ((rc = mdb_cursor_get(lt_cursor, &wdb_entry.path, &wdb_entry.data, MDB_FIRST)) == 0) return 0; if (rc != MDB_NOTFOUND) puts(mdb_strerror(rc)); return 1; } walkdb_entry_t *walk_database_get_entry(void) { return &wdb_entry; } // Returns 1 on success and 0 in error int walk_database_next(void) { int rc; if ((rc = mdb_cursor_get(lt_cursor, &wdb_entry.path, &wdb_entry.data, MDB_NEXT)) == 0) return 1; if (rc != MDB_NOTFOUND) puts(mdb_strerror(rc)); return 0; } void walk_database_finish(void) { mdb_cursor_close(lt_cursor); abort_transaction(lt_txn); close_db(0); } linux-application-whitelisting-fapolicyd-e086a8a/src/library/database.h000066400000000000000000000045221520336644600264340ustar00rootroot00000000000000/* * 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 "conf.h" #include "file.h" #include "gcc-attributes.h" typedef struct { MDB_val path; MDB_val data; } walkdb_entry_t; void lock_update_thread(void); void unlock_update_thread(void); void set_integrity_mode(integrity_t mode); 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)); 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); void database_report(FILE *f); int unlink_db(void) __wur; void unlink_fifo(void); unsigned get_default_db_max_size(void); void lock_rule(void); void unlock_rule(void); // Database verification functions 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); // Functions for unit test use 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); #define RELOAD_TRUSTDB_COMMAND '1' #define FLUSH_CACHE_COMMAND '2' #define RELOAD_RULES_COMMAND '3' #endif linux-application-whitelisting-fapolicyd-e086a8a/src/library/deb-backend.c000066400000000000000000000145741520336644600270120ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include #include "conf.h" #include "fapolicyd-backend.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); backend deb_backend = { kDebBackend, deb_init_backend, deb_load_list, deb_destroy_backend, -1, -1, }; // ================================================================ // 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. // ======================================================================= static int do_deb_load_list(const conf_t *conf) { const char *control_file = "md5sums"; struct _hash_record *hashtable = NULL; struct _hash_record **hashtable_ptr = &hashtable; 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) { if (add_file_to_backend_by_md5(path, hash, hashtable_ptr, SRC_DEB, &deb_backend) != 0) { rc = 1; goto out; } } 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); return rc; } static int deb_load_list(const conf_t *conf) { msg(LOG_DEBUG, "Loading debian backend"); int memfd = memfd_create("deb_snapshot", MFD_CLOEXEC | MFD_ALLOW_SEALING); deb_backend.memfd = memfd; deb_backend.entries = -1; if (memfd < 0) { msg(LOG_ERR, "memfd_create failed for debian backend (%s)", strerror(errno)); return 1; } if (do_deb_load_list(conf) != 0) { close(memfd); deb_backend.memfd = -1; deb_backend.entries = -1; msg(LOG_WARNING, "Failed making debian backend snapshot due to load error"); return 1; } /* 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 debian backend memfd (%s)", strerror(errno)); return 0; } static int 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); return 1; } return 0; } static int deb_destroy_backend(void) { dpkg_program_done(); modstatdb_shutdown(); return 0; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/decision-event.h000066400000000000000000000047211520336644600276050ustar00rootroot00000000000000/* * 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 /* * 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_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; } #endif linux-application-whitelisting-fapolicyd-e086a8a/src/library/decision-timing.c000066400000000000000000002452441520336644600277550ustar00rootroot00000000000000/* * 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_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_stage_order *order; unsigned long long decisions; unsigned long long duration_ns; 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_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_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); } #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.order = ℴ report.decisions = totals[DECISION_TIMING_STAGE_TOTAL].count; report.duration_ns = input ? input->duration_ns : 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_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)); 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++) { for (stage = 0; stage < DECISION_TIMING_STAGE_COUNT; stage++) snapshot_stage(&totals[stage], &workers[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.order = ℴ report.decisions = decision_count; report.duration_ns = duration_ns; 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"); 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_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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/decision-timing.h000066400000000000000000000166521520336644600277610ustar00rootroot00000000000000/* * 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_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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/escape.c000066400000000000000000000100311520336644600261130ustar00rootroot00000000000000/* * 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 "message.h" static const char sh_set[] = "\"'`$\\!()| "; /* * 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) { if (!input) return 0; const char *p = input; size_t size = 0, cnt = 0; while (*p) { // \000 if (*p < 32) cnt += 4; // \\ \/ else if (strchr(sh_set, *p)) cnt += 2; // non escaped char else cnt++; p++; size++; } // if no escaped char if (cnt == size) return 0; return cnt; } #define MAX_SIZE 8192 char *escape_shell(const char *input, const size_t expected_size) { char *escape_buffer; const char *p; unsigned int j = 0; if (!input) return NULL; if (expected_size >= MAX_SIZE) return NULL; escape_buffer = malloc(expected_size + 1); if (escape_buffer == NULL) return NULL; p = input; while (*p) { if ((unsigned char)*p < 32) { escape_buffer[j++] = ('\\'); escape_buffer[j++] = ('0' + ((*p & 0300) >> 6)); escape_buffer[j++] = ('0' + ((*p & 0070) >> 3)); escape_buffer[j++] = ('0' + (*p & 0007)); } else if (strchr(sh_set, *p)) { escape_buffer[j++] = ('\\'); escape_buffer[j++] = *p; } else escape_buffer[j++] = *p; p++; } escape_buffer[j] = '\0'; /* terminate string */ return escape_buffer; } #define isoctal(a) (((a) & ~7) == '0') void unescape_shell(char *s, const size_t len) { size_t sz = 0; char *buf = s; while (*s) { if (*s == '\\' && sz + 3 < len && 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 == '\\' && sz + 2 < len) { *buf++ = s[1]; s += 2; sz += 2; } else { *buf++ = *s++; sz++; } } *buf = '\0'; } #define IS_HEX(X) (isxdigit(X) > 0 && !(islower(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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/escape.h000066400000000000000000000023451520336644600261310ustar00rootroot00000000000000/* * 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" char *escape_shell(const char *, const size_t) __attr_dealloc_free __attr_access ((__read_only__, 1, 2)); size_t check_escape_shell(const char *); void unescape_shell(char *s, const size_t len) __attr_access ((__read_write__, 1, 2)); char *unescape(const char *input) __attr_dealloc_free; #endif linux-application-whitelisting-fapolicyd-e086a8a/src/library/event.c000066400000000000000000001125751520336644600260140ustar00rootroot00000000000000/* * 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 "attr-lookup-metrics.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 Queue *subj_cache = NULL; static Queue *obj_cache = NULL; static bool obj_cache_warned = false; static unsigned int early_subj_cache_evictions = 0; static unsigned int building_tracer_evictions = 0; static unsigned int building_stale_evictions = 0; static struct message_rate_limit building_tracer_log = MESSAGE_RATE_LIMIT_INIT(SUBJECT_BUILDING_LOG_INTERVAL); static struct message_rate_limit building_stale_log = MESSAGE_RATE_LIMIT_INIT(SUBJECT_BUILDING_LOG_INTERVAL); atomic_bool needs_flush = false; 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; }; /* * 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) { building_tracer_evictions++; log_limit = &building_tracer_log; } else { building_stale_evictions++; 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; early_subj_cache_evictions++; } } if (early_subj_cache_evictions > 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 int occ, thr_m = 0, thr_q = 0; if (obj_cache_warned) return; if (obj_cache->evictions & 0xF) return; evicts = obj_cache->evictions + 1; miss = obj_cache->misses + 1; hit = obj_cache->hits; lookups = hit + miss; occ = (obj_cache->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; } } // Return 0 on success and 1 on error int init_event_system(const conf_t *config) { /* * 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. */ subj_cache=init_lru(config->subj_cache_size, (void (*)(void *))subject_clear, "Subject", (void (*)(void *))subject_evict_warn); if (!subj_cache) return 1; obj_cache = init_lru(config->obj_cache_size, (void (*)(void *))object_clear, "Object", obj_evict_warn); if (!obj_cache) { destroy_lru(subj_cache); subj_cache = NULL; return 1; } return 0; } static int flush_cache(void) { if (obj_cache->count == 0) return 0; const unsigned int size = obj_cache->total; msg(LOG_DEBUG, "Flushing object cache"); obj_cache->evict_cb = NULL; destroy_lru(obj_cache); obj_cache = init_lru(size, (void (*)(void *))object_clear, "Object", obj_evict_warn); if (!obj_cache) return 1; msg(LOG_DEBUG, "Flushed"); return 0; } void destroy_event_system(void) { /* We're intentionally clearing the caches; disable warnings */ if (subj_cache) subj_cache->evict_cb = NULL; if (early_subj_cache_evictions) msg(LOG_WARNING, "Processes are being evicted from the subject cache before " "pattern detection completes: increase subj_cache_size " "(total early evictions: %u)", early_subj_cache_evictions); if (obj_cache) obj_cache->evict_cb = NULL; if (obj_cache_warned) msg(LOG_WARNING, "object cache eviction ratios high: increase obj_cache_size"); destroy_lru(subj_cache); destroy_lru(obj_cache); subj_cache = NULL; obj_cache = 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 (atomic_exchange_explicit(&needs_flush, false, memory_order_acq_rel)) { decision_timing_stage_begin( DECISION_TIMING_STAGE_CACHE_FLUSH, &timing); flush_cache(); decision_timing_stage_end(&timing); } // 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. */ subject_attr_t *fetch_proc_status(event_t *e, subject_type_t t) { unsigned int mask = policy_get_rules_proc_status_mask(); mask |= policy_get_syslog_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, "%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); } 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 (obj_cache->count == 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 (subj_cache->count == 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 lru_metrics metrics; if (f == NULL) return; lru_metrics_snapshot(subj_cache, &metrics, 0); print_cache_config(f, &metrics); lru_metrics_snapshot(obj_cache, &metrics, 0); print_cache_config(f, &metrics); } /* * do_cache_utilization_report - write current cache slot utilization. * @f: report stream. * Returns nothing. */ void do_cache_utilization_report(FILE *f) { struct lru_metrics metrics; if (f == NULL) return; lru_metrics_snapshot(subj_cache, &metrics, 0); print_cache_utilization(f, &metrics); lru_metrics_snapshot(obj_cache, &metrics, 0); print_cache_utilization(f, &metrics); } /* * do_cache_health_report - write cache counters that warrant investigation. * @f: report stream. * Returns nothing. */ void do_cache_health_report(FILE *f) { if (f == NULL) return; fprintf(f, "Early subject cache evictions: %u\n", early_subj_cache_evictions); fprintf(f, "Subject BUILDING tracer evictions: %u\n", building_tracer_evictions); fprintf(f, "Subject BUILDING stale evictions: %u\n", building_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 lru_metrics metrics; unsigned int early_evictions = early_subj_cache_evictions; unsigned int tracer_evictions = building_tracer_evictions; unsigned int stale_evictions = building_stale_evictions; if (f == NULL) return; if (reset) { early_subj_cache_evictions = 0; building_tracer_evictions = 0; building_stale_evictions = 0; } fprintf(f, "\nSubject cache effectiveness:\n"); lru_metrics_snapshot(subj_cache, &metrics, reset); print_cache_metrics(f, &metrics); fprintf(f, "Early subject cache evictions: %u\n", early_evictions); fprintf(f, "Subject BUILDING tracer evictions: %u\n", tracer_evictions); fprintf(f, "Subject BUILDING stale evictions: %u\n", stale_evictions); fprintf(f, "\nObject cache effectiveness:\n"); lru_metrics_snapshot(obj_cache, &metrics, reset); print_cache_metrics(f, &metrics); } /* * 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); } linux-application-whitelisting-fapolicyd-e086a8a/src/library/event.h000066400000000000000000000037321520336644600260130ustar00rootroot00000000000000/* * event.h - Header file for event.c * Copyright (c) 2016,2018-19,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 */ #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); 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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/failure-action.c000066400000000000000000000132031520336644600275610ustar00rootroot00000000000000/* * 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 "failure-action.h" struct failure_definition { const char *name; failure_action_t action; }; /* * failure_definitions - known daemon reliability failures * * Every failure starts in observe mode. Existing compatibility behavior, such * as queue-full denial or the deadman kill, stays at the call site until 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_OBSERVE }, [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) { return reason >= 0 && reason < FAILURE_REASON_MAX && failure_definitions[reason].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"; } return "unknown"; } /* * 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); } linux-application-whitelisting-fapolicyd-e086a8a/src/library/failure-action.h000066400000000000000000000027421520336644600275740ustar00rootroot00000000000000/* * 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_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_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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/fapolicyd-backend.h000066400000000000000000000033271520336644600302310ustar00rootroot00000000000000/* * 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 "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; // source, size, sha // Do not pad the hash value so SHA1 and SHA256 digests parse correctly // The reason for in and out is they mean different things for printf // and scanf. For scanf, it limits the buffer. For printf, its the minimum // bytes to write. helper: stringify macro value #define STR_IMPL(x) #x #define STR(x) STR_IMPL(x) #define DATA_FORMAT "%u %zu %s" #define DATA_FORMAT_IN "%u %zu %" STR(FILE_DIGEST_STRING_WIDTH) "s" typedef struct _backend { const char * name; int (*init)(void); int (*load)(const conf_t *); int (*close)(void); int memfd; long entries; } backend; #endif linux-application-whitelisting-fapolicyd-e086a8a/src/library/fapolicyd-defs.h000066400000000000000000000020541520336644600275570ustar00rootroot00000000000000/* * 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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/fd-fgets.c000066400000000000000000000205441520336644600263640ustar00rootroot00000000000000/* 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 makes eof true. 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); } linux-application-whitelisting-fapolicyd-e086a8a/src/library/fd-fgets.h000066400000000000000000000042031520336644600263630ustar00rootroot00000000000000/* 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)) __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)) __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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/file-backend.c000066400000000000000000000041531520336644600271670ustar00rootroot00000000000000/* * 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/file.c000066400000000000000000001316311520336644600256040ustar00rootroot00000000000000/* * 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-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 variables static struct udev *udev; magic_t magic_fast, magic_full; struct cache { dev_t device; const char *devname; }; static struct cache c = { 0, NULL }; // 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)); // 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) { // Setup udev udev = udev_new(); // Setup libmagic unsetenv("MAGIC"); // Fast magic: minimal rules, all expensive checks disabled 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 (magic_fast == NULL) { msg(LOG_ERR, "Unable to init libmagic"); return 1; } // Load only essential magic rules if (magic_load(magic_fast, MAGIC_PATH) != 0) { msg(LOG_ERR, "Unable to load fast magic database"); magic_close(magic_fast); return 2; } // Full magic: normal operation magic_full = magic_open(MAGIC_MIME|MAGIC_ERROR|MAGIC_NO_CHECK_CDF| MAGIC_NO_CHECK_ELF); if (magic_full == NULL) { msg(LOG_ERR, "Unable to init libmagic"); magic_close(magic_fast); return 3; } // System default if (magic_load(magic_full, NULL) != 0) { msg(LOG_ERR, "Unable to load default magic database"); magic_close(magic_fast); magic_close(magic_full); return 4; } return 0; } // Release memory during shutdown void file_close(void) { udev_unref(udev); magic_close(magic_fast); magic_close(magic_full); free((void *)c.devname); } /* * 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; } // 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 udev_device *dev; const char *node; if (c.device) { if (c.device == device) { strncpy(buf, c.devname, blen-1); buf[blen-1] = 0; return buf; } } // Create udev_device from the dev_t obtained from stat dev = udev_device_new_from_devnum(udev, 'b', device); 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((void *)c.devname); c.device = device; c.devname = strdup(buf); 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 { p = tok; 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 (len >= 3 && (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, "= 5 && 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. decision_timing_mime_stage_begin( DECISION_TIMING_MIME_LIBMAGIC_FALLBACK, &timing); ptr = magic_descriptor(magic_fast, fd); if (ptr == NULL || (ptr && (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(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 unsigned char e_ident[EI_NIDENT]; static inline ssize_t read_preliminary_header(int fd) { return safe_read(fd, (char *)e_ident, EI_NIDENT); } static Elf32_Ehdr *read_header32(int fd, Elf32_Ehdr *ptr) { 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) { 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) { uint32_t info = 0; ssize_t rc; rc = read_preliminary_header(fd); 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); 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 unsigned long sz = (unsigned)hdr->e_phentsize * (unsigned)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; Elf32_Dyn *dyn_tbl = malloc(ph_tbl[i].p_filesz); if((unsigned int)lseek(fd, ph_tbl[i].p_offset, SEEK_SET) != ph_tbl[i].p_offset) { free(dyn_tbl); goto err_out32; } num = ph_tbl[i].p_filesz / sizeof(Elf32_Dyn); if (num > 1000) { 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); 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 unsigned long sz = (unsigned)hdr->e_phentsize * (unsigned)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; Elf64_Dyn *dyn_tbl = malloc(ph_tbl[i].p_filesz); if ((unsigned int)lseek(fd, ph_tbl[i].p_offset, SEEK_SET) != ph_tbl[i].p_offset) { free(dyn_tbl); goto err_out64; } num = ph_tbl[i].p_filesz / sizeof(Elf64_Dyn); if (num > 1000) { 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/file.h000066400000000000000000000074121520336644600256100ustar00rootroot00000000000000/* * 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; #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_close(void); struct file_info *stat_file_entry(int fd) __attr_dealloc_free; 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)); 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)); 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; int get_ima_hash(int fd, file_hash_alg_t *alg, char *sha); uint32_t gather_elf(int fd, off_t size); #endif linux-application-whitelisting-fapolicyd-e086a8a/src/library/filter.c000066400000000000000000000543321520336644600261540ustar00rootroot00000000000000/* * 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. * * 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->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); } /* * stack_pop_reset - reset flags and pop top item */ static void stack_pop_reset(stack_t *_stack, int *sp) { if (_stack == NULL || sp == NULL || *sp <= 0) return; stack_item_t *item = (stack_item_t *)stack_top(_stack); if (item && item->filter) { item->filter->processed = 0; item->filter->matched = 0; } stack_pop(_stack); (*sp)--; } /* * stack_pop_all_reset - reset and pop all stack items */ static void stack_pop_all_reset(stack_t *_stack, int *sp) { if (_stack == NULL || sp == NULL) return; while (!stack_is_empty(_stack)) stack_pop_reset(_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; 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; filter->processed = 1; // 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++; filter->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_t * stack_item = NULL; // pop already processed filters from the top of the stack do { if (stack_item) { filter = stack_item->filter; offset = stack_item->offset; level = stack_item->level; // assuimg that nothing has matched on the // upper level so it's a directory match if (filter->matched && filter->path[filter->len-1] == '/') { res = filter->type == ADD ? FILTER_ALLOW : FILTER_DENY; goto end; } // reset processed flag stack_pop_reset(&stack, &sp); } stack_item = (stack_item_t*)stack_top(&stack); } while(stack_item && stack_item->filter->processed); if (!stack_item) break; filter = stack_item->filter; 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_reset(&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; } } ssize_t nread; 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 ((nread = 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. * */ linux-application-whitelisting-fapolicyd-e086a8a/src/library/filter.h000066400000000000000000000034261520336644600261570ustar00rootroot00000000000000/* * 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; int processed; int matched; list_t list; } filter_t; typedef struct _stack_item { int level; int offset; 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_ linux-application-whitelisting-fapolicyd-e086a8a/src/library/gcc-attributes.h000066400000000000000000000014201520336644600276020ustar00rootroot00000000000000#ifndef GCC_ATTRIBUTES_H #define GCC_ATTRIBUTES_H #define NEVERNULL __attribute__ ((returns_nonnull)) #define WARNUNUSED __attribute__ ((warn_unused_result)) #define NORETURN __attribute__ ((noreturn)) // These macros originate in sys/cdefs.h. These are stubs in case undefined. #include // any major header brings cdefs.h #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 __nonnull # define __nonnull(params) #endif #ifndef __wur # define __wur #endif #endif linux-application-whitelisting-fapolicyd-e086a8a/src/library/llist.c000066400000000000000000000063351520336644600260160ustar00rootroot00000000000000/* * 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); } linux-application-whitelisting-fapolicyd-e086a8a/src/library/llist.h000066400000000000000000000031301520336644600260110ustar00rootroot00000000000000/* * 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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/lru.c000066400000000000000000000300651520336644600254660ustar00rootroot00000000000000/* * 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) { msg(LOG_DEBUG, "%s cache size: %u", q->name, q->total); msg(LOG_DEBUG, "%s slots in use: %u (%u%%)", q->name, q->count, q->total ? (100*q->count)/q->total : 0); msg(LOG_DEBUG, "%s hits: %lu", q->name, q->hits); msg(LOG_DEBUG, "%s misses: %lu", q->name, q->misses); msg(LOG_DEBUG, "%s collisions: %lu", q->name, q->collisions); msg(LOG_DEBUG, "%s evictions: %lu (%lu%%)", q->name, q->evictions, q->hits ? (100*q->evictions)/q->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 queue->count = 0; queue->hits = 0; queue->misses = 0; queue->collisions = 0; 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 (queue->count) dequeue(queue); free(queue->pool); free(queue); } static unsigned int are_all_slots_full(const Queue *queue) { return queue->count == 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 == q->count) { 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) { 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) { 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 queue->count--; } /* * 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) { 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 queue->count--; queue->evictions++; } /* * 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) queue->collisions++; } /* * 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 queue->count++; } // 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); queue->misses++; // 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++; queue->hits++; } else queue->hits++; 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 = queue->count; metrics->total = queue->total; metrics->hits = queue->hits; metrics->misses = queue->misses; metrics->collisions = queue->collisions; metrics->evictions = queue->evictions; if (reset) { queue->hits = 0; queue->misses = 0; queue->collisions = 0; queue->evictions = 0; } } 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/lru.h000066400000000000000000000054441520336644600254760ustar00rootroot00000000000000/* * 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 "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 { unsigned int count; // Number of filled slots unsigned int total; // total number of slots unsigned long hits; // Number of times object was in cache unsigned long misses;// number of times object was not in cache unsigned long collisions;// cached object was unusable for this key unsigned long 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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/md5-backend.c000066400000000000000000000102501520336644600267300ustar00rootroot00000000000000/* * md5-backend.c - functions for adding files to the trust database * based on MD5 hashes. * * 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 "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, abort. * 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. */ int add_file_to_backend_by_md5(const char *path, const char *expected_md5, struct _hash_record **hashtable, trust_src_t trust_src, backend *dstbackend) { #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 %si, %s", path, strerror(errno)); return 1; } if (fstat(fd, &path_stat)) { close(fd); msg(LOG_WARNING, "fstat file %s failed %s", path, strerror(errno)); return 1; } // 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 1; } size_t file_size = path_stat.st_size; #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 1; } 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 1; } 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 1; } char *data; if (asprintf(&data, DATA_FORMAT, trust_src, file_size, sha_digest) == -1) { data = NULL; } free(sha_digest); if (data) { // Getting rid of the duplicates. struct _hash_record *rcd = NULL; char key[kMaxKeyLength]; snprintf(key, kMaxKeyLength - 1, "%s %s", path, data); HASH_FIND_STR(*hashtable, key, rcd); if (!rcd) { rcd = (struct _hash_record *)malloc( sizeof(struct _hash_record)); rcd->key = strdup(key); HASH_ADD_KEYPTR(hh, *hashtable, rcd->key, strlen(rcd->key), rcd); if (dprintf(dstbackend->memfd, "%s %s\n", path, data) < 0) { msg(LOG_ERR, "dprintf failed writing %s to memfd (%s)", path, strerror(errno)); free((void *)data); return 1; } } free((void *)data); return 0; } return 1; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/md5-backend.h000066400000000000000000000024021520336644600267350ustar00rootroot00000000000000/* * md5-backend.h - header file for md5-backend.c * * 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 kMaxKeyLength = 4096; static const int kMd5HexSize = 32; int 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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/message.c000066400000000000000000000056541520336644600263160ustar00rootroot00000000000000/* * 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 "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; void set_message_mode(message_t mode, debug_message_t debug) { message_mode = mode; debug_message = debug; } 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 (message_mode == MSG_SYSLOG) vsyslog(priority, fmt, ap); else { // For stderr we'll include the log level, use ANSI escape // codes to colourise the it, and prefix lines with the time // and date. const char *color; const char *level; switch (priority) { case LOG_EMERG: color = "\x1b[31m"; level = "EMERGENCY"; break; /* Red */ case LOG_ALERT: color = "\x1b[35m"; level = "ALERT"; break; /* Magenta */ case LOG_CRIT: color = "\x1b[33m"; level = "CRITICAL"; break; /* Yellow */ case LOG_ERR: color = "\x1b[31m"; level = "ERROR"; break; /* Red */ case LOG_WARNING: color = "\x1b[33m"; level = "WARNING"; break; /* Yellow */ case LOG_NOTICE: color = "\x1b[32m"; level = "NOTICE"; break; /* Green */ case LOG_INFO: color = "\x1b[36m"; level = "INFO"; break; /* Cyan */ case LOG_DEBUG: color = "\x1b[34m"; level = "DEBUG"; break; /* Blue */ default: color = "\x1b[0m"; level = "UNKNOWN"; break; /* Reset */ } time_t rawtime; struct tm timeinfo; char buffer[80]; time(&rawtime); // localtime is not threadsafe, use _r version for safety (void) localtime_r(&rawtime, &timeinfo); if (strftime(buffer, sizeof(buffer), "%x %T [ ", &timeinfo) == 0) fputs("time unavailable [ ", stderr); else fputs(buffer, stderr); fputs(color, stderr); fputs(level, stderr); fputs("\x1b[0m ]: ", stderr); vfprintf(stderr, fmt, ap); fputc('\n', stderr); fflush(stderr); } va_end(ap); } linux-application-whitelisting-fapolicyd-e086a8a/src/library/message.h000066400000000000000000000045631520336644600263210ustar00rootroot00000000000000/* * 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); void msg(int priority, const char *fmt, ...) #ifdef __GNUC__ __attribute__ ((format (printf, 2, 3))); #else ; #endif #endif linux-application-whitelisting-fapolicyd-e086a8a/src/library/nv.h000066400000000000000000000022021520336644600253040ustar00rootroot00000000000000/* * 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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/object-attr.c000066400000000000000000000040411520336644600270750ustar00rootroot00000000000000/* * 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/object-attr.h000066400000000000000000000030721520336644600271050ustar00rootroot00000000000000/* * 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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/object.c000066400000000000000000000056551520336644600261410ustar00rootroot00000000000000/* * 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(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; 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; a->obj[obj->type - OBJ_START] = newnode; a->cnt++; 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/object.h000066400000000000000000000030021520336644600261260ustar00rootroot00000000000000/* * 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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/paths.h000066400000000000000000000036501520336644600260100ustar00rootroot00000000000000/* 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/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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/policy-metrics.c000066400000000000000000000207751520336644600276360ustar00rootroot00000000000000/* * 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 "event.h" #include "policy-metrics.h" static atomic_ulong allowed = 0, denied = 0; static atomic_ulong allowed_by_rule; static atomic_ulong allowed_by_fallthrough; static atomic_ulong fallthrough_open; static atomic_ulong fallthrough_execute; static atomic_ulong fallthrough_trusted; static atomic_ulong fallthrough_untrusted; static atomic_ulong fallthrough_trust_unknown; static atomic_ulong fallthrough_executable; static atomic_ulong fallthrough_programmatic; static atomic_ulong fallthrough_sharedlib; static atomic_ulong fallthrough_unknown_ftype; static atomic_ulong fallthrough_other_ftype; static atomic_uint ruleset_generation; /* * policy_metrics_record_ruleset_update - count a published policy generation. * Returns nothing. */ void policy_metrics_record_ruleset_update(void) { atomic_fetch_add_explicit(&ruleset_generation, 1, 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 * @e: event whose object ftype should be classified if it is already cached. * Returns nothing. */ static void count_fallthrough_ftype(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(&fallthrough_unknown_ftype, 1, memory_order_relaxed); return; } if (strcmp(name, "application/x-sharedlib") == 0) { atomic_fetch_add_explicit(&fallthrough_sharedlib, 1, memory_order_relaxed); return; } if (strstr(name, "executable") || strcmp(name, "application/x-bad-elf") == 0) { atomic_fetch_add_explicit(&fallthrough_executable, 1, memory_order_relaxed); return; } if (ftype_is_programmatic(name)) { atomic_fetch_add_explicit(&fallthrough_programmatic, 1, memory_order_relaxed); return; } atomic_fetch_add_explicit(&fallthrough_other_ftype, 1, memory_order_relaxed); } /* * count_fallthrough_details - record low-cardinality default-allow dimensions * @e: event that reached the no-opinion allow path. * Returns nothing. */ static void count_fallthrough_details(event_t *e) { object_attr_t *trust; if (e->type & FAN_OPEN_EXEC_PERM) atomic_fetch_add_explicit(&fallthrough_execute, 1, memory_order_relaxed); else atomic_fetch_add_explicit(&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(&fallthrough_trust_unknown, 1, memory_order_relaxed); else if (trust->val) atomic_fetch_add_explicit(&fallthrough_trusted, 1, memory_order_relaxed); else atomic_fetch_add_explicit(&fallthrough_untrusted, 1, memory_order_relaxed); count_fallthrough_ftype(e); } /* * count_allow_source - record whether an allow came from a rule or fallback * @e: event that was allowed. * @source: source reported by process_event_with_source(). * Returns nothing. */ static void count_allow_source(event_t *e, decision_source_t source) { if (source == DECISION_SOURCE_RULE) { atomic_fetch_add_explicit(&allowed_by_rule, 1, memory_order_relaxed); return; } atomic_fetch_add_explicit(&allowed_by_fallthrough, 1, memory_order_relaxed); if (e) count_fallthrough_details(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) { if ((decision & DENY) == DENY) { atomic_fetch_add_explicit(&denied, 1, memory_order_relaxed); return; } atomic_fetch_add_explicit(&allowed, 1, memory_order_relaxed); count_allow_source(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); } /* * 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_snapshot(&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_snapshot(&denied, 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) { if (!metrics) return; metrics->allowed_by_rule = policy_counter_snapshot(&allowed_by_rule, reset); metrics->allowed_by_fallthrough = policy_counter_snapshot(&allowed_by_fallthrough, reset); metrics->fallthrough_open = policy_counter_snapshot(&fallthrough_open, reset); metrics->fallthrough_execute = policy_counter_snapshot(&fallthrough_execute, reset); metrics->fallthrough_trusted = policy_counter_snapshot(&fallthrough_trusted, reset); metrics->fallthrough_untrusted = policy_counter_snapshot(&fallthrough_untrusted, reset); metrics->fallthrough_trust_unknown = policy_counter_snapshot(&fallthrough_trust_unknown, reset); metrics->fallthrough_executable = policy_counter_snapshot(&fallthrough_executable, reset); metrics->fallthrough_programmatic = policy_counter_snapshot(&fallthrough_programmatic, reset); metrics->fallthrough_sharedlib = policy_counter_snapshot(&fallthrough_sharedlib, reset); metrics->fallthrough_unknown_ftype = policy_counter_snapshot(&fallthrough_unknown_ftype, reset); metrics->fallthrough_other_ftype = policy_counter_snapshot(&fallthrough_other_ftype, reset); metrics->ruleset_generation = atomic_load_explicit(&ruleset_generation, memory_order_relaxed); } linux-application-whitelisting-fapolicyd-e086a8a/src/library/policy-metrics.h000066400000000000000000000017211520336644600276310ustar00rootroot00000000000000/* * 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 - count a published policy generation. * Returns nothing. */ void policy_metrics_record_ruleset_update(void); /* * 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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/policy.c000066400000000000000000000744631520336644600261750ustar00rootroot00000000000000/* * policy.c - functions that encapsulate the notion of a policy * Copyright (c) 2016,2019-25 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-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 /* * 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 { 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; char *rule_file_identity; }; /* * active_policy - currently published policy generation * * Evaluation and reload both run under the rule lock, so pointer replacement * and old-snapshot destruction are serialized with policy readers. */ static struct policy_snapshot *active_policy; /* * active_*_proc_status_mask - atomic copies of active snapshot masks * * Event construction reads these without taking the rule lock so proc-status * collection can request fields required by the active rules and log format. */ static atomic_uint active_rules_proc_status_mask; static atomic_uint active_syslog_proc_status_mask; 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; extern conf_t config; #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 #define WB_SIZE 512 static char *working_buffer = NULL; // 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_destroy - release one policy snapshot * @policy: snapshot to destroy, or NULL. * Returns nothing. */ static void policy_snapshot_destroy(struct policy_snapshot *policy) { if (!policy) return; rules_clear(&policy->rules); free_syslog_fields(policy); free(policy->rule_file_identity); free(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; } 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) { if (active_policy) msg(LOG_ERR, "Daemon configuration update failed; " "previous policy preserved"); else msg(LOG_ERR, "Daemon configuration update failed; " "no policy installed"); } /* * 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 = active_policy; policy->rule_count = policy->rules.cnt; policy->rules_proc_status_mask = rules_get_proc_status_mask(&policy->rules); /* * Transaction point: after this assignment, new decisions use the * candidate policy. Everything before this must be able to fail while * leaving the old active_policy untouched. */ active_policy = policy; policy_metrics_record_ruleset_update(); atomic_store_explicit(&active_rules_proc_status_mask, policy->rules_proc_status_mask, memory_order_release); atomic_store_explicit(&active_syslog_proc_status_mask, policy->syslog_proc_status_mask, memory_order_release); if (policy->rule_file_identity) msg(LOG_INFO, "Ruleset identity: %s", policy->rule_file_identity); msg(LOG_INFO, "Daemon rules updated"); policy_snapshot_destroy(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; size_t len = 0; struct policy_snapshot *policy = policy_snapshot_create(identity); *out = NULL; if (!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); policy_snapshot_destroy(policy); return 1; } lineno++; } free(line); if (ferror(f)) { msg(LOG_ERR, "Error reading rules file (%s)", strerror(errno)); policy_snapshot_destroy(policy); return 1; } if (policy->rules.cnt == 0) { msg(LOG_INFO, "No rules in file - exiting"); policy_snapshot_destroy(policy); return 1; } else { msg(LOG_DEBUG, "Loaded %u rules", policy->rules.cnt); } rc = parse_syslog_format(policy, _config->syslog_format); if (!rc || policy->num_fields == 0) { policy_snapshot_destroy(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; } void destroy_rules(void) { policy_snapshot_destroy(active_policy); active_policy = NULL; atomic_store_explicit(&active_rules_proc_status_mask, 0, memory_order_release); atomic_store_explicit(&active_syslog_proc_status_mask, 0, memory_order_release); if (stop) { free(working_buffer); working_buffer = NULL; } } unsigned int policy_get_syslog_proc_status_mask(void) { return atomic_load_explicit(&active_syslog_proc_status_mask, memory_order_acquire); } /* * 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) { return atomic_load_explicit(&active_rules_proc_status_mask, memory_order_acquire); } /* * 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 taking the rule lock. * ff_identity - SHA256 identity for @ff, transferred to the new snapshot. * * load_rule_file() prepares these so do_reload_rules() can spend the locked * section 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 && subj->set) { 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++; } } else if (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; } static void log_it(const struct policy_snapshot *policy, unsigned int num, decision_t results, event_t *e) { struct decision_timing_span timing; int mode = results & SYSLOG ? LOG_INFO : LOG_DEBUG; unsigned int i; size_t dsize; ptrdiff_t written; char *p1, *p2, *val; decision_timing_stage_begin( DECISION_TIMING_STAGE_SYSLOG_DEBUG_FORMAT, &timing); if (working_buffer == NULL) { working_buffer = malloc(WB_SIZE); if (working_buffer == NULL) { msg(LOG_ERR, "No working buffer for logging"); decision_timing_stage_end(&timing); return; } } dsize = WB_SIZE; p1 = p2 = working_buffer; // Dummy assignment for p1 to quiet warnings for (i = 0; i < 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, policy->fields[i].name, dsize); written = p1 - p2; if ((size_t)written > dsize) break; dsize -= (size_t)written; if (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(policy->fields[i].item, num, 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); } } working_buffer[WB_SIZE-1] = 0; // Just in case msg(mode, "%s", working_buffer); decision_timing_stage_end(&timing); } /* * process_event_with_source - evaluate policy and report decision source * @e: event to evaluate. * @source: optional output receiving rule or fallthrough source. * * 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) { decision_t results = NO_OPINION; struct policy_snapshot *policy = active_policy; decision_timing_driver_t previous_driver; struct decision_timing_span eval_timing; lnode *r; if (source) *source = DECISION_SOURCE_FALLTHROUGH; if (!policy) { if (response_timing) decision_timing_stage_begin( DECISION_TIMING_STAGE_RESPONSE_TOTAL, response_timing); return ALLOW; } /* Use a local cursor so concurrent readers do not share list state. */ //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)) ) { previous_driver = decision_timing_driver_push( DECISION_TIMING_DRIVER_RESPONSE); log_it(policy, r ? r->num : 0xFFFFFFFF, results, e); decision_timing_driver_pop(previous_driver); } // 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) return results; return ALLOW; } /* * 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 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 void reply_event(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 static int use_new = 2; if (use_new == 2) use_new = test_info_api(fd); if (reply & FAN_AUDIT && use_new) { 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: // Close this last so that no other thread can open a file which // reclaims this fd number before we render a decision. 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 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 rule_wait_timing; struct decision_timing_span response_timing = { 0 }; decision_timing_driver_t previous_driver; 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_event_build_deny(decision_event); } else { decision_timing_stage_end(&event_timing); metric_event = &e; decision_timing_stage_begin( DECISION_TIMING_STAGE_RULE_LOCK_WAIT, &rule_wait_timing); lock_rule(); decision_timing_stage_end(&rule_wait_timing); decision = process_event_with_source(&e, &source, &response_timing); unlock_rule(); } 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 (__atomic_load_n(&config.permissive, __ATOMIC_RELAXED)) reply_event(fd, metadata, FAN_ALLOW | (decision & AUDIT), metric_event); else reply_event(fd, metadata, decision & FAN_RESPONSE_MASK, metric_event); decision_timing_driver_pop(previous_driver); } 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; } void policy_no_audit(void) { if (active_policy) rules_unsupport_audit(&active_policy->rules); } /* * policy_rule_hits_report - write per-rule hit counters for the active policy. * @f: output stream. * * The rule mutex protects the active snapshot from reload destruction 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; if (f == NULL || active_policy == NULL) return; lock_rule(); policy = active_policy; if (policy) rules_hits_report_reset(f, &policy->rules, reset); unlock_rule(); } linux-application-whitelisting-fapolicyd-e086a8a/src/library/policy.h000066400000000000000000000065061520336644600261730ustar00rootroot00000000000000/* * 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 "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; } 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 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); 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); #endif linux-application-whitelisting-fapolicyd-e086a8a/src/library/process.c000066400000000000000000000346321520336644600263460ustar00rootroot00000000000000/* * 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; } } /* * 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 on success and * -1 when the status file cannot be processed. */ 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; unsigned int found = 0; size_t gid_partial_len = 0; if (info == NULL || fields == 0) return 0; // Initialize info struct if (fields & PROC_STAT_UID) { if (info->uid) { attr_set_destroy(info->uid); info->uid = NULL; } info->uid = attr_set_create(NULL, UNSIGNED); if (info->uid == NULL) return -1; } if (fields & PROC_STAT_GID) { if (info->groups) { attr_set_destroy(info->groups); info->groups = NULL; } info->groups = attr_set_create(NULL, UNSIGNED); if (info->groups == NULL) { if (fields & PROC_STAT_UID) { attr_set_destroy(info->uid); info->uid = NULL; } return -1; } } 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; if (fd < 0) { 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; } return -1; } fd_fgets_state_t *st = fd_fgets_init(); if (st == NULL) return -1; 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) { 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; 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); } 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); } // Not marking found - wait for supplemental 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) 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); fd_fgets_destroy(st); return 0; } /* * 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/process.h000066400000000000000000000076161520336644600263550ustar00rootroot00000000000000/* * 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); int read_proc_status(pid_t pid, unsigned int fields, struct proc_status_info *info); int check_environ_from_pid(pid_t pid); #endif linux-application-whitelisting-fapolicyd-e086a8a/src/library/queue.c000066400000000000000000000247151520336644600260150ustar00rootroot00000000000000/* * 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" struct queue_entry { decision_event_t event; }; /* * 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 deadman 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. */ /* Initialize a queue */ struct queue *q_open(size_t num_entries) { struct queue *q; int saved_errno; 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; 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); } /* * 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); 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) { fprintf(f, "Inter-thread max queue depth: %u\n", metrics->max_depth); } /* * 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; 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); q->events[n].event = *data; q->events[n].event.enqueue_ns = decision_timing_queue_enqueue_time(); 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) { 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); } linux-application-whitelisting-fapolicyd-e086a8a/src/library/queue.h000066400000000000000000000062201520336644600260110ustar00rootroot00000000000000/* * 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; }; /* 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 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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/rpm-backend.c000066400000000000000000000256151520336644600270540ustar00rootroot00000000000000/* * 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 #include "message.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 int rpm_load_list(const conf_t *conf) { // before the spawn int sv[2]; if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sv) < 0) { msg(LOG_ERR, "socketpair failed"); exit(1); } posix_spawn_file_actions_t actions; posix_spawn_file_actions_init(&actions); // child sees sv[1] as FD 3 (arbitrary but fixed) posix_spawn_file_actions_adddup2(&actions, sv[1], 3); posix_spawn_file_actions_addclose(&actions, sv[0]); posix_spawn_file_actions_addclose(&actions, sv[1]); char *argv[] = { "fapolicyd-rpm-loader", NULL }; char *custom_env[] = { "FAPO_SOCK_FD=3", NULL }; pid_t pid = -1; int status = posix_spawn(&pid, BINARYDIR "/fapolicyd-rpm-loader", &actions, NULL, argv, custom_env); close(sv[1]); // Parent doesn't write if (status == 0) { msg(LOG_DEBUG, "fapolicyd-rpm-loader spawned with pid: %d",pid); 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; if (recvmsg(sv[0], &_msg, 0) < 0) { msg(LOG_ERR, "recvmesg failed"); exit(1); } close(sv[0]); struct cmsghdr *c = CMSG_FIRSTHDR(&_msg); if (!c || c->cmsg_type != SCM_RIGHTS) { msg(LOG_ERR, "missing fd"); exit(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; waitpid(pid, &status, 0); } else msg(LOG_ERR, "posix_spawn failed: %s\n", strerror(status)); posix_spawn_file_actions_destroy(&actions); int exit_rc = status; if (status) { if (WIFEXITED(status)) exit_rc = WEXITSTATUS(status); else if (WIFSIGNALED(status)) exit_rc = 128 + WTERMSIG(status); } if (exit_rc) msg(LOG_ERR, "fapolicyd-rpm-loader exited with rc=%d", exit_rc); return exit_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; rpm_loff_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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/rules.c000066400000000000000000001152661520336644600260250ustar00rootroot00000000000000/* * 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 "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; 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(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: 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: 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) { int state = 0; // 0 == subj, 1 == obj char *ptr; while ((ptr = strtok(NULL, " "))) { int type; char *ptr2 = strchr(ptr, '='); if (ptr2) { *ptr2 = 0; ptr2++; if (state == 0) { type = subj_name_to_val(ptr, 2); 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; } } 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. 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; 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 { *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; } if (attr_sets_find(sets, name)) { 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; rformat_t format = RULE_FMT_ORIG; attr_sets_t *sets = l->sets; if (strchr(buf, ':')) format = RULE_FMT_COLON; n->format = format; ptr = strtok(buf, " "); 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 = strtok(NULL, " "))) { 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, 2); 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)) 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; } } 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; 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; } /* * 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 (!attr_set_check_str(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; } // Returns 0 if no match, 1 if a match __attribute__((hot)) static decision_t 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 PATH: // skip if fall through if (type == PATH) { if (r->s[cnt].type == EXE || r->s[cnt].type == EXE_DIR) if (attr_set_check_str(r->s[cnt].set, "untrusted")) if (is_obj_trusted(e)) return 0; } // 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 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/rules.h000066400000000000000000000061211520336644600260170ustar00rootroot00000000000000/* * 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; 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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/stack.c000066400000000000000000000040421520336644600257650ustar00rootroot00000000000000/* * 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/stack.h000066400000000000000000000023311520336644600257710ustar00rootroot00000000000000/* * 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_ linux-application-whitelisting-fapolicyd-e086a8a/src/library/string-util.c000066400000000000000000000037411520336644600271460ustar00rootroot00000000000000/* * 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/string-util.h000066400000000000000000000026161520336644600271530ustar00rootroot00000000000000/* * 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 "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)); #endif linux-application-whitelisting-fapolicyd-e086a8a/src/library/subject-attr.c000066400000000000000000000051621520336644600272730ustar00rootroot00000000000000/* * 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/library/subject-attr.h000066400000000000000000000030511520336644600272730ustar00rootroot00000000000000/* * 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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/subject.c000066400000000000000000000115171520336644600263240ustar00rootroot00000000000000/* * 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_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; a->subj[t - SUBJ_START] = newnode; a->cnt++; 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"); } } linux-application-whitelisting-fapolicyd-e086a8a/src/library/subject.h000066400000000000000000000032221520336644600263230ustar00rootroot00000000000000/* * 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 linux-application-whitelisting-fapolicyd-e086a8a/src/library/trust-file.c000066400000000000000000000465301520336644600267660ustar00rootroot00000000000000/* * 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. */ #define BUFFER_SIZE (PATH_MAX+1+1+1+10+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" list_t _list; char *_path; int _count; bool _use_filter; 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); 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; } /* * 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, sb.st_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, 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(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; unsigned long long parsed_size = strtoull(delims[1] + 1, &endptr, 10); if (errno || endptr == delims[1] + 1 || *endptr != '\0') return -1; *size = 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]; size_t sz; unsigned int tsource = SRC_FILE_DB; line++; if (iscntrl(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); } linux-application-whitelisting-fapolicyd-e086a8a/src/library/trust-file.h000066400000000000000000000031501520336644600267620ustar00rootroot00000000000000/* * 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 linux-application-whitelisting-fapolicyd-e086a8a/src/perf-test/000077500000000000000000000000001520336644600247615ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/src/perf-test/fapolicyd-perf-test.c000066400000000000000000000075031520336644600310130ustar00rootroot00000000000000/* * 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 struct fanotify_event_metadata metadata; 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/000077500000000000000000000000001520336644600242125ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/src/tests/Makefile.am000066400000000000000000000133631520336644600262540ustar00rootroot00000000000000# 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 \ trustdb_format_test trustdb_lmdb_test lru_test notify_test \ decision_defer_test \ decision_timing_report_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 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 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 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-defer.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 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/daemon/decision-defer.c decision_defer_test_CPPFLAGS = \ -I${top_builddir} \ -I${top_srcdir}/src/library/ \ -I${top_srcdir}/src/daemon/ 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-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 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}\" 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}\" endif if WITH_DEB check_PROGRAMS += deb_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_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 \ ${top_srcdir}/src/library/file.c \ ${top_srcdir}/src/library/backend-manager.c \ ${top_srcdir}/src/library/deb-backend.c endif TESTS = $(check_PROGRAMS) linux-application-whitelisting-fapolicyd-e086a8a/src/tests/attr_sets_test.c000066400000000000000000000040641520336644600274310ustar00rootroot00000000000000#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 (!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"); attr_set_destroy(standalone); attr_sets_destroy(sets); return 0; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/avl_test.c000066400000000000000000000125271520336644600262060ustar00rootroot00000000000000#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; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/deb_test.c000066400000000000000000000014341520336644600261510ustar00rootroot00000000000000#include #include #include #include "backend-manager.h" #include "conf.h" #include "config.h" #include "message.h" extern atomic_bool stop; int main(int argc, char* const argv[]) { set_message_mode(MSG_STDERR, DBG_YES); conf_t conf; conf.trust = "debdb"; backend_init(&conf); backend_load(&conf); msg(LOG_INFO, "\nDone loading."); backend_entry* debdb_entry = backend_get_first(); backend* debdb = NULL; if (debdb_entry != NULL) { debdb = debdb_entry->backend; } else { msg(LOG_ERR, "ERROR: No backends registered."); } if (debdb == NULL) { msg(LOG_ERR, "ERROR: debdb not registered"); } if (strcmp(conf.trust, debdb->name) != 0) { msg(LOG_ERR, "ERROR: debdb bad name"); } backend_close(); return 0; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/decision_defer_test.c000066400000000000000000000237541520336644600303720ustar00rootroot00000000000000/* * decision_defer_test.c - unit tests for subject-slot decision deferral */ #include #include #include #include #include "decision-defer.h" #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; } /* * 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; } /* 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; return 0; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/decision_timing_report_test.c000066400000000000000000000231601520336644600321560ustar00rootroot00000000000000/* * 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); } 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); 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); 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); } /* * 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/elf_file_test.c000066400000000000000000000117161520336644600271700ustar00rootroot00000000000000/* * 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/escape_test.c000066400000000000000000000046151520336644600266630ustar00rootroot00000000000000/* * 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; } /* 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); /* unescape_shell */ char buf1[] = "\\040\\$"; unescape_shell(buf1, sizeof(buf1)); if (strcmp(buf1, " $")) { fprintf(stderr, "[ERROR:3] unescape_shell octal '%s'\n", buf1); return 3; } char buf2[] = "abc\\"; unescape_shell(buf2, sizeof(buf2)); if (strcmp(buf2, "abc\\")) { fprintf(stderr, "[ERROR:3] trailing '%s'\n", buf2); return 3; } char buf3[] = "abc\\0"; unescape_shell(buf3, strlen(buf3)); if (strcmp(buf3, "abc\\0")) { fprintf(stderr, "[ERROR:3] malformed '%s'\n", buf3); 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/event_test.c000066400000000000000000000710551520336644600265460ustar00rootroot00000000000000/* * 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 "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, additional needs_flush interactions, or * validating that trust-database results propagate into the event fields. */ extern atomic_bool needs_flush; 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; } /* * 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; } 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; if (sscanf(record, DATA_FORMAT_IN, &parsed->tsource, &parsed->size, parsed->digest) != 3) 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, (size_t)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, (size_t)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, (size_t)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; atomic_store_explicit(&needs_flush, false, memory_order_relaxed); return init_event_system(&cfg); } /* * 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; 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; } /* * 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 needs_flush triggers an object cache flush so the next lookup * allocates a fresh entry. */ static int test_needs_flush_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; 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"); atomic_store_explicit(&needs_flush, true, memory_order_relaxed); meta.mask = FAN_OPEN_PERM; CHECK(new_event(&meta, &second) == 0, 33, "[ERROR:33] second new_event failed"); CHECK(!atomic_load_explicit(&needs_flush, memory_order_relaxed), 34, "[ERROR:34] needs_flush not cleared"); 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; } /* 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_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_needs_flush_resets_object_cache(); if (rc) return rc; return 0; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/failure_action_test.c000066400000000000000000000055031520336644600304040ustar00rootroot00000000000000/* * 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_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"); 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/fd_fgets_test.c000066400000000000000000000216321520336644600272020ustar00rootroot00000000000000#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); } /* * 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_mmap_file_no_trailing_newline(); test_mmap_file_readme(); printf("fd-fgets_r tests: all passed\n"); return 0; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/file_filter_test.c000066400000000000000000000035671520336644600277140ustar00rootroot00000000000000/* * 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/file_type_detect_test.c000066400000000000000000000174361520336644600307400ustar00rootroot00000000000000/* * 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 "file.h" extern magic_t magic_fast, magic_full; #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 the libmagic handles used by file.c globals. * Returns 0 on success, -1 on failure. */ static int init_magic_handles(void) { 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"); 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 (!magic_fast) return -1; for (i = 0; fast_db[i]; i++) { (void)snprintf(path, sizeof(path), "%s", fast_db[i]); if (magic_load(magic_fast, path) == 0) break; } if (!fast_db[i]) return -1; magic_full = magic_open(MAGIC_MIME | MAGIC_ERROR | MAGIC_NO_CHECK_CDF | MAGIC_NO_CHECK_ELF); if (!magic_full) return -1; if (magic_load(magic_full, NULL) != 0) return -1; return 0; } /* * close_magic_handles - release libmagic handles used in direct tests. */ static void close_magic_handles(void) { if (magic_fast) magic_close(magic_fast); if (magic_full) magic_close(magic_full); } /* * 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) { 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 }; if (init_magic_handles() != 0) error(1, 0, "failed to initialize test libmagic handles"); 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", 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", 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", 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", 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", 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", 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", 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", magic_fast, fd, "text/x-nftables"); close(fd); close_magic_handles(); return 0; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/filter_test.c000066400000000000000000000155241520336644600267110ustar00rootroot00000000000000/* * filter_test.c - comprehensive tests for filter configuration */ #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" 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; } 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; 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/fixtures/000077500000000000000000000000001520336644600260635ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/src/tests/fixtures/broken-filter.conf000066400000000000000000000000721520336644600314740ustar00rootroot00000000000000# malformed filter + / - usr/ usr/share/ + usr/dir#name linux-application-whitelisting-fapolicyd-e086a8a/src/tests/fixtures/filter-cases.txt000066400000000000000000000035231520336644600312100ustar00rootroot00000000000000minimal /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 linux-application-whitelisting-fapolicyd-e086a8a/src/tests/fixtures/filter-minimal.conf000066400000000000000000000001601520336644600316400ustar00rootroot00000000000000# test filter configuration + / - usr/ + bin/allowed + share/ - *.tmp + *.py - var/log/ + public/ linux-application-whitelisting-fapolicyd-e086a8a/src/tests/fixtures/rules-valid.rules000066400000000000000000000002441520336644600313660ustar00rootroot00000000000000%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 linux-application-whitelisting-fapolicyd-e086a8a/src/tests/gid_proc_test.c000066400000000000000000000125511520336644600272070ustar00rootroot00000000000000#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); } /* * 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(); 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/lru_test.c000066400000000000000000000116271520336644600262260ustar00rootroot00000000000000#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; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/notify_test.c000066400000000000000000000450001520336644600267240ustar00rootroot00000000000000/* * notify_test.c - unit tests for daemon fanotify metadata handling */ #include "config.h" #include #include #include #include #include #include #include #include #include #include "failure-action.h" #include "fanotify-fs-error.h" #include "notify.h" #include "policy.h" #include "decision-defer.h" #include "decision-timing.h" #include "state-report.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; int test_notify_queue_reset(unsigned int entries); void test_notify_queue_destroy(void); int test_notify_queue_push(const decision_event_t *event); 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); #define CHECK(expr, code, msg) \ do { \ if (!(expr)) \ error(1, 0, "%s", msg); \ } while (0) 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_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", .ruleset_generation = 7, .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); } /* * 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); } /* * test_operating_mode_report_order - verify state field order. * * The operating mode group keeps the timing control fields together. Ruleset * generation is last so readers see the active policy after all control * state in the same group. * * Returns nothing. Exits on test failure. */ static void test_operating_mode_report_order(void) { const char *ruleset, *last_stop; char report[1024]; read_operating_mode_report(report, sizeof(report)); last_stop = strstr(report, "Timing collection last stop time: never\n"); ruleset = strstr(report, "Ruleset generation: 7\n"); CHECK(last_stop != NULL, 58, "[ERROR:58] operating mode report missing timing stop field"); CHECK(ruleset != NULL, 59, "[ERROR:59] operating mode report missing ruleset field"); CHECK(last_stop < ruleset, 60, "[ERROR:60] ruleset generation was not last in group"); } /* * 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(test_notify_shutdown_deferred_events() == 3, 61, "[ERROR:61] deferred shutdown count mismatch"); 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 thread 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(test_notify_shutdown_queued_events() == 3, 65, "[ERROR:65] queued shutdown count mismatch"); for (i = 0; i < 3; i++) { errno = 0; CHECK(close(pipes[i][0]) == -1 && errno == EBADF, 66, "[ERROR:66] queued fd was not closed exactly once"); close(pipes[i][1]); } test_notify_queue_destroy(); } /* * 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; unsigned long fs_error_after = 0; char report[4096], expected[128]; int event_pipe[2]; test_operating_mode_report_order(); 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 (observe): ") != 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)); CHECK(strstr(report, "Last metrics reset: never") != NULL, 44, "[ERROR:44] metrics report missing last reset header"); CHECK(strstr(report, "Ruleset generation: ") != NULL, 45, "[ERROR:45] metrics report missing ruleset generation header"); 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(); return 0; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/policy_concurrent_test.c000066400000000000000000000114331520336644600311600ustar00rootroot00000000000000/* * 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 1000 #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. * Returns nothing. Exits on overflow because the test fixture is invalid. */ static void make_policy_text(char *buf, size_t buflen) { 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\n", i, 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_test_policy - publish the policy used by reader threads * @void: no arguments are required. * Returns nothing. Exits if policy loading fails. */ static void load_test_policy(void) { conf_t cfg = { .syslog_format = "rule,dec,perm,:,path" }; char policy[POLICY_BUFSIZE]; FILE *f; int rc; make_policy_text(policy, sizeof(policy)); f = fmemopen(policy, strlen(policy), "r"); if (!f) error(1, errno, "fmemopen failed"); rc = load_rules_from_stream(&cfg, f); fclose(f); if (rc) 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; } /* * 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]; 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"); } 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"); } 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/policy_reload_test.c000066400000000000000000000310061520336644600302420ustar00rootroot00000000000000/* * 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" 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_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); atomic_store(&stop, true); destroy_rules(); return 0; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/queue_test.c000066400000000000000000000104371520336644600265460ustar00rootroot00000000000000/* * queue_test.c - verify queue metric accounting */ #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); } /* * 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[128]; 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(q_enqueue(q, &event) == 0, 5, "[ERROR:5] first enqueue failed"); CHECK(q_enqueue(q, &event) == 0, 6, "[ERROR:6] second enqueue failed"); 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(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(strcmp(report, "Inter-thread max queue depth: 2\n") == 0, 15, "[ERROR:15] legacy queue report format changed"); 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/rules_test.c000066400000000000000000000210671520336644600265550ustar00rootroot00000000000000/* * 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 #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 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!" }, { { "allow auid=1000 uid=-1 path=/bin/ls", NULL }, "negative value -1 not allowed for uid" }, { { "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" } }; /* * 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; } /* * 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); } /* * 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; } int main(void) { char err[ERRBUF]; llist l; event_t e; unsigned i, j; int rc; set_message_mode(MSG_STDERR, DBG_NO); /* 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); /* 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/stress/000077500000000000000000000000001520336644600255355ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/src/tests/stress/Makefile.am000066400000000000000000000014301520336644600275670ustar00rootroot00000000000000# 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\" 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 linux-application-whitelisting-fapolicyd-e086a8a/src/tests/stress/README.md000066400000000000000000000536571520336644600270340ustar00rootroot00000000000000# 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 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 tries to find `fapolicyd-cli` and capture daemon status before and after the workload. - `--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`: Explicit `fapolicyd-cli` path. If omitted, the harness searches relative to itself, then common system paths. - `-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 found. - `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 found. - 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` - `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` - `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. linux-application-whitelisting-fapolicyd-e086a8a/src/tests/stress/fapolicyd-stress.c000066400000000000000000001745521520336644600312120ustar00rootroot00000000000000/* * 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 "paths.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define DEFAULT_ROOTS 4 #define DEFAULT_FANOUT 1 #define DEFAULT_DEPTH 0 #define DEFAULT_ITERATIONS 100 #define DEFAULT_SECONDS 0 #define DEFAULT_HASH_MB 16 #define DEFAULT_CHURN_FILES 2048 #define GRACEFUL_STOP_NS 5000000000ULL #define TERM_STOP_NS 250000000ULL #define MAX_COMMANDS 64 #define CAPTURE_LIMIT (1024 * 1024) #define HASH_BLOCK_SIZE 65536 #define NORETURN __attribute__((noreturn)) #ifndef STRESS_SCRIPT_DIR #define STRESS_SCRIPT_DIR "scripts" #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]); execvp(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_from_exe - locate fapolicyd-cli relative to this helper. * @dst: destination path buffer. * Returns 0 when an executable candidate is found, 1 otherwise. */ static int find_cli_from_exe(char *dst) { char exe[PATH_MAX]; char *slash; ssize_t len; len = readlink("/proc/self/exe", exe, sizeof(exe) - 1); if (len < 0) return 1; exe[len] = 0; slash = strrchr(exe, '/'); if (slash == NULL) return 1; *slash = 0; if (snprintf(dst, PATH_MAX, "%s/../../fapolicyd-cli", exe) >= PATH_MAX) return 1; return file_executable(dst) ? 0 : 1; } /* * find_cli - locate fapolicyd-cli for report collection. * @opts: run options. * @dst: destination path buffer. * Returns 0 when an executable candidate is found, 1 otherwise. */ static int find_cli(const struct stress_options *opts, char *dst) { const char *env = getenv("FAPOLICYD_CLI"); static const char * const candidates[] = { "src/fapolicyd-cli", "./fapolicyd-cli", "../../fapolicyd-cli", "/usr/sbin/fapolicyd-cli", "/sbin/fapolicyd-cli", "/usr/bin/fapolicyd-cli", "/bin/fapolicyd-cli", NULL }; unsigned int idx; if (opts->cli_path) { if (!file_executable(opts->cli_path)) return 1; snprintf(dst, PATH_MAX, "%s", opts->cli_path); return 0; } if (env && file_executable(env)) { snprintf(dst, PATH_MAX, "%s", env); return 0; } if (find_cli_from_exe(dst) == 0) return 0; for (idx = 0; candidates[idx]; idx++) { if (file_executable(candidates[idx])) { snprintf(dst, PATH_MAX, "%s", candidates[idx]); return 0; } } return 1; } /* * 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_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_word_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 : "0ns", after->subject_defer_oldest_age[0] ? after->subject_defer_oldest_age : "0ns"); 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] = ""; 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)); cli_available = find_cli(&opts, cli_path) == 0; if (opts.collect_timing && !cli_available) { fprintf(stderr, "--timing requires fapolicyd-cli\n"); 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 not found; daemon metrics disabled\n"); 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; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/stress/scripts/000077500000000000000000000000001520336644600272245ustar00rootroot00000000000000linux-application-whitelisting-fapolicyd-e086a8a/src/tests/stress/scripts/with-shebang.sh000077500000000000000000000002031520336644600321360ustar00rootroot00000000000000#!/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 linux-application-whitelisting-fapolicyd-e086a8a/src/tests/stress/scripts/without-shebang000077500000000000000000000001611520336644600322600ustar00rootroot00000000000000data=${1:?missing data path} i=0 while [ "$i" -lt 4 ]; do test -r "$data" || exit 1 i=$((i + 1)) done exit 0 linux-application-whitelisting-fapolicyd-e086a8a/src/tests/test-stubs.c000066400000000000000000000011011520336644600264640ustar00rootroot00000000000000/* * 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; linux-application-whitelisting-fapolicyd-e086a8a/src/tests/trustdb_format_test.c000066400000000000000000000013741520336644600304610ustar00rootroot00000000000000// 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; off_t size; int written; written = snprintf(data, sizeof(data), DATA_FORMAT, SRC_RPM, (off_t)9400, digest); if (written < 0 || written >= (int)sizeof(data)) return 1; if (sscanf(data, DATA_FORMAT_IN, &tsource, &size, parsed_digest) != 3) return 1; if (strcmp(digest, parsed_digest)) return 1; return 0; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/trustdb_lmdb_test.c000066400000000000000000000244161520336644600301110ustar00rootroot00000000000000// 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 "database.h" #include "fapolicyd-backend.h" #define CHECK(cond, code, msg) \ do { \ if (!(cond)) { \ fprintf(stderr, "%s\n", msg); \ return code; \ } \ } while (0) static int remove_lmdb_files(const char *dir) { char path[512]; snprintf(path, sizeof(path), "%s/data.mdb", dir); unlink(path); snprintf(path, sizeof(path), "%s/lock.mdb", dir); unlink(path); return rmdir(dir); } /* * 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 with_temp_db(char *tmpdir, size_t tmpdir_sz, conf_t *cfg) { char template[] = "/tmp/fapolicyd-lmdb-XXXXXX"; char *dir = mkdtemp(template); if (dir == NULL) return 1; if (strlen(dir) + 1 > tmpdir_sz) return 1; strcpy(tmpdir, dir); memset(cfg, 0, sizeof(*cfg)); cfg->db_max_size = 16; cfg->integrity = IN_NONE; if (database_set_location(tmpdir, NULL)) return 1; return database_open_for_tests(cfg); } 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; off_t size; int written; written = snprintf(data, sizeof(data), DATA_FORMAT, SRC_RPM, (off_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; } 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]; 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, (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(); 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, (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; 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, (size_t)3000, digest_a, path_b, SRC_FILE_DB, (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"); entry = walk_database_get_entry(); CHECK(entry != NULL, 46, "[ERROR:46] walk_database_get_entry failed"); while (walk_database_next()) count++; 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, (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, (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; } 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, (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; } int main(void) { int rc; rc = test_data_format_round_trip(); 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; return 0; } linux-application-whitelisting-fapolicyd-e086a8a/src/tests/uid_proc_test.c000066400000000000000000000037401520336644600272250ustar00rootroot00000000000000#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; }