pax_global_header00006660000000000000000000000064151302370150014507gustar00rootroot0000000000000052 comment=6522744312a1818a92b9ad9552ab1661c2351185 fapolicyd-1.4.3/000077500000000000000000000000001513023701500134665ustar00rootroot00000000000000fapolicyd-1.4.3/.editorconfig000066400000000000000000000007201513023701500161420ustar00rootroot00000000000000 root = true [{*.{c,h,spec},Makefile,Makefile.*}] charset = utf-8 end_of_line = lf indent_style = tab indent_size = 8 trim_trailing_whitespace = true insert_final_newline = true [*.py] charset = utf-8 end_of_line = lf indent_style = space indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true [*.{ac,m4,yml}] charset = utf-8 end_of_line = lf indent_style = space indent_size = 2 trim_trailing_whitespace = true insert_final_newline = true fapolicyd-1.4.3/.fmf/000077500000000000000000000000001513023701500143145ustar00rootroot00000000000000fapolicyd-1.4.3/.fmf/version000066400000000000000000000000021513023701500157140ustar00rootroot000000000000001 fapolicyd-1.4.3/.github/000077500000000000000000000000001513023701500150265ustar00rootroot00000000000000fapolicyd-1.4.3/.github/workflows/000077500000000000000000000000001513023701500170635ustar00rootroot00000000000000fapolicyd-1.4.3/.github/workflows/.rawhide-fedora-build000066400000000000000000000014051513023701500230420ustar00rootroot00000000000000name: 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-rpm --with-audit --disable-shared --disable-dependency-tracking - name: build run: make - name: check run: make check - name: dist run: make dist fapolicyd-1.4.3/.github/workflows/almalinux9.yml000066400000000000000000000015631513023701500216760ustar00rootroot00000000000000name: 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-rpm --with-audit --disable-shared --disable-dependency-tracking - name: build run: make - name: check run: make check - name: dist run: make dist fapolicyd-1.4.3/.github/workflows/centosstream10.yml000066400000000000000000000016121513023701500224560ustar00rootroot00000000000000name: 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-rpm --with-audit --disable-shared --disable-dependency-tracking - name: build run: make - name: check run: make check - name: dist run: make dist fapolicyd-1.4.3/.github/workflows/rockylinux9.yml000066400000000000000000000016001513023701500221030ustar00rootroot00000000000000name: 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-rpm --with-audit --disable-shared --disable-dependency-tracking - name: build run: make - name: check run: make check - name: dist run: make dist fapolicyd-1.4.3/.github/workflows/stable-fedora-build.yml000066400000000000000000000014271513023701500234170ustar00rootroot00000000000000name: 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-rpm --with-audit --disable-shared --disable-dependency-tracking - name: build run: make - name: check run: make check - name: dist run: make dist fapolicyd-1.4.3/.github/workflows/ubuntu22.yml000066400000000000000000000020551513023701500212760ustar00rootroot00000000000000name: 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 --without-rpm --with-audit --disable-shared --disable-dependency-tracking --with-deb - name: build run: make - name: check run: make check - name: dist run: make dist - name: build deb run: cd deb && ./build_deb.sh fapolicyd-1.4.3/.gitignore000066400000000000000000000017151513023701500154620ustar00rootroot00000000000000# 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-rpm-loader *.log *.rpm *.tar.gz fapolicyd-1.4.3/.packit.yaml000066400000000000000000000033321513023701500157040ustar00rootroot00000000000000specfile_path: fapolicyd.spec upstream_package_name: fapolicyd downstream_package_name: fapolicyd upstream_tag_template: v{version} jobs: - job: copr_build trigger: pull_request identifier: build-normal actions: post-upstream-clone: - bash -c "sed -i 's/#ELN %//' fapolicyd.spec" - bash -c "curl -L -o $(grep Source1 fapolicyd.spec | cut -d ' ' -f 2 | sed -r \"s/%\{name\}/$(grep 'Name:' fapolicyd.spec | cut -f 2 -d ' ')/g;s/%\{semodule_version\}/$(grep '%define semodule_version' fapolicyd.spec | cut -f 3 -d ' ')/g\" | sed -r 's|(.*)#/(.*)|\2 \1|')" - bash -c "pwd" - bash -c "ls -la" targets: - fedora-all - epel-10 - epel-9 # - job: copr_build # trigger: pull_request # identifier: build-asan # actions: # post-upstream-clone: # - bash -c "sed -i 's/#ASAN %//' fapolicyd.spec" # - bash -c "sed -i 's/#ELN %//' fapolicyd.spec" # - bash -c "curl -L -o $(grep Source1 fapolicyd.spec | cut -d ' ' -f 2 | sed -r \"s/%\{name\}/$(grep 'Name:' fapolicyd.spec | cut -f 2 -d ' ')/g;s/%\{semodule_version\}/$(grep '%define semodule_version' fapolicyd.spec | cut -f 3 -d ' ')/g\" | sed -r 's|(.*)#/(.*)|\2 \1|')" # - bash -c "curl -L -o $(grep Source2 fapolicyd.spec | cut -d ' ' -f 2 | sed -r 's|(.*)#/(.*)|\2 \1|')" # - bash -c "cp uthash*.tar.gz /builddir/build/SOURCES/" # - bash -c "pwd" # - bash -c "ls -la" # targets: # - fedora-all # - epel-9 - job: tests trigger: pull_request identifier: build-normal targets: - fedora-all - epel-10 - epel-9 # - job: tests # trigger: pull_request # identifier: build-asan # targets: # - fedora-all # - epel-9 fapolicyd-1.4.3/AGENTS.md000066400000000000000000000071061513023701500147750ustar00rootroot00000000000000# 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. fapolicyd-1.4.3/AUTHORS000066400000000000000000000001131513023701500145310ustar00rootroot00000000000000This program was created and maintained by Steve Grubb fapolicyd-1.4.3/BUILD.md000066400000000000000000000106331513023701500146520ustar00rootroot00000000000000BUILDING ======== Building fapolicyd is reasonably straightforward on Fedora and RedHat-based Linux distributions. This document will guide in installing the build-time dependencies, configure and compile the code, and finally build the RPMs for distribution on compatible non-production systems. BUILD-TIME DEPENDENCIES (fedora and RHEL8) ------------------------------------------ * gcc * autoconf * automake * libtool * make * libudev-devel * kernel-headers * systemd-devel * libgcrypt-devel ( <= fapolicyd-1.1.3) * openssl ( >= fapolicyd-1.1.4) * rpm-devel (optional) * file * file-devel * libcap-ng-devel * libseccomp-devel * lmdb-devel * uthash-devel * python3-devel * kernel >= 4.20 (Must support FANOTIFY_OPEN_EXEC_PERM. See [1] below.) RHEL8: ENABLE CODEREADY AND INSTALL EPEL REPOS ---------------------------------------------- ```bash sudo subscription-manager repos --enable codeready-builder-for-rhel-8-$(arch)-rpms sudo dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm ``` INSTALL BUILD DEPENDENCIES (fedora and RHEL8) --------------------------------------------- ```bash sudo dnf install -y gcc autoconf automake libtool make libudev-devel kernel-headers systemd-devel libgcrypt-devel rpm-devel file file-devel libcap-ng-devel libseccomp-devel lmdb-devel uthash-devel python3-devel ``` CONFIGURING AND COMPILING ------------------------- To build from the repo after cloning and installing dependencies: ```bash cd fapolicyd ./autogen.sh ./configure --with-audit --disable-shared make make dist ``` This will create a tarball. You can use the new tarball with the spec file and create your own rpm. If you want to experiment without installing, just run make with no arguments. It should run fine from where it was built as long as you put the configuration files in /etc/fapolicyd (fapolicyd.rules, fapolicyd.trust, fapolicyd.conf). Note that the shipped policy expects that auditing is enabled. This is done by passing --with-audit to ./configure. The use of rpm as a trust source is now optional. You can run ./configure passing --without-rpm and it will not link against librpm. In this mode, it purely uses the file database in fapolicyd.trust. If rpm is used, then the file trust database can be used in addition to rpmdb. BUILDING THE RPMS ----------------- :exclamation: These unofficial RPMs should only be used for testing and experimentation purposes and not for production systems. :exclamation: To build the RPMs, first install the RPM development tools: ```bash sudo dnf install -y rpmdevtools ``` Then in the root of the repository where fapolicyd was built, use `rpmbuild` to build the RPMs: ```bash rpmbuild -ta fapolicyd-*.tar.gz ``` By default, the RPMs will appear in `~/rpmbuild/RPMS/$(arch)`. NOT BUILDING RPMS ----------------- If you chose to do it yourself, you need to do a couple prep steps: ``` 1) sed -i "s/%python2_path%/`readlink -f /bin/python2 | sed 's/\//\\\\\//g'`/g" rules.d/*.rules 2) sed -i "s/%python2_path%/`readlink -f /bin/python3 | sed 's/\//\\\\\//g'`/g" rules.d/*.rules 3) interpret=`readelf -e /usr/bin/bash \ | grep Requesting \ | sed 's/.$//' \ | rev | cut -d" " -f1 \ | rev` 4) sed -i "s|%ld_so_path%|`realpath $interpret`|g" rules.d/*.rules ``` This corrects the placeholders to match your current system. Then follow the rules listed above for compiling except run make install instead of make dist. CREATING RUNTIME ENVIRONMENT ---------------------------- If you are not using rpm's spec file and are doing it yourself, there are a few more steps. You need to create the necessary directories in the right spot: ``` mkdir -p /etc/fapolicyd/{rules.d,trust.d} mkdir -p /var/lib/fapolicyd/ mkdir --mode=0755 -p /usr/share/fapolicyd/ mkdir -p /usr/lib/tmpfiles.d/ mkdir --mode=0755 -p /run/fapolicyd/ cd init cp fapolicyd.bash_completion /etc/bash_completion.d/ cp fapolicyd.conf /etc/fapolicyd/ cp fapolicyd-magic /usr/share/fapolicyd/ cp fapolicyd-magic.mgc /usr/share/fapolicyd/ cp fapolicyd.service /usr/lib/systemd/system/ cp fapolicyd-tmpfiles.conf /usr/lib/tmpfiles.d/fapolicyd.conf cp fapolicyd.trust /etc/fapolicyd/trust.d/ useradd -r -M -d /var/lib/fapolicyd -s /sbin/nologin -c "Application Whitelisting Daemon" fapolicyd chown root:fapolicyd /etc/fapolicyd/ chown root:fapolicyd /etc/fapolicyd/rules.d/ chown root:fapolicyd /etc/fapolicyd/trust.d/ chown root:fapolicyd /var/lib/fapolicyd/ ``` fapolicyd-1.4.3/CI/000077500000000000000000000000001513023701500137615ustar00rootroot00000000000000fapolicyd-1.4.3/CI/ci-tests.fmf000066400000000000000000000002541513023701500162070ustar00rootroot00000000000000discover: how: fmf url: https://github.com/linux-application-whitelisting/fapolicyd-tests.git filter: component:fapolicyd & tag:CI-Tier-1 execute: how: tmt fapolicyd-1.4.3/COPYING000066400000000000000000001045131513023701500145250ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . fapolicyd-1.4.3/ChangeLog000066400000000000000000000326731513023701500152530ustar00rootroot000000000000001.4.3 - Fix missing allow decision in fapolicyd-cli --test-filter - Update the trust-db filter rules to add and drop some things - Add new magic detections and drop a lot of old rules - Performance improvement looking up mime-types - Various code cleanups, hardening, and performance optimizing - Classify ELF files with an unknown interpreter as application/x-bad-elf - Consolidate the fapolicyd-cli return codes - Add integrity value to state report 1.4.2 - Correct identification of kworker threads - Fix various threading, initialization, and function attribute issues - Add support for SHA512, replace SHA256HASH with FILE_HASH - Add --filter option to fapolicyd-cli --file - Update bash completions for new options - Add an early subject cache evictions counter to the state report - Add CPU cores to the state report - Move mounts update to it's own thread to prevent deadlocks with LUKS - Add db_max_size = auto support so db size is managed by the daemon - In rpm backend, drop any file from trustdb that doesn't verify size or hash 1.4.1 - Fix deadlock on reconfigure - On reconfigure, update the trust list and reload the rpm filter 1.4 - Add ignore_mounts option to fapolicyd to not watch some mount points - add --check-ignore_mounts option to fapolicyd-cli - Fix overlay filesystem detection in fapolicyd-cli --check-watch_fs - Fix fapolicyd-cli --check-path so that it properly detects symlinked dirs - In fapolicyd-cli, add --verbose flag to --check-ignore_mounts - Watch the /run partition - Update some config items in SIGHUP - Clarify warnings for fapolicyd-cli --check-trustdb - All trust source backend checks/updates performance improvement - Fix line too long error when last line of conf file had no newline 1.3.7 - Refactor queue to unify enqueue and dequeue and make it lockless - Improve text/x-shellscript detection - Fix bug in fapolicyd-cli --ftype detection which was introduced in 1.3.6 - Add watched mount points to state report - important since they can change - Warn when object cache eviction ratios are high - Use uthash for trust file backend duplication detection - Update subject identity collection to gather all uid/gid components 1.3.6 - Increase the default subject cache size - Move fapolicyd-rpm-loader to bin directory - Suppress the subject cache eviction for scripts run by interpreters - Fix decriptor leak in previous release - only leaks on rpminstall/delete - Improve performance of filter code - Drop 'device' keyword for subject part of a rule - Add documentation to fapolicyd-filter.conf for better understanding - Fix build flags for Debian to include libmd - Add --test-filter to fapolicy-cli to help test filter rules - Fix bug in the filter that was allowing unexpected files though the filter - Drop 2 more kinds of files from the rpm filter: html and md 1.3.5 - Raise default value for db_max_size - Increase buffer size for reading process groups - Pid read buffer size is defined using define rather than const - Allow override of mounts file - Fix leak and problematic memory managament - Fix creation of RUN_DIR because it breaks rpm verify - Add Microsoft Windows PE MIME magic definition - Microsoft Windows PE rules - Optimize path allocation - Revert change to report interval logic (#325) - Ensure fagenrules handles incomplete lines - Describe how to handle nss-user-lookup correctly - Fix normal pattern handling - Improve AVL test - Install gawk in test workflows - Cleanup and document filesystem code - Add AGENTS.md - Revert "Fix for rpmdb with SQLite3 backend" - Fix rpmdb locking issues by loading via separate process - Fix an infinite loop (#345) - Use memfd instead of pipe (#346) - fapolicyd-cli --check-path doesn't exit - Fix null argv when spawning fapolicyd-rpm-loader (#343) - Fix some concurrency issues - Update comments in lru_evict header - Update fd_fgets code - On exit due to poll error, try to stop and save the database - Improve update thread synchronization in fapolicyd - Modify rpm_load_list for stop checks - Fix a TODO that notes NULL should be returned - Add some error cleanup in filter_load_file - Increase buffer size when reading the PT_INTERP string in gather_elf() - Add debug logs for STATE_LD_SO decisions - Remove volatile qualifier from atomic types - Add MEM_MMAP_FILE storage type for fd_fgets - Fix segfault when writing to readonly memory - Switch rpm-backend to mmap based list parsing - Add memory statistics report - To allow sealing, need to create with MFD_ALLOW_SEALING - Ease SHA strings allocated on each iteration - Skip to add non regular files to trustdb (#333) - Fix segfault when socket is inside of the directory (#355) - Subject cache eviction warning - No eviction warnings on shutdown 1.3.4 - Fix race on fanotify fd on termination - Improve efficiency loading the rpm database into trust db - Fix issue where lock from rpmdb(Sqlite3 backend) is dropped when it shouldn't 1.3.3 - Improve dpkg support (Stephen Tridgell) - Fix issue when no initial mount points (wjhunter3) - Add RuntimeDirectory to the systemd service file - Update code to limit the number group id's logged - Improve mount point detection code - Double the amount of groups a user could have (32) - Small performance improvement by not double rewinding descriptor - Improve logging: make stderr output more colourful; add timestamps (Kangie) - Identify ruleset being loaded by a sha hash of the rule file (John Wass) - Add 22-buildroot.rules to use if the machine builds software - Add --with-asan for configure 1.3.2 - Remove LimitNOFILE and instead setrlimit more carefully - Sync q_size to the documentation - Fix multiple memory leaks 1.3.1 - Fix not complete patch for filter file renaming 1.3 - Be consistent in updating and removing file system marks - Add escaping to /proc/mount entries - Revise escaping of trust files - Add LimitNOFILE to the service file - Add dpkg support (Stephen Tridgell) - Add support for runtime reloading of rules 1.2 - On shutdown when running reports, if trust db empty warn (Nobuhiro Iwamatsu) - Extend state machine to skip opens after exec until dyn linker found - Control filtering of unwanted files in rpm backend with config file - Add support for logging rule number of decision in the audit event 1.1.7 - Re-add dropped FAN_MARK_MOUNT for monitoring events (Steven Brzozowski) - Make some updates to allow running without an rpm back successful 1.1.6 - Correct the optional inclusion of code based on HAVE_DECL_FAN_MARK_FILESYSTEM 1.1.5 - If in debug mode, do not write audit events to audit system - Update filesystems we dont care about - Add --check-path to fapolicyd-cli to locate missed files - Detect trusted static apps running programs by ld.so - Add support for using FAN_MARK_FILESYSTEM to see bind mounted accesses 1.1.4 - Fix descriptor leak on enqueue failure (Steven Brzozowski) - Switch SHA256 hashing to openssl - Add --check-status to fapolicyd-cli - If fapolicyd is already running, exit - Do trust db size check on all fapolicyd updates - Add bash completions 1.1.3 - Replace snprintf integer to char conversion with uitoa function - Update the locking between the main and decision threads - Speedup sha256 hashing by mmap'ing the object - Add OOMScoreAdjust to fapolicyd.service 1.1.2 - Release the update lock if starting trust db read operations errors - CVE-2022-1117 fapolicyd incorrectly detects the run time linker - Add the btrfs to the watch_fs config option - Fix a problem tracking trusted static apps that launch other apps 1.1.1 - Reorder patterns and loopholes in rule.d - Add support for subject ppid rule matching - Add support for reloading the trust database from SIGHUP 1.1 - Add support for a rules.d directory - Add --check-config, --check-watch_fs, and --check-trustdb to fapolicyd-cli - Add libgcrypt initialization - Break up all the rules so they can be installed in rules.d - Add text/x-nftables magic - Add interpreter for s390x, ppc64le 1.0.4 - Tighten up ELF detection - Add support for multiple trust files in a trust.d directory - Add troubleshooting info for when the trust db is full - In permissive mode, allow audit events when rules say to log it - Add new rpm_sha256_only config option to the daemon - Escape whitespaces in file names put into the file trust database 1.0.3 - Add startup and shutdown syslog message - fapolicyd-cli open trustdb without locking to prevent daemon hang - If db migration fails due to unlinking problem, fail startup - Do not exit on fanotify_event read failure - Add application/javascript to Language macro 1.0.2 - Add Group ID support for rules - Add test cases for avl library - Update support for multiple copies of a trusted executable - Add support for dynamic trust updating 1.0.1 - If trust db is empty when fapolicyd-cli dumps it, say its empty - Make fapolicyd-cli buffer bigger for rule listing - Fix ignored db errors from check_trust_database - Adjust ELF x-object detection - Do device mime-type detection in-house instead of libmagic - Allow arbitrarily large group statements - Fix logging of object trust - Correct denial accounting - Add new form of LD_PRELOAD pattern detection - Fix mount reading routine to get it all - Update languages kept from /usr/share 1.0 - Add file size, IMA, and sha256 based integrity checking - Add ability to send decision results to syslog - Add ability to define the format of the syslog event - Add support for sets in rules - Add support for dumping the trustdb by fapolicyd-cli - Print a warning if rpm backend doesn't have a sha256 hash - In rpm backend, add back javascript from /usr/share 0.9.4 - Fix pattern detection in light of EXEC_PERM events - Conserve memory by dropping unneeded lists after startup - Do full reset of subject credentials when execve finishes - Drop files in /usr/share, /usr/src, and /usr/include to reduce memory use - Add error checking of the trust database - Fixed threading issue during rpm update - Add option to delete the trust database to cli, --delete-db - Add option to cli to add/delete/update the file trust database 0.9.3 - In fapolicyd-cli, add a --list option to list rules - Change lmdb to use writable mmap for startup performance improvment - Change the database to support duplicate keys - Provide a magic override file and use it during file inspection - Update rules to match new magic overrides - Add --ftype command to fapolicyd-cli - Add database statistics to usage report 0.9.2 - Split codebase into daemon, library and cli - Add Admin defined trust database - Make use of librpm optional - Updated the man pages - Setting boost, queue, user, and group on the command line are deprecated 0.9.1 - Make watched filesystems configurable - Improve ELF file classification - Expose file type in debug output - Update rules for ansible and dracut - Skip config files in database check - Expand definition of doc files - Create new rule format exposing Subj and Obj trust - Redesign the rules for trust based rules 0.9 - Convert hashes to lowercase like sha256sum outputs - Use FAN_OPEN_EXEC_PERM for subject cache management - Add static pattern detection - Performance improvements - Switch from static mounts to hotplug configuration of mount points - Dont collect documentation in trust database - When path is longer than lmdb can store, use a sha512 hash (Attila Lakatos) - Cache subject trustworthiness information after lookup (Radovan Sroka) 0.8.10 - Fix segfault for rules whose subject is number oriented - When database problem is found on startup, rebuild database - Don't flush empty caches on database rebuild - Revise default settings for better performance 0.8.9 - Systemd usage updates - File permission adjustments based on selinux policy review - Fix unterminated reads of auid & sessionid values - Deprecate ld_preload pattern until new method exists 0.8.8 - Add FAN_OPEN_EXEC_PERM Support (Matthew Bobrowski) - Man page updates - Add dnf plugin to sync database when rpms install 0.8.7 - If the path has a top level symlinked dir, retry db lookup without /usr - Fix parsing of command line options (Matthew Bobrowski) - Add more validation of mount types (Matthew Bobrowski) - Elf parser updates (Matthew Bobrowski) 0.8.6 - Update object hash calculation to better determine uniqueness - Override rpm's signal handling - Use private database as trust store - Update the rules for python 3.6 and remove systemd exclusion - Rename exec_dir rule option unpackaged to untrusted - Remove unneeded rpm code - Add support for daemon config file - Allow database size to be configurable - Add permissive setting, q_size, and q_depth to usage report 0.8.5 - Update spec file and license info 0.8.4 - Mask signals from deadman's switch - Reinstate strong umask before writing report - Use pw_gid to set the group when changing gid - Allow the use of account names for auid & uid in rules - Support group option on command line 0.8.3 - Add audit support for the linux-4.15 kernel - Don't close report descriptor in report - Fix busy loop to use poll as originally intended - Relax timing on deadman's switch 0.8.2 - Add seccomp filter support - Fix leaked descriptor in exe_type processing - Add LRU cache for subject and objects - Create fapolicyd user on install - Update systemd service file to run as user fapolicyd - Adjust inter-thread queue default size - Write statistics on shutdown - Change attribute access to hash table - Deny access to stale pid's or fd's - Add new pattern subject detection - Add executable report on shutdown - Add --no-details to suppress file/exe names on shutdown report 0.8.1 - Documentation updates - Update rules - Output how many rules are loaded in debug mode - Add user commandline option 0.8 - Initial public release fapolicyd-1.4.3/IMA-setup-RHEL8-example.md000066400000000000000000000107251513023701500200320ustar00rootroot00000000000000# How to Set Up IMA (Integrity Measurement Architecture) ## Step-by-Step Guide ### 1. Ensure `securityfs` is mounted: ```bash mount -l | grep securityfs ``` Example output: ``` # mount -l | grep securityfs securityfs on /sys/kernel/security type securityfs (rw,nosuid,nodev,noexec,relatime) ``` ### 2. Confirm IMA kernel options are configured: ```bash grep -e IMA -e INTEGRITY /boot/config-$(uname -r) ``` Example output: ``` $ grep -e IMA -e INTEGRITY /boot/config-4.18.0-80.11.2.e18_0.x86_64 CONFIG BLK _DEV_INTEGRIT’ CONFIG _KEXEC_BZIMAGE_VERIFY_SIG=-y # CONFIG WIMAX is not set CONFIG _DM_INTEGRITY=m CONFIG _MLXSW_MINIMAL=m CONFIG_FB_CFB_IMAGEBLIT=y CONFIG_FB_SYS_IMAGEBLIT=m CONFIG_FRAMEBUFFER_CONSOLE_DETECT_PRIMARY=y CONFIG_HID_PRIMAX=m CONFIG_INTEGRITY=y CONFIG_INTEGRITY_SIGNATURE=y CONFIG INTEGRITY ASYMMETRIC KEYS=y CONFIG_INTEGRITY_TRUSTED_KEYRING=y CONFIG INTEGRITY PLATFORM KEYRING=y CONFIG_INTEGRITY_AUDIT=y CONFIG_IMA=y CONFIG_IMA_MEASURE_PCR_IDX=10 CONFIG_IMA_LSM RULES-y_ # CONFIG INA TEMPLATE is not set CONFIG_IMA_NG_TEMPLATE=y # CONFIG INA SIG TEMPLATE is not set CONFIG_IMA_DEFAULT_TEMPLATE="ima-ng” CONFIG _IMA DEFAULT HASH SHAL=y # CONFIG_IMA_DEFAULT_HASH_SHA2S6 is not set CONFIG _IMA_DEFAULT_HASH="shal" # CONFIG_IMA WRITE POLICY is not set # CONFIG_IMA READ POLICY is not set CONFIG_IMA_APPRAISE=y CONFIG_IMA_APPRAISE_BOOTPARAM=y CONFIG_IMA_TRUSTED_KEYRING=y # CONFIG IMA BLACKLIST KEYRING is not set # CONFIG IMA LOAD _XS09 is not set ``` ### 3. Ensure the file system supports `i_version`: - `ext4` has `i_version` enabled by default. - Other filesystems like XFS and ext3 require it to be explicitly enabled in `/etc/fstab`. Example `/etc/fstab` entry: ``` [shearerd@awc-devel fapolicyd]$ cat /etc/fstab /etc/fstab Created by anaconda on Wed Apr 22 11:31:17 2020 See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info After editing this file, run ‘systemctl daemon-reload' to update systemd # # # # # Accessible filesystems, by reference, are maintained under '/dev/disk/' # # # # units generated from this file. # /dev/mapper/cl_awc--devel-root / ext4 defaults, iversion ai UUID=f26b7db8 -4935-4507-9722-79ad9eb5c55b /boot ext4 defaults 12 /dev/mapper/cl_awc--devel-swap swap swap defaults 00 ``` Check if `i_version` is enabled: ```bash mount -l | grep version ``` Example output: ``` [shearerd@awc-devel fapolicyd]$ mount -l | grep version /dev/mapper/cl_awc--devel-root on / type ext4 (rw,relatime,seclabel,i version) [shearerd@awc-devel fapolicyd]$ | ``` ### 4. Enable IMA in appraise fix mode: **a.** Backup the grub config: ```bash cp /etc/default/grub /etc/default/grub.orig ``` **b.** Edit `/etc/default/grub` to include IMA settings: ``` 3_TIMEOUT=5 GRUB_DISTRIBUTOR="5 (sed GRUB_DEFAULT=2aved GRUB_DISABLE_SUBMENU-true GRUB_TERMINAL_OUTPUT="console” GRUB_CMDLINE_EINUX="crashkernel=auto ima_policy=tcb ima_appraise_tcb ima_appraise=fix ima_hash=sha256 ima_audit=1 resume=/dev/mapper/cl-swap rd.ivm.lv=cl/root rd.ivm.iv=cl/swap rhgb quiet” GRUB_DISABLE_RECOVERY="true" GRUB_ENABLE_BLSCFG=true release .*5,,g" /etc/system-release)” ``` **c.** Rebuild grub: - BIOS-based machines: ```bash grub2-mkconfig -o /boot/grub2/grub.cfg ``` - UEFI-based machines: ```bash grub2-mkconfig -o /boot/efi/EFI/redhat/grub.cfg ``` **d.** Reboot machine: ```bash reboot ``` **e.** Confirm that the IMA directory structure is present: ```bash ls /sys/kernel/security/ima ``` **f.** Label files: ```bash find / -fstype ext4 -type f -uid 0 -exec dd if='{}' of=/dev/null count=0 status=none \; ``` **g.** [OPTIONAL] View measurements: ```bash tail -f /sys/kernel/security/ima/ascii_runtime_measurements ``` ### 5. View contents of security labels: ```bash # getfattr -m ^security --dump -e hex /bin/bash getfattr: Removing leading '/' from absolute path names # file: bin/bash security.ima=0x040420557151302622baSc281893436a62164538C77bd43452267fa2da6c3cf23ed8 security.selinux=0x73797374656d5£753a6£626a6563745£723a7368656CEcS£E657865635£743a733000 ``` ### 6. Install fapolicyd ```bash # dnf install fapolicyd ``` ### 7. Set IMA in fapolicyd.conf ```bash # vim /etc/fapolicyd/fapolicyd.conf ... integrity = ima ... ``` ### 7. Start fapolicyd ```bash # fapolicyd --debug-deny ``` If no errors, you are good to go. If following appears hashes are not present in extended attributes. ```bash 10/28/24 06:13:21 [ ERROR ]: IMA integrity checking selected, but the extended attributes can't be read 10/28/24 06:13:21 [ ERROR ]: Exiting due to bad configuration ``` fapolicyd-1.4.3/INSTALL.tmp000066400000000000000000000001411513023701500153120ustar00rootroot00000000000000Installation Instructions ************************* See README.md for build, install, testing. fapolicyd-1.4.3/Makefile.am000066400000000000000000000007341513023701500155260ustar00rootroot00000000000000 SUBDIRS = src init doc rules.d TEST_FIXTURES = \ src/tests/fixtures/filter-minimal.conf \ src/tests/fixtures/filter-cases.txt \ src/tests/fixtures/broken-filter.conf \ init/fapolicyd-filter.conf \ src/tests/fixtures/rules-valid.rules EXTRA_DIST = ChangeLog AUTHORS NEWS README.md INSTALL fapolicyd.spec \ dnf/fapolicyd-dnf-plugin.py autogen.sh \ $(TEST_FIXTURES) dist_check_DATA = $(TEST_FIXTURES) clean-generic: rm -rf autom4te*.cache rm -f *.rej *.orig *.lang *.list fapolicyd-1.4.3/NEWS000066400000000000000000000000001513023701500141530ustar00rootroot00000000000000fapolicyd-1.4.3/README.md000066400000000000000000000771531513023701500147620ustar00rootroot00000000000000File 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. 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. This report can be scheduled to be written 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. 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 | What it does | |------------------------|--------------------------------------------| | --check-config | Opens fapolicyd.conf and parses it to see if there are any syntax errors in the file. | | --check-path | Check that every file in $PATH is in the trustdb. (New in 1.1.5) | | --check-status | Output internal metrics kept by the daemon. (New in 1.1.4) | | --check-trustdb | Check the trustdb against the files on disk to look for mismatches that will cause problems at run time. | | --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. | | --check-ignore_mounts | Check the configured mounts that are ignored to see that they are mounted noexec and there are no suspicious files in the partition. (New in 1.4) | | --test-filter | Test a path to a file against the filter rules to determine if a file will be trusted. (New in 1.3.7) | MANAGING TRUST -------------- Fapolicyd use lmdb as a backend database for its trusted software list. You can find this database in /var/lib/fapolicyd/. This list gets updated whenever packages are installed by dnf or rpm by a rpm plugin. The files that go into the trust database from rpm go through a filter to eliminate as many unimportant files as possible so that the trust database is concise. To see what kinds of files are in the trust database, you can try this: ``` fapolicyd-cli -D | awk '{print $2}' | awk -F/ '{ base=$NF ext="*" if (base !~ /^\./ && base ~ /\./) { n=split(base,a,"."); ext="*."a[n] } path="" for(i=1;i "/sys/kernel/security/ipe/Ex Policy/active" ``` and so on. Since they can all be disabled, the fact that an admin can issue a service stop command is not a unique weakness. 6) How do you prevent race conditions on startup? Can something execute before the daemon takes control? One of the design goals is to take control before users can login. Users are the main problem being addressed. They can pip install apps to the home dir or do other things an admin may wish to prevent. Only root can install things that run before login. And again, root can change the rules or turn off the daemon. Another design goal is to prevent malicious apps from running. Suppose someone guesses your password and they login to your account. Perhaps they wish to ransomware your home dir. The app they try to run is not known to the system and will be stopped. Or suppose there is an exploitable service on your system. The attacker is lucky enough to pop a shell. Now they want to download privilege escalation tools or perhaps an LD_PRELOAD key logger. Since neither of these are in the trust database, they won't be allowed to run. This is really about stopping escalation or exploitation before the attacker can gain any advantage to install root kits. If we can do that, UEFI secure boot can make sure no other problems exist during boot. Wrt to the second question being asked, fapolicyd starts very early in the boot process and startup is very fast. It's running well before other login daemons. NOTES ----- * It's highly recommended to run in permissive mode while you are testing the daemon's policy. * Stracing the fapolicyd daemon WILL DEADLOCK THE SYSTEM. * About shell script restrictions...there's not much difference between running a script or someone typing things in by hand. The aim at this point is to check that any program it calls meets the policy. * Some interpreters do not immediately read all lines of input. Rather, they read content as needed until they get to end of file. This means that if they do stuff like networking or sleeping or anything that takes time, someone with the privileges to modify the file can add to it after the file's integrity has been checked. This is not unique to fapolicyd, it's simply how things work. Make sure that trusted file permissions are not excessive so that no unexpected file content modifications can occur. * If for some reason rpm database errors are detected, you may need to do the following: ``` 1. db_verify /var/lib/rpm/Packages if OK, then 2. rm -f /var/lib/rpm/__db* 3. rpm --rebuilddb ``` [1] - https://git.kernel.org/pub/scm/linux/kernel/git/jack/linux-fs.git/commit/?id=66917a3130f218dcef9eeab4fd11a71cd00cd7c9 fapolicyd-1.4.3/RELEASE_PROCESS.md000066400000000000000000000027051513023701500162720ustar00rootroot00000000000000# fapolicyd Release Process 1. **Clean the repository** ```bash git clean -xfd ``` 2. **Bootstrap and build** ```bash ./autogen.sh ./configure --with-audit --with-rpm --disable-shared make ``` 3. **Run the test suite** ```bash make check ``` 4. **Build with Address Sanitizer** Reconfigure with `--with-asan`, rebuild, and run both the daemon and command-line client to ensure there are no ASAN failures. ```bash ./configure --with-audit --with-rpm --disable-shared --with-asan make sudo ./src/fapolicyd --debug-deny sudo ./src/fapolicyd-cli --dump-db ``` 5. **Update version numbers** - `configure.ac` line 2 - `fapolicyd.spec` line 12 - Document the changes in `ChangeLog`. 6. **Create the source tarball** ```bash ./autogen.sh ./configure --with-audit --with-rpm --disable-shared make dist ``` 7. **Tag the release** ```bash git tag -s -m "fapolicyd-X.Y.Z" vX.Y.Z git push origin vX.Y.Z ``` 8. **Sign the tarball** ```bash sha256sum fapolicyd-X.Y.Z.tar.gz > fapolicyd-X.Y.Z.tar.gz.sum gpg --armor --detach-sign fapolicyd-X.Y.Z.tar.gz gpg --clearsign fapolicyd-X.Y.Z.tar.gz.sum ``` 9. **Publish on GitHub** Create a new release with the tag, include notes from `ChangeLog`, and upload the following files: - `fapolicyd-X.Y.Z.tar.gz` - `fapolicyd-X.Y.Z.tar.gz.asc` - `fapolicyd-X.Y.Z.tar.gz.sum` - `fapolicyd-X.Y.Z.tar.gz.sum.asc` fapolicyd-1.4.3/TODO000066400000000000000000000003341513023701500141560ustar00rootroot00000000000000Userspace ========= * 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 fapolicyd-1.4.3/autogen.sh000077500000000000000000000002141513023701500154640ustar00rootroot00000000000000#! /bin/sh set -x -e # --no-recursive is available only in recent autoconf versions autoreconf -fv --install cp INSTALL.tmp INSTALL || true fapolicyd-1.4.3/configure.ac000066400000000000000000000142441513023701500157610ustar00rootroot00000000000000AC_REVISION($Revision: 1.3 $)dnl AC_INIT([fapolicyd],[1.4.3]) 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) 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 ]]) 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) 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 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 " fapolicyd-1.4.3/deb/000077500000000000000000000000001513023701500142205ustar00rootroot00000000000000fapolicyd-1.4.3/deb/README.Debian000066400000000000000000000005411513023701500162610ustar00rootroot00000000000000fapolicyd for Debian This is a simple application whitelisting daemon for Linux. RUNTIME DEPENDENCIES -------------------- * kernel >= 4.20 (Must support FANOTIFY_OPEN_EXEC_PERM. See [1] below.) After configuring fapolicyd with /etc/fapolicyd/fapolicyd.conf and adding the desired set of rules to /etc/fapolicyd/rules.d/ start the fapolicyd service. fapolicyd-1.4.3/deb/build_deb.sh000077500000000000000000000005511513023701500164710ustar00rootroot00000000000000#! /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 .. fapolicyd-1.4.3/deb/postinst000077500000000000000000000005131513023701500160300ustar00rootroot00000000000000#! /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 fapolicyd:fapolicyd /run/fapolicyd/ fapolicyd-1.4.3/deb/rules000077500000000000000000000003571513023701500153050ustar00rootroot00000000000000#!/usr/bin/make -f %: dh $@ --with autoreconf override_dh_auto_configure: dh_auto_configure -- \ --with-audit \ --disable-shared \ --without-rpm \ --with-deb \ --prefix=/usr override_dh_autoreconf: dh_autoreconf -- ./autogen.sh fapolicyd-1.4.3/dnf/000077500000000000000000000000001513023701500142355ustar00rootroot00000000000000fapolicyd-1.4.3/dnf/fapolicyd-dnf-plugin.py000066400000000000000000000016731513023701500206310ustar00rootroot00000000000000#!/usr/bin/python3 import dnf import os import stat import sys class Fapolicyd(dnf.Plugin): name = "fapolicyd" pipe = "/run/fapolicyd/fapolicyd.fifo" file = None def __init__(self, base, cli): pass def transaction(self): if not os.path.exists(self.pipe): sys.stderr.write("Pipe does not exist (" + self.pipe + ")\n") sys.stderr.write("Perhaps fapolicy-plugin does not have enough permissions\n") sys.stderr.write("or fapolicyd is not running...\n") return if not stat.S_ISFIFO(os.stat(self.pipe).st_mode): sys.stderr.write(self.pipe + ": is not a pipe!\n") return try: self.file = open(self.pipe, "w") except PermissionError: sys.stderr.write("fapolicy-plugin does not have write permission: " + self.pipe + "\n") return self.file.write("1\n") self.file.close() fapolicyd-1.4.3/doc/000077500000000000000000000000001513023701500142335ustar00rootroot00000000000000fapolicyd-1.4.3/doc/Makefile.am000066400000000000000000000020331513023701500162650ustar00rootroot00000000000000# 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.conf.5 \ rpm-filter.conf.5 \ fapolicyd-filter.conf.5 fapolicyd-1.4.3/doc/fagenrules.8000066400000000000000000000021521513023701500164570ustar00rootroot00000000000000.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). fapolicyd-1.4.3/doc/fapolicyd-cli.8000066400000000000000000000142431513023701500170470ustar00rootroot00000000000000.TH "FAPOLICYD-CLI" "8" "Dec 2021" "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 is a tool to tell the daemon that it needs to update the trust database. Normally, the daemon learns that the trust database needs updating because it uses a dnf plugin to inform it. However, you may install an rpm by hand and it can't see that a system package was installed or updated. Or perhaps the admin updates the fapolicyd.trust file and would like the changes to take effect immediately. In either of these cases, you would need to tell the daemon that it needs to do an update by running this command. .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 internal performance statistics. See also the fapolicyd.conf option \fBreport_interval\fP. .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 \-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 FILTER_FILE against the given path and emit a rule-by-rule trace ending with "decision include" or "decision exclude". This let's you know if the file will be included in the trust database or not. .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 Only for the \-\-check-ignore_mounts, print a list of files that do 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.trust (5), and .BR fapolicyd.conf (5) .SH AUTHOR Zoltan Fridrich fapolicyd-1.4.3/doc/fapolicyd-filter.conf.5000066400000000000000000000076341513023701500205140ustar00rootroot00000000000000.TH FAPOLICYD_FILTER.CONF: "15" "June 2023" "Red Hat" "System Administration Utilities" .SH NAME fapolicyd-filter.conf \- fapolicyd filter configuration file .SH DESCRIPTION The filter controls which files from a trust source are added to the TrustDB. Rules are processed from top to bottom, with indentation narrowing the scope of the parent rule. A matching plus (+) rule causes a file to be included; a minus (-) rule excludes it. If no rule matches, the file is excluded by default. The filter is consulted only when trust data is imported: during system updates and when the daemon starts and rebuilds its database. Runtime policy decisions use the TrustDB itself, not the filter, so changes to the filter affect new or rebuilt trust entries rather than live access checks. Valid line starts with character '+', '-' or '#' for comments. The rest of the line contains a path specification. Space can be used as indentation to add more specific filters to the previous one. Note, that only one space is required for one level of an indent. If there are multiple specifications on the same indentation level they extend the previous line with lower indentation, usually a directory. The path may be specified using the glob pattern. A directory specification has to end with a slash ‘/’; without it the rule is treated as a file glob and the directory decision from the parent rule remains in effect. If the result was a plus (+), the respective file from a trust source is imported to the TrustDB. Vice versa, if the result was a minus (-), the respective file is not imported. From a performance point of view it is better to design an indented filter because in the ideal situation each component of the path is compared only once. In contrast to it, a filter without any indentation has to contain a full path which makes the pattern more complicated and thus slower to process. The motivation behind this is to have a flexible configuration and keep the TrustDB as small as possible to make the look-ups faster. .nf .B # this is simple allow list .B - /usr/bin/some_binary1 .B - /usr/bin/some_binary2 .B + / .fi .nf .B # this is the same .B + / .B \ + usr/bin/ .B \ \ - some_binary1 .B \ \ - some_binary2 .fi .nf .B # this is similar allow list with a wildcard .B - /usr/bin/some_binary? .B + / .fi .nf .B # this is similar with another wildcard .B + / .B \ - usr/bin/some_binary* .fi .nf .B # keeps everything except usr/share except python and perl files .B # /usr/bin/ls - result is '+' .B # /usr/share/something - result is '-' .B # /usr/share/abcd.py - result is '+' .B + / .B \ - usr/share/ .B \ \ + *.py .B \ \ + *.pl .fi .SH THEORY OF OPERATION .PP The filter configuration is parsed into a tree where each node represents a path fragment and whether it is included or excluded. Each level of indentation in the configuration file becomes another depth level in that tree. During evaluation the daemon iteratively walks the tree with an explicit stack rather than recursion, advancing through the path as fragments match. This approach keeps evaluation deterministic and prevents deep call stacks, but it also means filter nesting cannot exceed 64 levels; longer hierarchies are rejected and reported as depth errors. .SH TESTING FILTERS .PP Administrators can validate how a change to \fIfapolicyd-filter.conf\fP behaves before rebuilding the trust database. Use \fBfapolicyd-cli --test-filter /path/to/file\fP to trace how the configuration is applied to a specific path and see the final include/exclude decision. When adding or updating trust entries, combine \fB--file add\fP or \fB--file update\fP with \fB--filter\fP so only paths that survive the filter are processed; this is useful when pointing the tool at directories to ensure the filter omits unwanted content while you test. .SH FILES .B /etc/fapolicyd/fapolicyd-filter.conf .SH "SEE ALSO" .BR fapolicyd (8), .BR fapolicyd-cli (1) .BR fapolicy.rules (5) and .BR glob (7) .SH AUTHOR Radovan Sroka fapolicyd-1.4.3/doc/fapolicyd.8000066400000000000000000000106021513023701500162750ustar00rootroot00000000000000.TH "FAPOLICYD" "8" "March 2022" "Red Hat" "System Administration Utilities" .SH NAME fapolicyd \- File Access Policy Daemon .SH SYNOPSIS \fBfapolicyd\fP [\fIoptions\fP] .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 leave the daemon in the foreground for debugging. Event information is written to stderr so that policy decisions can be observed. .TP .B \-\-debug\-deny leave the daemon in the foreground for debugging. Event information is written to stderr only when the decision is to deny access. .TP .B \-\-permissive the daemon will allow file access regardless of the policy decision. This is useful for debugging rules before making them permanent. .TP .B \-\-mounts=PATH only in debug mode will the daemon use this flag to override the source of fanotify mounts with the contents of the file at the specified path. This file must be the same format as /proc/mounts, which allows one to head, tail, or grep a sublist from /proc/mounts into a file passed with this arg. .TP .B \-\-no-details when fapolicyd ends, it dumps a usage report with various statistics that may be useful for tuning performance. It can also detail which processes it knew about and files being accessed by them. This can be useful for forensics investigations. In some settings, this may not be desirable as the file names may be sensitive. Using this option removes process and file names leaving only the statistics. The default without giving this option is to generate a full report. .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 it's internal statistics to /var/run/fapolicyd.state .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 - internal performance metrics .SH "SEE ALSO" .BR fapolicyd-cli (8), .BR fapolicyd.rules (5), .BR fapolicyd.trust (5), .BR fapolicyd-filter.conf (5), .BR fagenrules (8), and .BR fapolicyd.conf (5) .SH AUTHOR Steve Grubb fapolicyd-1.4.3/doc/fapolicyd.conf.5000066400000000000000000000267761513023701500172410ustar00rootroot00000000000000.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 a recurring dump of internal performance statistics to the \fBfapolicyd.state\fP file. The default value of 0 disables interval reporting. .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) and .BR fapolicy.rules (5). .SH AUTHOR Steve Grubb fapolicyd-1.4.3/doc/fapolicyd.rules.5000066400000000000000000000264011513023701500174270ustar00rootroot00000000000000.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 fapolicyd-1.4.3/doc/fapolicyd.trust.5000066400000000000000000000032101513023701500174470ustar00rootroot00000000000000.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 fapolicyd-1.4.3/doc/rpm-filter.conf.5000066400000000000000000000006571513023701500173360ustar00rootroot00000000000000.TH FAPOLICYD.FILTER: "26" "April 2023" "Red Hat" "System Administration Utilities" .SH NAME fapolicyd-filter.conf \- fapolicyd filter configuration file .SH DESCRIPTION The file .I /etc/fapolicyd/rpm-filter.conf was migrated to .I /etc/fapolicyd/fapolicyd-filter.conf or see .BR fapolicyd-filter.conf(5). .SH "SEE ALSO" .BR fapolicyd (8), .BR fapolicyd-cli (1) .BR fapolicy.rules (5) and .BR glob (7) .SH AUTHOR Radovan Sroka fapolicyd-1.4.3/fapolicyd-selinux-var-run.patch000066400000000000000000000022701513023701500215370ustar00rootroot00000000000000From 750c5e288f8253c71a9722da960addb078aee93c Mon Sep 17 00:00:00 2001 From: Zdenek Pytela Date: Tue, 6 Feb 2024 21:17:27 +0100 Subject: [PATCH] Rename all /var/run file context entries to /run With the 1f76e522a ("Rename all /var/run file context entries to /run") selinux-policy commit, all /var/run file context entries moved to /run and the equivalency was inverted. Subsequently, changes in fapolicyd.fc need to be done, too, in a similar manner. --- fapolicyd.fc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fapolicyd-selinux-master/fapolicyd.fc b/fapolicyd-selinux-master/fapolicyd.fc index 2bdc7aa..d081dc8 100644 --- a/fapolicyd-selinux-master/fapolicyd.fc +++ b/fapolicyd-selinux-master/fapolicyd.fc @@ -8,6 +8,6 @@ /var/log/fapolicyd-access.log -- gen_context(system_u:object_r:fapolicyd_log_t,s0) -/var/run/fapolicyd(/.*)? gen_context(system_u:object_r:fapolicyd_var_run_t,s0) +/run/fapolicyd(/.*)? gen_context(system_u:object_r:fapolicyd_var_run_t,s0) -/var/run/fapolicyd\.pid -- gen_context(system_u:object_r:fapolicyd_var_run_t,s0) +/run/fapolicyd\.pid -- gen_context(system_u:object_r:fapolicyd_var_run_t,s0) -- 2.44.0 fapolicyd-1.4.3/fapolicyd.spec000066400000000000000000000241131513023701500163150ustar00rootroot00000000000000#ASAN %%global asan_build 1 #ELN %%global eln_build 1 %if %{defined eln_build} %global selinuxtype targeted %global moduletype contrib %define semodule_version master %endif Summary: Application Whitelisting Daemon Name: fapolicyd Version: 1.4.3 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 # applied in CI only Patch2: fapolicyd-selinux-var-run.patch %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 %if %{defined eln_build} %if 0%{?fedora} < 40 %define selinux_var_run 1 %endif %if 0%{?rhel} < 10 %define selinux_var_run 1 %endif %if %{defined selinux_var_run} %patch -P2 -R -p1 -b .selinux %endif %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) %{_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 Nov 26 2025 Petr Lautrbach - 1.4.3-1 - New release fapolicyd-1.4.3/init/000077500000000000000000000000001513023701500144315ustar00rootroot00000000000000fapolicyd-1.4.3/init/Makefile.am000066400000000000000000000012421513023701500164640ustar00rootroot00000000000000EXTRA_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 fapolicyd-1.4.3/init/fagenrules000066400000000000000000000065461513023701500165220ustar00rootroot00000000000000#!/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}" # 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 fapolicyd-1.4.3/init/fapolicyd-filter.conf000066400000000000000000000042231513023701500205360ustar00rootroot00000000000000# --------------------------------------------------------------------------- # Overview of the default fapolicyd-filter.conf # # Decision flow # 1. Rules are read top-to-bottom. Leading “+” means the path is # considered essential and will be added to the trust database; # leading “-” means the path is skipped. # 2. Indentation creates a parent-child relationship. A child rule # refines the decision of its parent. Think “allow everything # under /usr/, then deny one sub-tree, then allow one file type # back in”, etc. # 3. If no rule matches at the end of the scan, the parent’s decision # stands; the very last top-level rule is therefore the catch-all. # # What the shipped rules do # • Start with “+ /” → everything allowed by default. # • Deny /usr/include/ entirely. (- usr/include/) # • Deny /usr/share/ but re-allow developer artefacts: # *.py *.pl etc # • Deny /usr/src/kernel*/ except: # */scripts/* and */tools/objtool/* # • Deny html, md, conf, and other non-executable files # # For full syntax see: man 5 fapolicyd-filter.conf # --------------------------------------------------------------------------- + / - usr/include/ - usr/share/ # Python byte code + *.py? # Python text files + *.py # Some apps have a private bin + */bin/* # Some apps have a private libexec + */libexec/* # Ruby + *.rb # Perl + *.pl # System tap + *.stp # Javascript + *.js # Java archive + *.jar # M4 + *.m4 # PHP + *.php # Perl Modules + *.pm # Lua + *.lua # Java + *.class # Typescript + *.ts # Typescript JSX + *.tsx # Lisp + *.el # Compiled Lisp + *.elc # Allow Lmod to run in bash profiles.d/modules.sh + lmod/*/bash # Drop all source (kernel/nvidia) - usr/src/*/ # Allow scripts and tools + */scripts/* # Drop any source code - *.c - *.h + */tools/objtool/* # Drop any xz - *.xz # Drop any html - *.html # Drop any json - *.json # Drop any markdown - *.md # Drop any configuration - *.conf # drop png files scattered all over - *.png fapolicyd-1.4.3/init/fapolicyd-magic000066400000000000000000000013661513023701500174120ustar00rootroot000000000000000 string/wt #!\ /bin/rc Plan 9 shell script text executable !:mime text/x-plan9-shellscript 0 string/wt #!\ /usr/bin/gjs Gnome Javascript text executable !:mime application/javascript 0 belong 0x6f0d0d0a python 3.10 byte-compiled !:mime application/x-bytecode.python 0 search/1/wt #!\ /usr/libexec/platform-python Python script text executable !:strength + 15 !:mime text/x-python 0 string/wt #!\ /usr/bin/guile Guile script text executable !:mime text/x-script.guile 0 string/wt #!\ /usr/sbin/nft Netfilter tables script text executable !:mime text/x-nftables 0 belong 0x4d5a9000 Portable executable !:mime application/vnd.microsoft.portable-executable 0 string/wt #!\ /usr/bin/bpftrace Bpf tracing script text executable !:mime text/x-bpftrace fapolicyd-1.4.3/init/fapolicyd-tmpfiles.conf000066400000000000000000000001651513023701500210750ustar00rootroot00000000000000d /run/fapolicyd 0770 root fapolicyd - d /var/lib/fapolicyd 770 root fapolicyd - Z /etc/fapolicyd - root fapolicyd - fapolicyd-1.4.3/init/fapolicyd.bash_completion000066400000000000000000000102421513023701500214720ustar00rootroot00000000000000# 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-status --check-trustdb \ --check-watch_fs --check-ignore_mounts --delete-db --dump-db \ --file --filter --ftype --help --list --update --reload-rules \ --test-filter --trust-file --verbose -h -d -D -f -t -l -u -r" # 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 ;; --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 prev opts COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" opts="--debug --debug-deny --help --mounts --no-details --permissive --version" case ${prev} in --mounts) 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 } complete -F _fapolicydcli fapolicyd-cli complete -F _fapolicyd fapolicyd # ex: filetype=sh fapolicyd-1.4.3/init/fapolicyd.conf000066400000000000000000000010751513023701500172550ustar00rootroot00000000000000# # 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 fapolicyd-1.4.3/init/fapolicyd.service000066400000000000000000000014341513023701500177670ustar00rootroot00000000000000# 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 fapolicyd-1.4.3/init/fapolicyd.trust000066400000000000000000000003451513023701500175100ustar00rootroot00000000000000# AUTOGENERATED FILE VERSION 2 # This file contains a list of trusted files # # FULL PATH SIZE SHA256 # /home/user/my-ls 157984 61a9960bf7d255a85811f4afcac51067b8f2e4c75e21cf4f2af95319d4ed1b87 fapolicyd-1.4.3/m4/000077500000000000000000000000001513023701500140065ustar00rootroot00000000000000fapolicyd-1.4.3/m4/dyn_linker.m4000066400000000000000000000005571513023701500164150ustar00rootroot00000000000000AC_DEFUN([LD_SO_PATH], [ bash_path=`command -v bash` xpath1=`readelf -e $bash_path | grep Requesting | sed 's/.$//' | rev | cut -d" " -f1 | rev` xpath=`realpath $xpath1` if test ! -f "$xpath" ; then AC_MSG_ERROR([Cant find the dynamic linker]) fi echo "dynamic linker is.....$xpath" AC_DEFINE_UNQUOTED(SYSTEM_LD_SO, ["$xpath"], [dynamic linker]) ]) fapolicyd-1.4.3/rules.d/000077500000000000000000000000001513023701500150425ustar00rootroot00000000000000fapolicyd-1.4.3/rules.d/10-languages.rules000066400000000000000000000007351513023701500203070ustar00rootroot00000000000000# This file contains the list of all identified languages on the system %languages=application/x-bytecode.ocaml,application/x-bytecode.python,application/java-archive,text/x-java,application/x-java-applet,application/javascript,text/javascript,text/x-awk,text/x-gawk,text/x-lisp,application/x-elc,text/x-lua,text/x-m4,text/x-nftables,text/x-perl,text/x-php,text/x-script.python,text/x-python,text/x-R,text/x-ruby,text/x-script.guile,text/x-tcl,text/x-luatex,text/x-systemtap fapolicyd-1.4.3/rules.d/20-dracut.rules000066400000000000000000000002031513023701500176120ustar00rootroot00000000000000# Carve out an exception for dracut's initramfs building allow perm=any uid=0 : dir=/var/tmp/ allow perm=any uid=0 trust=1 : all fapolicyd-1.4.3/rules.d/21-updaters.rules000066400000000000000000000002741513023701500201700ustar00rootroot00000000000000# We have to carve out an exception for the system updaters # or things go very bad (deadlock). allow perm=open exe=/usr/bin/rpm : all allow perm=open exe=%python3_path% comm=dnf : all fapolicyd-1.4.3/rules.d/22-buildroot.rules000066400000000000000000000017411513023701500203450ustar00rootroot00000000000000# Exception for software builders. # # Software builders create lots of files. Since they were all just created, # fapolicyd has never seen them before and expensive integrity checking has # to be done which will fail because they are all untrusted. Since it's single # purpose is building applications, we need to carve out a permissive domain # for it to operate in. # # The buildroot is protected by file permissions. Noone except root and the # mock account can write to the buildroot. The buildroot is wiped clean after # a build so that nothing can persist. What the following rules say is that we # are trusting the mock account to create and access anything in it's buildroot. # Otherwise, the mock account can use anything that is trusted for any purpose # anywhere else. It is still restricted by user id permissions. # # The following uid and dir should be adjusted to fit your configuration. allow perm=any uid=mock : dir=/home/mock/rpmbuild allow perm=any uid=mock trust=1 : all fapolicyd-1.4.3/rules.d/30-patterns.rules000066400000000000000000000003411513023701500201740ustar00rootroot00000000000000# This file contains the list of all patterns. Only the ld_so pattern # is enabled by default. deny_audit perm=any pattern=ld_so : all #deny_audit perm=any pattern=ld_preload : all #deny_audit perm=any pattern=static : all fapolicyd-1.4.3/rules.d/40-bad-elf.rules000066400000000000000000000001451513023701500176310ustar00rootroot00000000000000# Do not allow malformed ELF even if trusted deny_audit perm=any all : ftype=application/x-bad-elf fapolicyd-1.4.3/rules.d/41-shared-obj.rules000066400000000000000000000003701513023701500203560ustar00rootroot00000000000000# Only allow known ELF libs - this is ahead of executable because typical # executable is linked with a dozen or more libraries. allow perm=open all : ftype=application/x-sharedlib trust=1 deny_audit perm=open all : ftype=application/x-sharedlib fapolicyd-1.4.3/rules.d/42-trusted-elf.rules000066400000000000000000000001071513023701500205750ustar00rootroot00000000000000# Allow trusted programs to execute allow perm=execute all : trust=1 fapolicyd-1.4.3/rules.d/43-known-elf.rules000066400000000000000000000006171513023701500202460ustar00rootroot00000000000000# Only allow known ELF Applications allow perm=execute all : ftype=application/x-executable trust=1 deny_audit perm=execute all : ftype=application/x-executable # This is a workaround for kernel thinking this is being executed because it # occurs during the execve call for an ELF binary. We catch actual execution # in the ld_so pattern rule. allow perm=execute all : path=%ld_so_path% trust=1 fapolicyd-1.4.3/rules.d/70-trusted-lang.rules000066400000000000000000000002171513023701500207530ustar00rootroot00000000000000# Allow any program to open trusted language files allow perm=open all : ftype=%languages trust=1 deny_audit perm=any all : ftype=%languages fapolicyd-1.4.3/rules.d/71-known-python.rules000066400000000000000000000004211513023701500210130ustar00rootroot00000000000000# Only allow system python executables and libs allow perm=any all : ftype=text/x-python trust=1 allow perm=open all : ftype=application/x-bytecode.python trust=1 deny_audit perm=any all : ftype=text/x-python deny_audit perm=any all : ftype=application/x-bytecode.python fapolicyd-1.4.3/rules.d/72-shell.rules000066400000000000000000000001401513023701500174460ustar00rootroot00000000000000# Allow all shell script execution and sourcing allow perm=any all : ftype=text/x-shellscript fapolicyd-1.4.3/rules.d/73-known-perl.rules000066400000000000000000000001741513023701500204430ustar00rootroot00000000000000# Only allow system perl files allow perm=any all : ftype=text/x-perl trust=1 deny_audit perm=any all : ftype=text/x-perl fapolicyd-1.4.3/rules.d/74-known-ocaml.rules000066400000000000000000000002371513023701500205750ustar00rootroot00000000000000# Only allow system Ocaml files allow perm=any all : ftype=application/x-bytecode.ocaml trust=1 deny_audit perm=any all : ftype=application/x-bytecode.ocaml fapolicyd-1.4.3/rules.d/75-known-php.rules000066400000000000000000000001711513023701500202670ustar00rootroot00000000000000# Only allow system PHP files allow perm=any all : ftype=text/x-php trust=1 deny_audit perm=any all : ftype=text/x-php fapolicyd-1.4.3/rules.d/76-known-ruby.rules000066400000000000000000000001741513023701500204650ustar00rootroot00000000000000# Only allow system ruby files allow perm=any all : ftype=text/x-ruby trust=1 deny_audit perm=any all : ftype=text/x-ruby fapolicyd-1.4.3/rules.d/77-known-lua.rules000066400000000000000000000001711513023701500202630ustar00rootroot00000000000000# Only allow system lua files allow perm=any all : ftype=text/x-lua trust=1 deny_audit perm=any all : ftype=text/x-lua fapolicyd-1.4.3/rules.d/78-known-wine.rules000066400000000000000000000002771513023701500204540ustar00rootroot00000000000000# Only allow system wine files allow perm=any all : ftype=application/vnd.microsoft.portable-executable trust=1 deny_audit perm=any all : ftype=application/vnd.microsoft.portable-executable fapolicyd-1.4.3/rules.d/90-deny-execute.rules000066400000000000000000000001141513023701500207370ustar00rootroot00000000000000# Deny execution for anything untrusted deny_audit perm=execute all : all fapolicyd-1.4.3/rules.d/91-deny-lang.rules000066400000000000000000000001321513023701500202170ustar00rootroot00000000000000# Deny all languages not explicitly enabled deny_audit perm=open all : ftype=%languages fapolicyd-1.4.3/rules.d/95-allow-open.rules000066400000000000000000000001051513023701500204220ustar00rootroot00000000000000# Allow everything else to open any file allow perm=open all : all fapolicyd-1.4.3/rules.d/Makefile.am000066400000000000000000000025731513023701500171050ustar00rootroot00000000000000# 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) fapolicyd-1.4.3/rules.d/README-rules000066400000000000000000000027221513023701500170550ustar00rootroot00000000000000This 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 fapolicyd-1.4.3/src/000077500000000000000000000000001513023701500142555ustar00rootroot00000000000000fapolicyd-1.4.3/src/Makefile.am000066400000000000000000000052071513023701500163150ustar00rootroot00000000000000SUBDIRS = 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 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 libfapolicyd_la_SOURCES = \ library/avl.c \ library/avl.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/escape.c \ library/escape.h \ library/event.c \ library/event.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/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_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/fapolicyd.c \ daemon/mounts.c \ daemon/mounts.h \ daemon/notify.c \ daemon/notify.h fapolicyd_cli_SOURCES = \ cli/fapolicyd-cli.c \ cli/file-cli.c \ cli/file-cli.h fapolicyd-1.4.3/src/cli/000077500000000000000000000000001513023701500150245ustar00rootroot00000000000000fapolicyd-1.4.3/src/cli/fapolicyd-cli.c000066400000000000000000001120751513023701500177150ustar00rootroot00000000000000/* * 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 #include // basename #include "policy.h" #include "database.h" #include "file-cli.h" #include "file.h" #include "fapolicyd-backend.h" #include "string-util.h" #include "daemon-config.h" #include "message.h" #include "llist.h" #include "avl.h" #include "fd-fgets.h" #include "paths.h" #include "filter.h" #include "file.h" bool verbose = false; 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 the deamon's internal performance statistics\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" "--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" #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 }, {"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; struct mount_scan_state { const avl_tree_t *languages; unsigned long *count; int had_error; }; static struct mount_scan_state scan_state; static char *get_line(FILE *f, unsigned *lineno) { 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_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)); 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)); 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)); rc = 1; 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; off_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, &tsource, &size, sha) != 3) goto next_record; source = lookup_tsource(tsource); printf("%s %s %lu %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; file_init(); 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, lineno = 0; 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, &lineno))) { char *str = buf; lineno++; 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; lptr = lptr->next) { // 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; } } 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; } /* * 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. */ static 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_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. */ file_init(); 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_config(); if (suspicious_total > 0) return CLI_EXIT_GENERIC; if (errors) return rc; return rc; } // 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, &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; } static int do_status_report(void) { const char *reason = "no pid file"; fd_fgets_state_t *st = fd_fgets_init(); if (!st) return CLI_EXIT_INTERNAL; // open pid file int pidfd = open(pidfile, O_RDONLY); if (pidfd >= 0) { char pid_buf[16]; // read contents if (fd_fgets_r(st, pid_buf, sizeof(pid_buf), pidfd)) { int rpt_fd; unsigned int pid, tries = 0; char exe_buf[64]; // convert to integer errno = 0; pid = strtoul(pid_buf, NULL, 10); if (errno) { reason = "bad pid in pid file"; goto err_out; } // verify it really is fapolicyd if (get_program_from_pid(pid, sizeof(exe_buf), exe_buf) == NULL) { reason = "can't read proc file"; goto err_out; } if (strcmp(basename(exe_buf), "fapolicyd")) { reason = "pid file doesn't point to fapolicyd"; goto err_out; } // delete the old report unlink(STAT_REPORT); // send the signal for the report kill(pid, SIGUSR1); // Access a file to provoke a response int fd = open(CONFIG_FILE, O_RDONLY); if (fd >= 0) close(fd); retry: // wait for it sleep(1); // display the report rpt_fd = open(STAT_REPORT, O_RDONLY); if (rpt_fd < 0) { if (tries < 25) { tries++; goto retry; } else { reason = "timed out waiting for report"; goto err_out; } } 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); } else reason = "can't read pid file"; close(pidfd); fd_fgets_destroy(st); return CLI_EXIT_SUCCESS; } err_out: fd_fgets_destroy(st); if (pidfd >= 0) close(pidfd); printf("Can't find fapolicyd: %s\n", reason); return CLI_EXIT_DAEMON_IPC; } #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; } 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:lur", long_opts, &option_index); switch (opt) { case 'd': if (arg_count > 2) goto args_err; rc = do_delete_db(); break; case 'D': if (arg_count > 2) goto args_err; rc = do_dump_db(); break; case 'f': 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': printf("%s", usage); rc = CLI_EXIT_SUCCESS; break; case 't': if (arg_count > 3) goto args_err; rc = do_ftype(optarg); break; case 'l': if (arg_count > 2) goto args_err; rc = do_list(); break; case 'u': if (arg_count > 2) goto args_err; rc = do_reload(DB); break; case 'r': if (arg_count > 2) goto args_err; rc = do_reload(RULES); break; // Now the pure long options case 1: { // --check-config 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 (arg_count > 2) goto args_err; return check_watch_fs(); break; case 3: // --check-trustdb if (arg_count > 2) goto args_err; return check_trustdb(); break; case 4: // --check-status if (arg_count > 2) goto args_err; return do_status_report(); break; case 5: // --check-path if (arg_count > 2) goto args_err; return check_path(); break; case 7: { // --check-ignore_mounts const char *override = optarg; if (override == NULL && optind < arg_count && argv[optind][0] != '-') override = args[optind++]; if (optind < arg_count) goto args_err; return check_ignore_mounts(override); } break; #ifdef HAVE_LIBRPM case 6: { // --test-filter 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; } fapolicyd-1.4.3/src/cli/file-cli.c000066400000000000000000000115271513023701500166620ustar00rootroot00000000000000/* * 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 "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); } } 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 { char *tmp = strdup(path); if (!tmp) return CLI_EXIT_INTERNAL; rc = list_append(&add_list, tmp, NULL); if (rc) free(tmp); } if (rc) { if (errno == ENOMEM) return CLI_EXIT_INTERNAL; return CLI_EXIT_IO; } return CLI_EXIT_SUCCESS; } 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) 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; } 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) { 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 (use_filter) { if (filter_init()) return CLI_EXIT_RULE_FILTER; if (filter_load_file(NULL)) { filter_destroy(); return CLI_EXIT_RULE_FILTER; } filter_ready = true; } if (fname) { char *file = fapolicyd_strcat(TRUST_DIR_PATH, fname); if (file) { count = trust_file_update_path(file, path, use_filter); free(file); } else count = -1; } else { count = trust_file_update_path_all(path, use_filter); } if (filter_ready) filter_destroy(); if (count < 0) rc = CLI_EXIT_PATH_CONFIG; else if (count == 0) { msg(LOG_ERR, "%s is not in the trust database", path); rc = CLI_EXIT_NOOP; } return rc; } fapolicyd-1.4.3/src/cli/file-cli.h000066400000000000000000000070111513023701500166600ustar00rootroot00000000000000/* * file-backend.h - Header file for CLI option file * Copyright (c) 2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * Radovan Sroka * Zoltan Fridrich */ #ifndef FILE_CLI_H #define FILE_CLI_H #include enum cli_exit_status { CLI_EXIT_SUCCESS = 0, CLI_EXIT_GENERIC = 1, CLI_EXIT_USAGE = 2, CLI_EXIT_PATH_CONFIG = 3, CLI_EXIT_DB_ERROR = 4, CLI_EXIT_RULE_FILTER = 5, CLI_EXIT_DAEMON_IPC = 6, CLI_EXIT_IO = 7, CLI_EXIT_INTERNAL = 8, CLI_EXIT_NOOP = 9, }; /** * Append a path into the file trust database * * @param path Path to append into the file trust database * @param fname Filename where \p path should be written. If NULL, then * \p path is written into fapolicyd.trust file. Otherwise, * write \p path into file \p fname within the trust.d directory * @param use_filter When true, apply the filter configuration to the list of * files gathered from \p path before writing anything * @return CLI_EXIT_SUCCESS on success, CLI_EXIT_NOOP when no new entries are * added, CLI_EXIT_RULE_FILTER for filter failures, CLI_EXIT_INTERNAL on * allocation failures, and CLI_EXIT_IO for filesystem errors. */ int file_append(const char *path, const char *fname, bool use_filter); /** * Delete a path from the file trust database. * It matches all occurrances so that a directory may be passed and * all parts of it get deleted * * @param path Path to delete from the file trust database * @param fname Filename from which \p path should be deleted. If NULL, then * \p path is deleted from fapolicyd.trust file. Otherwise, * deletes \p path from file \p fname within the trust.d directory * @return CLI_EXIT_SUCCESS on success, CLI_EXIT_NOOP when nothing is removed, * CLI_EXIT_IO for filesystem errors, and CLI_EXIT_PATH_CONFIG when trust * files cannot be parsed. */ int file_delete(const char *path, const char *fname); /** * Update a path in the file trust database. * It matches all occurrances so that a directory may be passed and * all parts of it get updated * * @param path Path to update in the file trust database * @param fname Filename in which \p path should be updated. If NULL, then * \p path is updated in fapolicyd.trust file. Otherwise, * updates \p path in file \p fname within the trust.d directory * @param use_filter When true, apply the filter configuration to the list of * files being updated so that filtered paths are skipped * @return CLI_EXIT_SUCCESS on success, CLI_EXIT_NOOP when nothing is updated, * CLI_EXIT_RULE_FILTER for filter parsing errors, CLI_EXIT_IO for * filesystem errors, and CLI_EXIT_PATH_CONFIG when trust files cannot be * parsed. */ int file_update(const char *path, const char *fname, bool use_filter); #endif fapolicyd-1.4.3/src/daemon/000077500000000000000000000000001513023701500155205ustar00rootroot00000000000000fapolicyd-1.4.3/src/daemon/fapolicyd.c000066400000000000000000000665321513023701500176520ustar00rootroot00000000000000/* * 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 #include /* syscall numbers */ #include /* umask */ #include #include #include #include /* PATH_MAX */ #include #include #ifndef HAVE_GETTID #include #endif #ifdef HAVE_MALLINFO2 #include #endif #include "notify.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 "conf.h" #include "queue.h" #include "gcc-attributes.h" #include "avl.h" #include "paths.h" #include "string-util.h" #include "filter.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; // 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; // 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 (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; } static void usr1_handler(int sig __attribute__((unused))) { run_stats = true; nudge_queue(); } /* * 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; if (load_daemon_config(&new_config)) { free_daemon_config(&new_config); msg(LOG_ERR, "Failed reloading daemon configuration"); return 1; } config.permissive = new_config.permissive; 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; 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 && (!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"); } /* * 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(); } /* * 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; } 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; } static void handle_mounts(int fd) { char buf[PATH_MAX * 2], device[1025], point[4097]; char type[32], mntops[128]; int fs_req, fs_passno; 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) return; mlist_mark_all_deleted(m); do { int rc = fd_fgets_r(st, buf, sizeof(buf), fd); // Get a line if (rc > 0) { // Parse it sscanf(buf, "%1024s %4096s %31s %127s %d %d\n", device, point, type, mntops, &fs_req, &fs_passno); unescape_shell(device, strlen(device)); 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); } /* * 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); } 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; 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) { const char *ptr = lookup_integrity(config.integrity); fprintf(f, "Permissive: %s\n", config.permissive ? "true" : "false"); fprintf(f, "Integrity: %s\n", ptr ? ptr : "unknown"); fprintf(f, "CPU cores: %ld\n", sysconf(_SC_NPROCESSORS_ONLN)); fprintf(f, "q_size: %u\n", config.q_size); q_report(f); decision_report(f); database_report(f); #ifdef HAVE_MALLINFO2 memory_use_report(f); #endif if (!shutdown) do_cache_reports(f); // Report mounts under fanotify watch if (m) { const char *path = mlist_first(m); while (path) { fprintf(f, "watching mount: %s\n", path); path = mlist_next(m); } } 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[2]; struct sigaction sa; struct rlimit limit; 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) { config.permissive = 1; } else if (strcmp(argv[i], "--boost") == 0) { i++; msg(LOG_ERR, "boost value on the command line is" " deprecated - ignoring"); } else if (strcmp(argv[i], "--queue") == 0) { i++; msg(LOG_ERR, "queue value on the command line is" " deprecated - ignoring"); } else if (strcmp(argv[i], "--user") == 0) { i++; msg(LOG_ERR, "user value on the command line is" " deprecated - ignoring"); } else if (strcmp(argv[i], "--group") == 0) { i++; msg(LOG_ERR, "group value on the command line is" " deprecated - ignoring"); } 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_handler = usr1_handler; sigaction(SIGUSR1, &sa, NULL); /* 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 = 16834; 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); } // 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 init_event_system(&config); // 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 file_init(); // Initialize the file watch system pfd[0].fd = open(mounts, O_RDONLY); pfd[0].events = POLLPRI; handle_mounts(pfd[0].fd); pfd[1].fd = init_fanotify(&config, m); pfd[1].events = POLLIN; 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, 2, -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[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..."); 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; } fapolicyd-1.4.3/src/daemon/mounts.c000066400000000000000000000051771513023701500172230ustar00rootroot00000000000000/* * 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; } fapolicyd-1.4.3/src/daemon/mounts.h000066400000000000000000000026721513023701500172250ustar00rootroot00000000000000/* * 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 fapolicyd-1.4.3/src/daemon/notify.c000066400000000000000000000314641513023701500172040ustar00rootroot00000000000000/* * 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 "conf.h" #include "policy.h" #include "event.h" #include "message.h" #include "queue.h" #include "mounts.h" #include "paths.h" #define FANOTIFY_BUFFER_SIZE 8192 // 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 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; // External functions void do_stat_report(FILE *f, int shutdown); // Local functions static void *decision_thread_main(void *arg); static void *deadmans_switch_thread_main(void *arg); /* * 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); } 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)); 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); 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); 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) { 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), path); exit(1); } msg(LOG_DEBUG, "added %s mount point", path); path = mlist_next(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) { 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), cur->path); } else { msg(LOG_DEBUG, "Added %s mount point", cur->path); } } // Now remove the deleted mount point if (cur->status == MNT_DELETE) { msg(LOG_DEBUG, "Deleted %s mount point", cur->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; } } m->cur = m->head; // Leave cur pointing to something valid } void unmark_fanotify(mlist *m) { const char *path = mlist_first(m); // Stop the flow of events while (path) { if (fanotify_mark(fd, FAN_MARK_FLUSH | mark_flag, 0, -1, path) == -1) msg(LOG_ERR, "Failed flushing path %s (%s)", path, strerror(errno)); 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_close(q); close(rpt_timer_fd); 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); } void decision_report(FILE *f) { if (f == NULL) return; // Report results fprintf(f, "Allowed accesses: %lu\n", getAllowed()); fprintf(f, "Denied accesses: %lu\n", getDenied()); } 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) { 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); } } } // write a stat report to file at the standard location static void rpt_write(void) { FILE *f = fopen(STAT_REPORT, "w"); if (f) { do_stat_report(f, 0); fclose(f); } } 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; struct fanotify_event_metadata metadata; // if an interval has been configured if (rpt_interval) { errno = 0; rc = q_timed_dequeue(q, &metadata, &rpt_timeout); if (rc == 0) { // check for timer expirations if (errno == ETIMEDOUT) { uint64_t expired = 0; 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) { rpt_write(); run_stats = 0; rpt_is_stale = 0; } // adjust the pthread timeout to // a full interval from now if (clock_gettime(CLOCK_MONOTONIC, &rpt_timeout)) { // gettime errors are // unrecoverable rpt_disable( "clock failure"); continue; } rpt_timeout.tv_sec += rpt_interval; } } continue; } } else { rc = q_dequeue(q, &metadata); if (rc == 0) { if (run_stats) { rpt_write(); run_stats = 0; } continue; } if (run_stats) { rpt_write(); run_stats = 0; } } atomic_store_explicit(&alive, true, memory_order_relaxed); rpt_is_stale = 1; make_policy_decision(&metadata, fd, mask); } msg(LOG_DEBUG, "Exiting decision thread"); return NULL; } void handle_events(void) { const struct fanotify_event_metadata *metadata; struct fanotify_event_metadata buf[FANOTIFY_BUFFER_SIZE]; ssize_t len = -2; 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 (metadata->fd >= 0) { if (metadata->mask & mask) { if (metadata->pid == our_pid) reply_event(fd, metadata, FAN_ALLOW, NULL); else if (q_enqueue(q, metadata)) { msg(LOG_ERR, "Failed to enqueue event for PID %d: " "queue is full, please consider tuning q_size " "if issue happens often", metadata->pid); int decision = FAN_DENY; if (config.permissive) 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); } } fapolicyd-1.4.3/src/daemon/notify.h000066400000000000000000000023041513023701500172000ustar00rootroot00000000000000/* * 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 #include "conf.h" #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 decision_report(FILE *f); void handle_events(void); void nudge_queue(void); #endif fapolicyd-1.4.3/src/handler/000077500000000000000000000000001513023701500156725ustar00rootroot00000000000000fapolicyd-1.4.3/src/handler/fapolicyd-rpm-loader.c000066400000000000000000000064571513023701500220640ustar00rootroot00000000000000/* * 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); fcntl(memfd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE); 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; } fapolicyd-1.4.3/src/library/000077500000000000000000000000001513023701500157215ustar00rootroot00000000000000fapolicyd-1.4.3/src/library/attr-sets.c000066400000000000000000000212571513023701500200220ustar00rootroot00000000000000/* * 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 */ #include #include #include #include "attr-sets.h" #include "message.h" #define RESIZE_BY 2 #define DEFAULT_CAPACITY 100 attr_sets_t sets; /* * this is a compare callback for avl string tree * * avl tree compare expect: * 0 when equals * <0 when a < b * >0 when a > b */ static int strcmp_cb(void * a, void * b) { return strcmp(((avl_str_data_t *)a)->str, ((avl_str_data_t *)b)->str); } /* * this is a compare callback for avl signed int tree * * avl tree compare expect: * 0 when equals * <0 when a < b * >0 when a > 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; } /* * compare callback for avl unsigned int tree */ 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; } /* * this is a traverse callback for avl string tree * it provides directory test * e.g a->str = "/usr/bin/" a->len = 9 * b->str = "/usr/bin/ls" * strncmp return 0 with output above that means match * * avl tree traverse calls callback on each node and * it sums all return values and it returns the sum * so when I return 1 it just sums how many strncmp * returned match * with -1 I can break the recursion with first 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; } int init_attr_sets(void) { sets.resize_factor = RESIZE_BY; // first is reserved // we are using 0th index as failure return value sets.size = 1; sets.capacity = DEFAULT_CAPACITY; sets.array = malloc(sizeof(attr_sets_entry_t) * sets.capacity); if (!sets.array) return 1; memset(sets.array, 0, sets.capacity * sizeof(attr_sets_entry_t)); return 0; } static int resize_attr_sets(void) { 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_entry_t * get_attr_set(const size_t index) { if (index == 0) return NULL; if (index > sets.size) return NULL; return &(sets.array[index]); } size_t search_attr_set_by_name(const char * name) { // ignore 0th index for (size_t i = 1 ; i < sets.size ; i++) { const char * nname = sets.array[i].name; if (nname) { if (strcmp(nname, name) == 0) return i; } } return 0; } int add_attr_set(const char * name, const int type, size_t * index) { if (sets.size == sets.capacity) if (resize_attr_sets()) return 1; // getting last free known entry attr_sets_entry_t * entry = get_attr_set(sets.size); if (!entry) return 1; // copy string or set NULL for sure entry->name = name ? strdup(name) : NULL; entry->type = type; if (type == STRING) avl_init(&entry->tree, strcmp_cb); else if (type == SIGNED) avl_init(&entry->tree, intcmp_cb); else if (type == UNSIGNED) avl_init(&entry->tree, unsigned_cmp_cb); else { free(entry->name); memset(entry, 0, sizeof(*entry)); return 1; } *index = sets.size; sets.size++; return 0; } attr_sets_entry_t *init_standalone_set(const int type) { attr_sets_entry_t *s = malloc(sizeof(attr_sets_entry_t)); if (s) { s->name = NULL; s->type = type; if (type == STRING) avl_init(&s->tree, strcmp_cb); else if (type == SIGNED) avl_init(&s->tree, intcmp_cb); else if (type == UNSIGNED) avl_init(&s->tree, unsigned_cmp_cb); else { free(s); s = NULL; } } return s; } int append_int_attr_set(attr_sets_entry_t *set, const int64_t num) { if (!set) return 1; if (set->type != SIGNED && set->type != UNSIGNED) { // trying to insert wrong type? return 1; } if (set->type == UNSIGNED && num < 0) return 1; avl_int_data_t * data = malloc(sizeof(avl_int_data_t)); if (!data) return 1; data->num = num; avl_t * ret = avl_insert(&set->tree, (avl_t *)data); if (ret != (avl_t *)data) { // Already present in avl tree free(data); return 1; } return 0; } int append_str_attr_set(attr_sets_entry_t * set, const char * str) { if (!set) return 1; if (set->type != STRING) { // trying to insert wrong type? return 1; } avl_str_data_t * 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); avl_t * ret = avl_insert(&set->tree, (avl_t *)data); if (ret != (avl_t *)data) { // Already present in avl tree free((void *)data->str); free(data); return 1; } return 0; } /* * is_attr_set_empty - Determine if a set has no members * @set: attribute set to check * * Return: true when the set is NULL or contains no entries, false otherwise. */ bool is_attr_set_empty(attr_sets_entry_t *set) { if (!set) return true; return set->tree.root == NULL; } int check_int_attr_set(attr_sets_entry_t *set, const int64_t num) { avl_int_data_t data; if (set->type == UNSIGNED && num < 0) return 0; data.num = num; // we are doing following checks on upper level //if (!set) return 1; //if (set->type != SIGNED) // return -1; // --------------------------------------------- // valid pointer to data if found // NULL -> 0 if not return avl_search(&set->tree, (avl_t*)(&data)) ? 1 : 0; } int check_str_attr_set(attr_sets_entry_t * set, const char * str) { avl_str_data_t data; data.str = str; // we are doing following checks on upper level //if (!set) return 1; //if (set->type != STRING) // return -1; // -------------------------------------------- // valid pointer to data if found // NULL -> 0 if not return avl_search(&set->tree, (avl_t*)(&data)) ? 1 : 0; } int check_pstr_attr_set(attr_sets_entry_t * set, const char * str) { avl_str_data_t data; data.str = str; // we are doing following checks on upper level // if (!set) return 1; // if (set->type != STRING) // return -1; // -------------------------------------------- // -1 means broken recursion -> found // 0 means not found int ret = avl_traverse(&set->tree, strncmp_cb, (void*)&data); // want to be consistent if (ret == -1) return 1; return 0; } 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; } 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; } 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; } void print_attr_set(attr_sets_entry_t * set) { if (!set) return; msg(LOG_DEBUG, "Set: %s", set->name); 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, "--------------"); } void print_attr_sets(void) { for (size_t i = 1 ; i < sets.size ; i++) { print_attr_set(&sets.array[i]); } } void destroy_attr_set(attr_sets_entry_t * set) { if (!set) return; free(set->name); // free tree avl_t *cur; 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); } } void destroy_attr_sets(void) { for (size_t i = 0 ; i < sets.size ; i++) { destroy_attr_set(&sets.array[i]); } free(sets.array); } fapolicyd-1.4.3/src/library/attr-sets.h000066400000000000000000000045361513023701500200300ustar00rootroot00000000000000/* * 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; // variable size array typedef struct attr_sets { // allocated size size_t capacity; size_t size; size_t resize_factor; attr_sets_entry_t * array; } attr_sets_t; typedef enum _types { STRING = 1, SIGNED, UNSIGNED, } DATA_TYPES; int init_attr_sets(void); attr_sets_entry_t * get_attr_set(const size_t index); int add_attr_set(const char * name, const int type, size_t * index); void destroy_attr_set(attr_sets_entry_t *set); void destroy_attr_sets(void); size_t search_attr_set_by_name(const char * name); attr_sets_entry_t *init_standalone_set(const int type); int append_int_attr_set(attr_sets_entry_t *set, const int64_t num); int append_str_attr_set(attr_sets_entry_t * set, const char * str); int check_int_attr_set(attr_sets_entry_t *set, const int64_t num); int check_str_attr_set(attr_sets_entry_t * set, const char * str); int check_pstr_attr_set(attr_sets_entry_t * set, const char * str); bool is_attr_set_empty(attr_sets_entry_t *set); void print_attr_sets(void); void print_attr_set(attr_sets_entry_t * set); #endif // ATTR_SETS_H fapolicyd-1.4.3/src/library/avl.c000066400000000000000000000304531513023701500166540ustar00rootroot00000000000000//#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; } fapolicyd-1.4.3/src/library/avl.h000066400000000000000000000037131513023701500166600ustar00rootroot00000000000000#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 */ fapolicyd-1.4.3/src/library/backend-manager.c000066400000000000000000000066761513023701500211030ustar00rootroot00000000000000/* * 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; } fapolicyd-1.4.3/src/library/backend-manager.h000066400000000000000000000023641513023701500210760ustar00rootroot00000000000000/* * 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 fapolicyd-1.4.3/src/library/conf.h000066400000000000000000000030071513023701500170170ustar00rootroot00000000000000/* 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 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; } conf_t; #endif fapolicyd-1.4.3/src/library/daemon-config.c000066400000000000000000000407231513023701500206010ustar00rootroot00000000000000/* * 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 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 }, { 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; } int load_daemon_config(conf_t *config) { int fd, lineno = 1; FILE *f; char buf[160]; 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 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[65]; 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; } static int syslog_format_parser(const struct nv_pair *nv, int line, conf_t *config) { free((void *)config->syslog_format); config->syslog_format = strdup(nv->value); if (config->syslog_format) return 0; msg(LOG_ERR, "Could not store value line %d", line); return 1; } static int rpm_sha256_only_parser(const struct nv_pair *nv, int line, conf_t *config) { int rc = 0; #ifndef USE_RPM msg(LOG_WARNING, "rpm_sha256_only: fapolicyd was not built with rpm support, ignoring" ); #else rc = unsigned_int_parser(&(config->rpm_sha256_only), nv->value, line); if (rc == 0 && config->rpm_sha256_only > 1) { msg(LOG_WARNING, "rpm_sha256_only value reset to 0 - line %d", line); config->rpm_sha256_only = 0; } #endif return rc; } static int fs_mark_parser(const struct nv_pair *nv, int line, conf_t *config) { int rc = 0; #if defined HAVE_DECL_FAN_MARK_FILESYSTEM && HAVE_DECL_FAN_MARK_FILESYSTEM != 0 rc = unsigned_int_parser(&(config->allow_filesystem_mark), nv->value, line); if (rc == 0 && config->allow_filesystem_mark > 1) { msg(LOG_WARNING, "allow_filesystem_mark value reset to 0 - line %d", line); config->allow_filesystem_mark = 0; } #else msg(LOG_WARNING, "allow_filesystem_mark is unsupported on this kernel - ignoring"); #endif return rc; } fapolicyd-1.4.3/src/library/daemon-config.h000066400000000000000000000021121513023701500205740ustar00rootroot00000000000000/* 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); #endif fapolicyd-1.4.3/src/library/database.c000066400000000000000000001520601513023701500176350ustar00rootroot00000000000000/* * 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 "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 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; /* * 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, const char *data) __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]; /* Ensure that the RUN_DIR exists */ 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; } 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 so stats reflect current use */ if (mdb_env_create(&tmp_env) || mdb_env_set_maxdbs(tmp_env, 2) || mdb_env_open(tmp_env, DB_DIR, MDB_RDONLY, 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; 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 mdb_close(env, dbi); mdb_env_close(env); } 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); } } void database_report(FILE *f) { fprintf(f, "Trust database max pages: %lu\n", max_pages); fprintf(f, "Trust database pages in use: %lu (%lu%%)\n", pages, max_pages ? ((100*pages)/max_pages) : 0); } /* * 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. * @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, const char *data) { MDB_val key, value; MDB_txn *txn; int rc, ret_val = 0; size_t len; char *hash = NULL; if (mdb_txn_begin(env, NULL, 0, &txn)) return 1; if (open_dbi(txn)) { abort_transaction(txn); return 2; } // Only scan enough to make a decision len = strnlen(idx, MDB_maxkeysize+1); if (len > MDB_maxkeysize) { hash = path_to_hash(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 = 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 (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 MDB_txn *lt_txn = NULL; static MDB_cursor *lt_cursor = NULL; 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; start_long_term_read_ops(); mdb_stat(lt_txn, dbi, &st); end_long_term_read_ops(); 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; start_long_term_read_ops(); mdb_stat(lt_txn, dbi, &status); end_long_term_read_ops(); 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 // Only scan enough to make a decision len = strnlen(index, MDB_maxkeysize+1); 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, data res = write_db(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); 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; // this function is going to be used from decision_thread that means // we need to be sure database won't change under our hands. lock_update_thread(); if (start_long_term_read_ops()) { unlock_update_thread(); 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(); unlock_update_thread(); 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); pthread_mutex_destroy(&update_lock); pthread_mutex_destroy(&rule_lock); backend_close(); unlink_fifo(); } 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) { pthread_mutex_lock(&rule_lock); //msg(LOG_DEBUG, "lock_rule()"); } /* * Unlock wrapper for rule mutex */ void unlock_rule(void) { 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]; off_t size; if (stop) return 1; // validating input int res = sscanf(buffer, "%2048s %lu %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, data); unlock_update_thread(); return 0; } void set_reload_trust_database(void) { atomic_store_explicit(&reload_db, true, memory_order_release); } 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); if (config->db_max_size < old_db_max_size) { mdb_env_close(env); env = NULL; if ((rc = init_db(config))) { msg(LOG_ERR, "Cannot open the trust database, init_db() (%d)", rc); if (stop) goto out; close(ffd[0].fd); backend_close(); unlink_fifo(); exit(rc); } } else if (config->db_max_size > old_db_max_size) { rc = mdb_env_set_mapsize(env, (size_t)config->db_max_size * MEGABYTE); if (rc) { msg(LOG_ERR, "env_set_mapsize error: %s", mdb_strerror(rc)); goto out; } } } if ((rc = backend_init(config))) { msg(LOG_ERR, "Failed to load trust data from backend (%d)", rc); close_db(0); goto out; } if ((rc = backend_load(config))) { msg(LOG_ERR, "Failed to load data from backend (%d)", rc); close_db(0); goto out; } if ((rc = update_database(config))) { msg(LOG_ERR, "Cannot update trust database!"); if (stop) goto out; close(ffd[0].fd); backend_close(); unlink_fifo(); exit(rc); } msg(LOG_INFO, "Updated"); out: // Conserve memory backend_close(); } static void *update_thread_main(void *arg) { int rc; 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; } fcntl(ffd[0].fd, F_SETFL, O_NONBLOCK); 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; if (load_rule_file()) { msg(LOG_ERR, "Rule reload aborted: unable to open rules file (%s)", strerror(errno)); } else { lock_rule(); do_reload_rules(config); unlock_rule(); } } // 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(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; if (load_rule_file()) { msg(LOG_ERR, "Rule reload aborted: unable to open rules file (%s)", strerror(errno)); } else { lock_rule(); do_reload_rules( config); unlock_rule(); } // 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); } fapolicyd-1.4.3/src/library/database.h000066400000000000000000000040021513023701500176320ustar00rootroot00000000000000/* * 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 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_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); #define RELOAD_TRUSTDB_COMMAND '1' #define FLUSH_CACHE_COMMAND '2' #define RELOAD_RULES_COMMAND '3' #endif fapolicyd-1.4.3/src/library/deb-backend.c000066400000000000000000000145741513023701500202170ustar00rootroot00000000000000#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; } fapolicyd-1.4.3/src/library/escape.c000066400000000000000000000077771513023701500173470ustar00rootroot00000000000000/* * 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) { 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; } fapolicyd-1.4.3/src/library/escape.h000066400000000000000000000023451513023701500173360ustar00rootroot00000000000000/* * 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 fapolicyd-1.4.3/src/library/event.c000066400000000000000000000535241513023701500172170ustar00rootroot00000000000000/* * 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 "event.h" #include "database.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) static Queue *subj_cache = NULL; static Queue *obj_cache = NULL; static bool obj_cache_warned = false; static unsigned int early_subj_cache_evictions = 0; atomic_bool needs_flush = false; /* * 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); } 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); } // 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, rc, evict = 1, skip_path = 0; s_array *s; o_array *o; struct proc_info *pinfo; struct file_info *finfo; if (atomic_exchange_explicit(&needs_flush, false, memory_order_acq_rel)) { flush_cache(); } // 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); q_node = check_lru_cache(subj_cache, key); s = (s_array *)q_node->item; // get proc fingerprint pinfo = stat_proc_entry(m->pid); 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 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)); subject_create(e->s); subj.type = PID; subj.pid = e->pid; subject_add(e->s, &subj); // 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; finfo = stat_file_entry(m->fd); 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_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)); object_create(e->o); // 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); } } } 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 = rules_get_proc_status_mask(); mask |= policy_get_syslog_proc_status_mask(); struct proc_status_info info = { .ppid = -1, .uid = NULL, .groups = NULL, .comm = NULL }; if (read_proc_status(e->pid, mask, &info) != 0) return NULL; // 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; 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: subj.uval = get_program_auid_from_pid(e->pid); break; case PPID: case UID: case GID: case COMM: /* * 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: subj.uval = (unsigned int) get_program_sessionid_from_pid(e->pid); break; case PID: 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; errno = 0; ptr = get_program_from_pid(e->pid, sizeof(buf), buf); if (errno == ENOENT) { /* kworkers have no exe entry, 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; ptr = get_type_from_pid(e->pid, sizeof(buf), buf); if (ptr) subj.str = strdup(buf); else subj.str = strdup("?"); } break; case SUBJ_TRUST: { subject_attr_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; 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: // Try to avoid looking up the path if we have it on = object_find_file(o); if (on) obj.o = strdup(on->o); else { ptr = get_file_from_fd(e->fd, e->pid, sizeof(buf), buf); if (ptr) obj.o = strdup(buf); else obj.o = strdup("?"); } break; case DEVICE: 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 = get_obj_attr(e, PATH); ptr = get_file_type_from_fd(e->fd, o->info, path ? path->o : "?", sizeof(buf), buf); if (ptr) obj.o = strdup(buf); else obj.o = strdup("?"); } break; case FILE_HASH: { file_hash_alg_t alg = FILE_HASH_ALG_SHA256; 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 = 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; case FMODE: 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 Queue *q) { fprintf(f, "%s cache size: %u\n", q->name, q->total); fprintf(f, "%s slots in use: %u (%u%%)\n", q->name, q->count, q->total ? (100*q->count)/q->total : 0); fprintf(f, "%s hits: %lu\n", q->name, q->hits); fprintf(f, "%s misses: %lu\n", q->name, q->misses); fprintf(f, "%s evictions: %lu (%lu%%)\n", q->name, q->evictions, q->hits ? (100*q->evictions)/q->hits : 0); } void run_usage_report(const conf_t *config, FILE *f) { time_t t; QNode *q_node; 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"); } print_queue_stats(f, obj_cache); 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"); } print_queue_stats(f, subj_cache); fprintf(f, "\n"); } void do_cache_reports(FILE *f) { print_queue_stats(f, subj_cache); fprintf(f, "Early subject cache evictions: %u\n", early_subj_cache_evictions); print_queue_stats(f, obj_cache); } fapolicyd-1.4.3/src/library/event.h000066400000000000000000000031251513023701500172140ustar00rootroot00000000000000/* * 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); __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_reports(FILE *f); #endif fapolicyd-1.4.3/src/library/fapolicyd-backend.h000066400000000000000000000033271513023701500214360ustar00rootroot00000000000000/* * 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 fapolicyd-1.4.3/src/library/fapolicyd-defs.h000066400000000000000000000020541513023701500207640ustar00rootroot00000000000000/* * fapolicyd-defs.h - Header file for defines & enums that cause loops * Copyright (c) 2019 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef FAPOLICYD_DEFS_HEADER #define FAPOLICYD_DEFS_HEADER typedef enum { OPEN_ACC, EXEC_ACC , ANY_ACC } access_t; typedef enum { RULE_FMT_ORIG, RULE_FMT_COLON } rformat_t; #endif fapolicyd-1.4.3/src/library/fd-fgets.c000066400000000000000000000174711513023701500175760ustar00rootroot00000000000000/* 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->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) { if (st == NULL || buf == NULL || buff_size == 0) 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; st->eptr = st->buffer + buff_size; st->eof = 0; st->mem_type = how; st->buff_size = buff_size; return 0; } int fd_setvbuf(void *buf, size_t buff_size, enum fd_mem how) { fd_fgets_ensure_global(); return fd_setvbuf_r(&global_state, buf, buff_size, how); } fapolicyd-1.4.3/src/library/fd-fgets.h000066400000000000000000000036751513023701500176040ustar00rootroot00000000000000/* 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, MEM_MALLOC, MEM_MMAP, MEM_MMAP_FILE }; 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 fapolicyd-1.4.3/src/library/file-backend.c000066400000000000000000000041531513023701500203740ustar00rootroot00000000000000/* * file-backend.c - file backend * Copyright (c) 2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka * Steve Grubb * Zoltan Fridrich */ #include "config.h" #include #include #include #include #include #include #include "fapolicyd-backend.h" #include "message.h" #include "trust-file.h" static int file_init_backend(void); static int file_load_list(const conf_t *conf); static int file_destroy_backend(void); backend file_backend = { "file", file_init_backend, file_load_list, file_destroy_backend, -1, -1, }; static int file_load_list(const conf_t *conf) { msg(LOG_DEBUG, "Loading file backend"); int memfd = memfd_create("file_snapshot", MFD_CLOEXEC | MFD_ALLOW_SEALING); file_backend.memfd = memfd; file_backend.entries = -1; if (memfd < 0) { msg(LOG_ERR, "memfd_create failed for file backend (%s)", strerror(errno)); return 1; } trust_file_load_all(NULL, memfd); /* Seal the snapshot so readers see a stable view. */ if (fcntl(memfd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE) == -1) // Not a fatal error msg(LOG_WARNING, "Failed to seal file backend memfd (%s)", strerror(errno)); return 0; } static int file_init_backend(void) { return 0; } static int file_destroy_backend(void) { return 0; } fapolicyd-1.4.3/src/library/file.c000066400000000000000000001221331513023701500170060ustar00rootroot00000000000000/* * 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 "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); } // Initialize what we can now so that its not done each call void 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"); exit(1); } // Load only essential magic rules if (magic_load(magic_fast, MAGIC_PATH) != 0) { msg(LOG_ERR, "Unable to load fast magic database"); exit(1); } // 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"); exit(1); } // System default if (magic_load(magic_full, NULL) != 0) { msg(LOG_ERR, "Unable to load default magic database"); exit(1); } } // 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") /* * 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 * #!/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) { /* Skip to next token */ p = end; while (*p == ' ' || *p == '\t') p++; /* Skip env flags like -S, -i, --split-string */ while (*p == '-') { while (*p && *p != ' ' && *p != '\t') p++; while (*p == ' ' || *p == '\t') p++; } /* Now p points to the interpreter */ end = p; while (*end && *end != ' ' && *end != '\t' && *end != '\n' && *end != '\r') 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')) // 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; return buf; } uint32_t elf = gather_elf(fd, i->size); if (elf & IS_ELF) { ptr = classify_elf_info(elf, path); if (ptr == NULL) return (char *)ptr; strncpy(buf, ptr, blen-1); buf[blen-1] = 0; 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; 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; 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; 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; return buf; } // Do the fast classification 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) return NULL; } 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; if (size == 0) { switch (alg) { case FILE_HASH_ALG_SHA1: return strdup(degenerate_hash_sha1); case FILE_HASH_ALG_SHA256: return strdup(degenerate_hash_sha256); case FILE_HASH_ALG_SHA512: return strdup(degenerate_hash_sha512); case FILE_HASH_ALG_MD5: return strdup(degenerate_hash_md5); default: return NULL; } } digest_length = file_hash_length(alg); if (digest_length == 0) return NULL; mapped = mmap(0, size, PROT_READ, MAP_PRIVATE|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); } } 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; if (alg) *alg = FILE_HASH_ALG_NONE; /* * 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"); return 0; } if (tmp[0] != IMA_XATTR_DIGEST_NG) { msg(LOG_DEBUG, "Wrong ima xattr type"); return 0; } desc = ima_lookup_algo(tmp[1]); if (desc == NULL) { msg(LOG_DEBUG, "Unsupported ima hash algorithm %u", tmp[1]); return 0; } if (len < (ssize_t)(2 + desc->digest_len)) { msg(LOG_DEBUG, "ima xattr too small for alg %u", tmp[1]); return 0; } bytes2hex(sha, &tmp[2], desc->digest_len); if (alg) *alg = desc->alg; return 1; } static unsigned char e_ident[EI_NIDENT]; static int read_preliminary_header(int fd) { ssize_t rc = safe_read(fd, (char *)e_ident, EI_NIDENT); if (rc == EI_NIDENT) return 0; return 1; } 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; fd = open(interp, O_RDONLY|O_CLOEXEC); 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; if (read_preliminary_header(fd)) goto rewind_out; /* Detect scripts via shebang before ELF check */ if (e_ident[0] == '#' && e_ident[1] == '!') { info |= HAS_SHEBANG; 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; 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_out32; } num = ph_tbl[i].p_filesz / sizeof(Elf64_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; } fapolicyd-1.4.3/src/library/file.h000066400000000000000000000074131513023701500170160ustar00rootroot00000000000000/* * 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]; }; void 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 fapolicyd-1.4.3/src/library/filter.c000066400000000000000000000507671513023701500173710ustar00rootroot00000000000000/* * 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 "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 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); } /* * 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) { if (_stack == NULL || buf == NULL || sp == NULL) return -1; if (*sp >= MAX_FILTER_DEPTH) return -1; /* TODO: trie rewrite to remove depth limit */ stack_item_t *item = &buf[(*sp)++]; item->level = _level; item->offset = _offset; item->filter = _filter; stack_push(_stack, item); return 0; } /* * 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 MAX_FILTER_DEPTH is exceeded (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 = strlen(_path); char *path = alloca(path_len + 1); strcpy(path, _path); /* 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); stack_item_t stack_buf[MAX_FILTER_DEPTH]; int sp = 0; filter_rc_t res = FILTER_DENY; int level = 0; if (stack_push_vars(&stack, stack_buf, &sp, level, offset, filter)) { msg(LOG_WARNING, "fapolicyd: rule nesting exceeds MAX_FILTER_DEPTH (%d)\n", MAX_FILTER_DEPTH); stack_destroy(&stack); return FILTER_ERR_DEPTH; /* TODO: trie rewrite removes limit */ } 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(&stack, stack_buf, &sp, level+1, offset, next_filter)) { msg(LOG_WARNING, "fapolicyd: rule nesting exceeds MAX_FILTER_DEPTH (%d)\n", MAX_FILTER_DEPTH); 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(&stack, stack_buf, &sp, level, offset, next_filter)) { msg(LOG_WARNING, "fapolicyd: rule nesting exceeds MAX_FILTER_DEPTH (%d)\n", MAX_FILTER_DEPTH); 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); 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. * */ fapolicyd-1.4.3/src/library/filter.h000066400000000000000000000034261513023701500173640ustar00rootroot00000000000000/* * 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_ fapolicyd-1.4.3/src/library/gcc-attributes.h000066400000000000000000000014201513023701500210070ustar00rootroot00000000000000#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 fapolicyd-1.4.3/src/library/llist.c000066400000000000000000000063351513023701500172230ustar00rootroot00000000000000/* * llist.c - Linked list as a temporary memory storage * for trust database data * Copyright (c) 2016,2018 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka * Zoltan Fridrich */ #include #include #include #include "message.h" #include "llist.h" #pragma GCC optimize("O3") void list_init(list_t *list) { list->count = 0; list->first = NULL; list->last = NULL; } list_item_t *list_get_first(const list_t *list) { return list->first; } static list_item_t * create_item(const char *index, const char *data) { list_item_t *item = malloc(sizeof(list_item_t)); if (!item) { msg(LOG_ERR, "Malloc failed"); return item; } item->index = index; item->data = data; item->next = NULL; return item; } int list_prepend(list_t *list, const char *index, const char *data) { list_item_t *item = create_item(index, data); if (item == NULL) return 1; item->next = list->first; list->first = item; ++list->count; return 0; } int list_append(list_t *list, const char *index, const char *data) { list_item_t *item = create_item(index, data); if (!item) return 1; if (list->first) { list->last->next = item; list->last = item; } else { list->first = item; list->last = item; } ++list->count; return 0; } void list_destroy_item(list_item_t **item) { free((void *)(*item)->index); free((void *)(*item)->data); free((*item)); *item = NULL; } void list_empty(list_t *list) { if (!list->first) return; list_item_t *actual = list->first; list_item_t *next = NULL; for (; actual; actual = next) { next = actual->next; list_destroy_item(&actual); } list_init(list); } // Return 1 if the list contains the string, 0 otherwise int list_contains(const list_t *list, const char *str) { for (list_item_t *lptr = list->first; lptr; lptr = lptr->next) { if (!strcmp(str, lptr->index)) return 1; } return 0; } // Return 1 if an item was removed, 0 otherwise int list_remove(list_t *list, const char *str) { list_item_t *lptr, *prev = NULL; for (lptr = list->first; lptr; lptr = lptr->next) { if (!strcmp(str, lptr->index)) { if (prev) prev->next = lptr->next; else list->first = lptr->next; if (!lptr->next) list->last = prev; --list->count; list_destroy_item(&lptr); return 1; } prev = lptr; } return 0; } void list_merge(list_t *dest, list_t *src) { if (!dest->last) { *dest = *src; } else { dest->last->next = src->first; dest->count += src->count; } list_init(src); } fapolicyd-1.4.3/src/library/llist.h000066400000000000000000000031301513023701500172160ustar00rootroot00000000000000/* * temporary_db.h - Header file for linked list * Copyright (c) 2018 Red Hat Inc., Durham, North Carolina. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka * Zoltan Fridrich */ #ifndef LLIST_H #define LLIST_H typedef struct item { const void *index; const void *data; struct item *next; } list_item_t; typedef struct list_header { long count; struct item *first; struct item *last; } list_t; void list_init(list_t *list); list_item_t *list_get_first(const list_t *list); int list_prepend(list_t *list, const char *index, const char *data); int list_append(list_t *list, const char *index, const char *data); void list_destroy_item(list_item_t **item); void list_empty(list_t *list); int list_contains(const list_t *list, const char *str); int list_remove(list_t *list, const char *str); void list_merge(list_t *dest, list_t *src); #endif fapolicyd-1.4.3/src/library/lru.c000066400000000000000000000240271513023701500166740ustar00rootroot00000000000000/* * 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 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->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; 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++; } // 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); } unsigned int compute_subject_key(const Queue *queue, unsigned int pid) { if (queue) return pid % queue->hash->size; else return 0; } unsigned long compute_object_key(const Queue *queue, unsigned long num) { if (queue) return num % queue->hash->size; else return 0; } fapolicyd-1.4.3/src/library/lru.h000066400000000000000000000045601513023701500167010ustar00rootroot00000000000000/* * 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 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; 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_evict(Queue *queue, unsigned int key); QNode *check_lru_cache(Queue *q, unsigned int key); unsigned int compute_subject_key(const Queue *queue, unsigned int pid); unsigned long compute_object_key(const Queue *queue, unsigned long num); #endif fapolicyd-1.4.3/src/library/md5-backend.c000066400000000000000000000102501513023701500201350ustar00rootroot00000000000000/* * 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; } fapolicyd-1.4.3/src/library/md5-backend.h000066400000000000000000000024021513023701500201420ustar00rootroot00000000000000/* * 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 fapolicyd-1.4.3/src/library/message.c000066400000000000000000000055621513023701500175210ustar00rootroot00000000000000/* * 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); strftime(buffer, sizeof(buffer), "%x %T [ ", &timeinfo); 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); } fapolicyd-1.4.3/src/library/message.h000066400000000000000000000023671513023701500175260ustar00rootroot00000000000000/* * 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 typedef enum { MSG_STDERR, MSG_SYSLOG, MSG_QUIET } message_t; typedef enum { DBG_NO, DBG_YES } debug_message_t; 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 fapolicyd-1.4.3/src/library/nv.h000066400000000000000000000022021513023701500165110ustar00rootroot00000000000000/* * nv.h - Header file for name value struct * Copyright (c) 2016 Red Hat Inc., Durham, North Carolina. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef NV_HEADER #define NV_HEADER #define SUBJ_START 0 #define OBJ_START 16 typedef struct nv { unsigned int value; const char *name; }nv_t; typedef struct nv_list { const char *name; int item; }nvlist_t; #endif fapolicyd-1.4.3/src/library/object-attr.c000066400000000000000000000040531513023701500203050ustar00rootroot00000000000000/* * 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" }, { FMODE, "mode" }, }; #define MAX_OBJECTS (sizeof(table)/sizeof(table[0])) int obj_name_to_val(const char *name) { static bool warned; /* Announce deprecation once per start while keeping the alias usable. */ if (!warned) { msg(LOG_NOTICE, "SHA256HASH object name is deprecated; use FILE_HASH instead"); warned = true; } // Accept the legacy name for compatibility with older rule sets. if (strcmp(name, "sha256hash") == 0) return FILE_HASH; unsigned int i = 0; while (i < MAX_OBJECTS) { if (strcmp(name, table[i].name) == 0) return table[i].value; i++; } return -1; } const char *obj_val_to_name(unsigned int v) { if (v < OBJ_START || v > OBJ_END) return NULL; unsigned int index = v - OBJ_START; if (index < MAX_OBJECTS) return table[index].name; return NULL; } fapolicyd-1.4.3/src/library/object-attr.h000066400000000000000000000031201513023701500203040ustar00rootroot00000000000000/* * 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, FMODE } object_type_t; #define OBJ_END FMODE #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 { size_t gr_index; attr_sets_entry_t * set; }; } object_attr_t; int obj_name_to_val(const char *name); const char *obj_val_to_name(unsigned int v); #endif fapolicyd-1.4.3/src/library/object.c000066400000000000000000000053031513023701500173340ustar00rootroot00000000000000/* * 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 void object_create(o_array *a) { int i; a->obj = malloc(sizeof(object_attr_t *) * OBJ_COUNT); for (i = 0; i < OBJ_COUNT; i++) a->obj[i] = NULL; a->cnt = 0; a->info = NULL; } #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) { 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; 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) { 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; for (i = 0; i < OBJ_COUNT; i++) { current = a->obj[i]; if (current == NULL) continue; free(current->o); free(current); } free(a->info); free(a->obj); a->cnt = 0; } fapolicyd-1.4.3/src/library/object.h000066400000000000000000000030031513023701500173340ustar00rootroot00000000000000/* * 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; void object_create(o_array *a); object_attr_t *object_access(const o_array *a, object_type_t t); int object_add(o_array *a, const object_attr_t *obj); object_attr_t *object_find_file(const o_array *a); void object_clear(o_array *a); static inline int type_is_obj(int type) {if (type >= OBJ_START) return 1; else return 0;} #endif fapolicyd-1.4.3/src/library/paths.h000066400000000000000000000034631513023701500172170ustar00rootroot00000000000000/* 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 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 fapolicyd-1.4.3/src/library/policy.c000066400000000000000000000377451513023701500174040ustar00rootroot00000000000000/* * 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 "attr-sets.h" #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include "database.h" #include "escape.h" #include "file.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 static llist rules; static atomic_ulong allowed = 0, denied = 0; static nvlist_t fields[MAX_SYSLOG_FIELDS]; static unsigned int num_fields; static unsigned int 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)); static int lookup_field(const char *ptr) { if (strcmp("rule", ptr) == 0) { fields[num_fields].name = strdup(ptr); fields[num_fields].item = F_RULE; goto success; } else if (strcmp("dec", ptr) == 0) { fields[num_fields].name = strdup(ptr); fields[num_fields].item = F_DECISION; goto success; } else if (strcmp("perm", ptr) == 0) { fields[num_fields].name = strdup(ptr); fields[num_fields].item = F_PERM; goto success; } else if (strcmp(":", ptr) == 0) { fields[num_fields].name = strdup(ptr); fields[num_fields].item = F_COLON; parsing_obj = 1; goto success; } 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: syslog_proc_status_mask |= PROC_STAT_UID; break; case PPID: syslog_proc_status_mask |= PROC_STAT_PPID; break; case GID: syslog_proc_status_mask |= PROC_STAT_GID; break; case COMM: syslog_proc_status_mask |= PROC_STAT_COMM; break; default: break; } fields[num_fields].name = strdup(ptr); fields[num_fields].item = ret_val; goto success; } } } 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 { fields[num_fields].name = strdup(ptr); fields[num_fields].item = ret_val; goto success; } } } return 0; success: num_fields++; return 1; } // This function returns 1 on success, 0 on failure static int parse_syslog_format(const char *syslog_format) { char *ptr, *saved, *tformat; int rc = 1; if (strchr(syslog_format, ':') == NULL) { msg(LOG_ERR, "syslog_format does not have a ':'"); return 0; } num_fields = 0; parsing_obj = 0; syslog_proc_status_mask = 0; tformat = strdup(syslog_format); // Must be delimited by comma ptr = strtok_r(tformat, ",", &saved); while (ptr && rc && num_fields < MAX_SYSLOG_FIELDS) { rc = lookup_field(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(void) { int fd; FILE *f; // 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) { msg(LOG_INFO, "Ruleset identity: %s", sha_buf); 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)); } return f; } // Returns 0 on success and 1 on error static int _load_rules(const conf_t *_config, FILE *f) { int rc, lineno = 1; char *line = NULL; size_t len = 0; if (rules_create(&rules)) 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(&rules, line, lineno); if (rc) { free(line); return 1; } lineno++; } free(line); rules_regen_sets(&rules); if (rules.cnt == 0) { msg(LOG_INFO, "No rules in file - exiting"); return 1; } else { msg(LOG_DEBUG, "Loaded %u rules", rules.cnt); } rc = parse_syslog_format(_config->syslog_format); if (!rc || num_fields == 0) return 1; return 0; } int load_rules(const conf_t *_config) { if (init_attr_sets()) return 1; FILE * f = open_file(); if (f == NULL) { destroy_attr_sets(); return 1; } int res = _load_rules(_config, f); fclose(f); if (res) { destroy_attr_sets(); return 1; } return 0; } void destroy_rules(void) { unsigned int i = 0; rules_clear(&rules); while (i < num_fields) { free((void *)fields[i].name); i++; } destroy_attr_sets(); if (stop) free(working_buffer); } unsigned int policy_get_syslog_proc_status_mask(void) { return syslog_proc_status_mask; } void set_reload_rules(void) { reload_rules = true; } static FILE * ff = NULL; int load_rule_file(void) { if (ff) { fclose(ff); ff = NULL; } ff = open_file(); if (ff == NULL) return 1; return 0; } int do_reload_rules(const conf_t *_config) { destroy_rules(); if (init_attr_sets()) return 1; _load_rules(_config, ff); fclose(ff); ff = NULL; 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) { 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 { // 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(unsigned int num, decision_t results, event_t *e) { int mode = results & SYSLOG ? LOG_INFO : LOG_DEBUG; unsigned int i; size_t dsize; ptrdiff_t written; char *p1, *p2, *val; if (working_buffer == NULL) { working_buffer = malloc(WB_SIZE); if (working_buffer == NULL) { msg(LOG_ERR, "No working buffer for logging"); return; } } dsize = WB_SIZE; p1 = p2 = working_buffer; // Dummy assignment for p1 to quiet warnings for (i = 0; i < 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, fields[i].name, dsize); written = p1 - p2; if ((size_t)written > dsize) break; dsize -= (size_t)written; if (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(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_t process_event(event_t *e) { decision_t results = NO_OPINION; /* populate the event struct and iterate over the rules */ rules_first(&rules); lnode *r = rules_get_cur(&rules); //int cnt = 0; while (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; r = rules_next(&rules); //cnt++; } // Output some information if debugging on or syslogging requested if ( (results & SYSLOG) || (debug_mode == 1) || (debug_mode > 1 && (results & DENY)) ) log_it(r ? r->num : 0xFFFFFFFF, results, e); // Record which rule (rules are 1 based when listed by the cli tool) if (r) e->num = r->num + 1; // If we are not in permissive mode, return any decision if (results != NO_OPINION) return results; return ALLOW; } #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) { close(metadata->fd); #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; 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; write(fd, &f, sizeof(struct fan_audit_response)); return; } #endif struct fanotify_response response; response.fd = metadata->fd; response.response = reply; write(fd, &response, sizeof(struct fanotify_response)); } void make_policy_decision(const struct fanotify_event_metadata *metadata, int fd, uint64_t mask) { event_t e; int decision; if (new_event(metadata, &e)) decision = FAN_DENY; else { lock_rule(); decision = process_event(&e); unlock_rule(); } if ((decision & DENY) == DENY) atomic_fetch_add_explicit(&denied, 1, memory_order_relaxed); else atomic_fetch_add_explicit(&allowed, 1, memory_order_relaxed); if (metadata->mask & mask) { // 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 (config.permissive) reply_event(fd, metadata,FAN_ALLOW | (decision & AUDIT), &e); else reply_event(fd, metadata, decision & FAN_RESPONSE_MASK, &e); } } unsigned long getAllowed(void) { return atomic_load_explicit(&allowed, memory_order_relaxed); } unsigned long getDenied(void) { return atomic_load_explicit(&denied, memory_order_relaxed); } void policy_no_audit(void) { rules_unsupport_audit(&rules); } fapolicyd-1.4.3/src/library/policy.h000066400000000000000000000041411513023701500173710ustar00rootroot00000000000000/* * 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 "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; int dec_name_to_val(const char *name); int load_rules(const conf_t *config); 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); void reply_event(int fd, const struct fanotify_event_metadata *metadata, unsigned reply, event_t *e); void make_policy_decision(const struct fanotify_event_metadata *metadata, int fd, uint64_t mask); unsigned long getAllowed(void); unsigned long getDenied(void); void policy_no_audit(void); void destroy_rules(void); unsigned int policy_get_syslog_proc_status_mask(void); #endif fapolicyd-1.4.3/src/library/process.c000066400000000000000000000251251513023701500175500ustar00rootroot00000000000000/* * 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 "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->state = STATE_COLLECTING; info->elf_info = 0; return info; } return NULL; } void clear_proc_info(struct proc_info *info) { 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] == ')') { if (strcmp(&buf[len - 10], " (deleted)") == 0) buf[len - 10] = '\0'; } return buf; } char *get_type_from_pid(pid_t pid, size_t blen, char *buf) { int fd; 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; // 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, 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; } /* * read_proc_status - Collect 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 * * 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(pid_t pid, unsigned int fields, struct proc_status_info *info) { char buf[80]; int fd, rc = 0; unsigned int found = 0; if (info == NULL || fields == 0) return 0; // Initialize info struct if (fields & PROC_STAT_UID) { if (info->uid) { destroy_attr_set(info->uid); free(info->uid); info->uid = NULL; } info->uid = init_standalone_set(UNSIGNED); if (info->uid == NULL) return -1; } if (fields & PROC_STAT_GID) { if (info->groups) { destroy_attr_set(info->groups); free(info->groups); info->groups = NULL; } info->groups = init_standalone_set(UNSIGNED); if (info->groups == NULL) { if (fields & PROC_STAT_UID) { destroy_attr_set(info->uid); free(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; const char *path = proc_path(pid, "/status"); fd = open(path, O_RDONLY|O_CLOEXEC); if (fd < 0) { if (fields & PROC_STAT_UID) { destroy_attr_set(info->uid); free(info->uid); info->uid = NULL; } if (fields & PROC_STAT_GID) { destroy_attr_set(info->groups); free(info->groups); info->groups = NULL; } return -1; } fd_fgets_state_t *st = fd_fgets_init(); if (st == NULL) { close(fd); return -1; } do { rc = fd_fgets_r(st, buf, sizeof(buf), fd); if (rc == -1) break; else if (rc > 0) { 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; } /* * 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) && is_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) append_int_attr_set(info->uid, (int64_t)real_uid); if (fields_read >= 2) append_int_attr_set(info->uid, (int64_t)eff_uid); if (fields_read >= 4) append_int_attr_set(info->uid, (int64_t)fs_uid); } found |= PROC_STAT_UID; continue; } if ((fields & PROC_STAT_GID) && is_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) append_int_attr_set(info->groups, (int64_t)real_gid); if (fields_read >= 2) append_int_attr_set(info->groups, (int64_t)eff_gid); if (fields_read >= 4) append_int_attr_set(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) { char *data = buf + 7; int offset; unsigned int gid; while (sscanf(data, " %u%n", &gid, &offset) == 1) { data += offset; append_int_attr_set(info->groups, (int64_t)gid); } } found |= PROC_STAT_GID; 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); close(fd); return 0; } // 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; FILE *f; const char *path = proc_path(pid, "/environ"); f = fopen(path, "rt"); if (f) { __fsetlocking(f, FSETLOCKING_BYCALLER); while (getline(&line, &len, f) != -1) { char *match = strstr(line, "LD_PRELOAD"); if (!match) match = strstr(line, "LD_AUDIT"); if (match) { rc = 1; break; } } fclose(f); if (rc == -1) rc = 0; free(line); } return rc; } fapolicyd-1.4.3/src/library/process.h000066400000000000000000000071051513023701500175530ustar00rootroot00000000000000/* * 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, }; /* * Results from read_proc_status() */ struct proc_status_info { pid_t ppid; 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; 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(pid_t pid, unsigned int fields, struct proc_status_info *info); int check_environ_from_pid(pid_t pid); #endif fapolicyd-1.4.3/src/library/queue.c000066400000000000000000000150461513023701500172170ustar00rootroot00000000000000/* * 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 "queue.h" #include "message.h" /* * Ring buffer queue * * The queue is a fixed-size ring of struct fanotify_event_metadata. 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. max_depth records the highest * queue_length observed for diagnostics. */ /* Queue implementation */ static atomic_uint max_depth; /* 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 fanotify_event_metadata)) { errno = EINVAL; return NULL; } q = malloc(sizeof(*q)); if (q == NULL) return NULL; q->events = calloc(num_entries, sizeof(struct fanotify_event_metadata)); 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); max_depth = 0; 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) { sem_destroy(&q->sem); free(q->events); msg(LOG_DEBUG, "Inter-thread max queue depth %u", max_depth); free(q); } void q_report(FILE *f) { fprintf(f, "Inter-thread max queue depth: %u\n", max_depth); } /* add DATA to Q */ int q_enqueue(struct queue *q, const struct fanotify_event_metadata *data) { unsigned int n; if (atomic_load_explicit(&q->queue_length, memory_order_relaxed) == q->num_entries) { 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] = *data; 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(&max_depth); while (n > old && !atomic_compare_exchange_weak(&max_depth, &old, n)) ; sem_post(&q->sem); return 0; } /* remove one event from Q */ int q_dequeue(struct queue *q, struct fanotify_event_metadata *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]; 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, struct fanotify_event_metadata *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]; 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) { sem_post(&q->sem); } fapolicyd-1.4.3/src/library/queue.h000066400000000000000000000045171513023701500172250ustar00rootroot00000000000000/* * 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 #include "gcc-attributes.h" struct queue { /* Ring buffer of fanotify events */ struct fanotify_event_metadata *events; size_t num_entries; atomic_uint q_next; atomic_uint q_last; atomic_uint queue_length; sem_t sem; }; /* 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); /* Write out q_depth */ void q_report(FILE *f); /* Add DATA to tail of Q. Return 0 on success, -1 on error and set errno. */ int q_enqueue(struct queue *q, const struct fanotify_event_metadata *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, struct fanotify_event_metadata *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, struct fanotify_event_metadata *data, const struct timespec *ts); /* Wake up anyone waiting on the queue. */ void q_shutdown(struct queue *q); /* Return the number of entries in Q. */ static inline size_t q_queue_length(const struct queue *q) { return atomic_load_explicit(&q->queue_length, memory_order_relaxed); } #endif fapolicyd-1.4.3/src/library/rpm-backend.c000066400000000000000000000252041513023701500202530ustar00rootroot00000000000000/* * 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, "/usr/bin/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, 4095, "%s %s", file_name, 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(memfd, "%s %s\n", file_name, data) < 0) { msg(LOG_ERR, "dprintf failed writing %s to memfd (%s)", file_name, strerror(errno)); free((void*)file_name); free((void*)data); rc = 1; goto out; } entries++; free((void*)file_name); free((void*)data); } else { free((void*)file_name); free((void*)data); } } else { free((void*)file_name); } } } rc = 0; out: close_rpm(); // cleaning up struct _hash_record *item, *tmp; HASH_ITER( hh, hashtable, item, tmp) { HASH_DEL( hashtable, item ); free((void*)item->key); free((void*)item); } if (rc == 0) rpm_backend.entries = entries; return rc; } static int rpm_init_backend(void) { return 0; } // this function is used in fapolicyd-rpm-loader int do_rpm_init_backend(void) { if (filter_init()) return 1; if (filter_load_file(NULL)) { filter_destroy(); return 1; } return 0; } static int rpm_destroy_backend(void) { return 0; } // this function is used in fapolicyd-rpm-loader int do_rpm_destroy_backend(void) { filter_destroy(); return 0; } fapolicyd-1.4.3/src/library/rules.c000066400000000000000000001057351513023701500172320ustar00rootroot00000000000000/* * 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 "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 // 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 unsigned int proc_status_mask; static int assign_subject(lnode *n, int type, const char *ptr2, int lineno) __wur; static int assign_object(lnode *n, int type, const char *ptr2, int lineno) __wur; int rules_create(llist *l) { l->head = NULL; l->cur = NULL; l->cnt = 0; 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(lnode *n, int type, const char *ptr2, int lineno) { size_t index = 0; // assign the subject unsigned int i = n->s_count; 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) proc_status_mask |= PROC_STAT_UID; else if (type == PPID) proc_status_mask |= PROC_STAT_PPID; else if (type == GID) proc_status_mask |= PROC_STAT_GID; else if (type == COMM) 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; } index = search_attr_set_by_name(defined_set); if (!index) { msg(LOG_ERR, "rules: line:%d: assign_subject: " "set \'%s\' was not defined before", lineno, defined_set); goto free_and_error; } attr_sets_entry_t * set = get_attr_set(index); if (!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; } if (type <= PPID) { 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].gr_index = index; 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; if (add_attr_set(name, set_type, &index)) { goto free_and_error; } attr_sets_entry_t * set = get_attr_set(index); 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 (append_int_attr_set(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 (append_int_attr_set(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 (append_int_attr_set(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 (append_int_attr_set(set, (int64_t)val)) goto free_and_error; } ptr = strtok_r(NULL, ",", &saved); } n->s[i].gr_index = index; 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: { if (add_attr_set(name, STRING, &index)) { goto free_and_error; } attr_sets_entry_t * set = get_attr_set(index); if (!set) goto free_and_error; ptr = strtok_r(tmp, ",", &saved); while (ptr) { append_str_attr_set(set, ptr); ptr = strtok_r(NULL, ",", &saved); } n->s[i].gr_index = index; 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: free(tmp); return 1; } static int assign_object(lnode *n, int type, const char *ptr2, int lineno) { // assign the object unsigned int i = n->o_count; size_t index = 0; 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; } index = search_attr_set_by_name(defined_set); if (!index) { msg(LOG_ERR, "rules: line:%d: assign_object: " "set \'%s\' was not defined before", lineno, defined_set); goto free_and_error; } attr_sets_entry_t * set = get_attr_set(index); if (!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].gr_index = index; 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: case FMODE: { if (add_attr_set(name, STRING, &index)) { goto free_and_error; } attr_sets_entry_t * set = get_attr_set(index); if (!set) goto free_and_error; ptr = strtok_r(tmp, ",", &saved); while (ptr) { append_str_attr_set(set, ptr); ptr = strtok_r(NULL, ",", &saved); } n->o[i].gr_index = index; 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: free(tmp); return 1; } static int parse_new_format(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(n, type, ptr2, lineno) == 3) 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 assign_object(n, type, ptr2, lineno); } } else if (state == 0 && strcmp(ptr, ":") == 0) state = 1; else if (strcmp(ptr, "all") == 0) { if (state == 0) { type = ALL_SUBJ; assign_subject(n, type, "", lineno); } else { type = ALL_OBJ; assign_object(n, type, "", lineno); } } else { msg(LOG_ERR, "'=' is missing for field %s, in line %d", ptr, lineno); return 5; } } return 0; } static int parse_set_line(const char * line, int lineno) { if (!line) return -1; // for sure 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; } size_t index = 0; index = search_attr_set_by_name(name); // looking for non zero result if (index) { 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; if (add_attr_set(name, type, &index)) { goto free_and_error; } attr_sets_entry_t * set = get_attr_set(index); 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 (append_str_attr_set(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 (append_int_attr_set(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 (append_int_attr_set(set, (int64_t)val)) goto free_and_error; } ptr = strtok_r(NULL, ",", &saved); } free(l); return -1; free_and_error: free(l); return 1; } /* * This function take a whole rule as input and parses it up. * Returns: -1 nothing, 0 OK, >0 error */ static int nv_split(char *buf, lnode *n, int lineno) { char *ptr, *ptr2; rformat_t format = RULE_FMT_ORIG; if (strchr(buf, ':')) format = RULE_FMT_COLON; n->format = format; ptr = strtok(buf, " "); if (ptr == NULL) return -1; /* If there's nothing, go to next line */ if (ptr[0] == '#') return -1; /* If there's a comment, go to next line */ if (ptr[0] == '%') return parse_set_line(ptr, lineno); // 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 1; } // 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 2; } } 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 1; } if (assign_subject(n, type, ptr2, lineno) == 3) return -1; } parse_new_format(n, lineno); 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 3; } else assign_object(n, type, ptr2, lineno); } else if (assign_subject(n, type, ptr2, lineno) == 3) return -1; } else if (strcmp(ptr, "all") == 0) { if (n->s_count == 0) { type = ALL_SUBJ; assign_subject(n, type, "", lineno); } else if (n->o_count == 0) { type = ALL_OBJ; assign_object(n, type, "", lineno); } else { msg(LOG_ERR, "All can only be used in place of a subject or object"); return 4; } } else { msg(LOG_ERR, "'=' is missing for field %s, in line %d", ptr, lineno); return 5; } } 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 6; } if (n->o_count == 0) { msg(LOG_ERR, "Object is missing in line %d", lineno); return 7; } return 0; } // 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) { // 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; for (i=0; is[i].type = UNUSED; newnode->o[i].type = UNUSED; } int rc = nv_split(buf, newnode, lineno); if (rc) { free(newnode); if (rc < 0) return 0; else 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 (!check_int_attr_set(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 (!check_int_attr_set(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 // simple presence check case EXE: { if (check_str_attr_set(r->s[cnt].set, "untrusted")) if (is_subj_trusted(e)) return 0; } // case // fall through case COMM: case EXE_TYPE: { if (!subj->str) { break; } if (!check_str_attr_set(r->s[cnt].set, subj->str)) return 0; break; } // case case EXE_DIR: { if (!subj->str) { break; } if (check_str_attr_set(r->s[cnt].set, "execdirs")) if (check_dirs(1, subj->str)) return 0; if (check_str_attr_set(r->s[cnt].set, "systemdirs")) if (check_dirs(0, subj->str)) return 0; // DEPRECATED if (check_str_attr_set(r->s[cnt].set, "untrusted")) if (is_subj_trusted(e)) return 0; // check partial match (via strncmp) // subdir test if (!check_pstr_attr_set(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 (check_str_attr_set(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 (check_str_attr_set(r->s[cnt].set, "untrusted")) if (is_obj_trusted(e)) return 0; } // fall through case DEVICE: case FILE_HASH: case FMODE: { if (!obj->o) { // Treat errors as denial for file hash lookups if (type == FILE_HASH) return 0; break; } if (!check_str_attr_set(r->o[cnt].set, obj->o)) return 0; break; } // case case ODIR: { if (!obj->o) { break; } if (check_str_attr_set(r->o[cnt].set, "execdirs")) if (check_dirs(1, obj->o)) return 0; if (check_str_attr_set(r->o[cnt].set, "systemdirs")) if (check_dirs(0, obj->o)) return 0; // DEPRECATED if (check_str_attr_set(r->o[cnt].set, "untrusted")) if (is_obj_trusted(e)) return 0; // check partial match (via strncmp) // subdir test if (!check_pstr_attr_set(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; } 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 } // Function iterates over rules and theirs subjects and objects // and it is setting direct pointer to set instead of // set index. // Rules are created with set index because internal set // array structure can be reallocated when it grows. // We can safely call this function once right after // rules.conf was processed. void rules_regen_sets(llist *l) { lnode *nextnode; lnode *current = l->head; while (current) { unsigned int i; nextnode=current->next; i = 0; while (i < current->s_count) { int type = current->s[i].type; if (type == ALL_SUBJ || type == SUBJ_TRUST || type == PATTERN) { i++; continue; } size_t index = current->s[i].gr_index; attr_sets_entry_t * set = get_attr_set(index); if (!set) { i++; continue; } current->s[i].set = set; i++; } i = 0; while (i < current->o_count) { int type = current->o[i].type; if (type == ALL_OBJ || type == OBJ_TRUST) { i++; continue; } size_t index = current->o[i].gr_index; attr_sets_entry_t * set = get_attr_set(index); if (!set) { i++; continue; } current->o[i].set = set; i++; } current=nextnode; } } void rules_clear(llist *l) { lnode *nextnode; register lnode *current = l->head; while (current) { nextnode=current->next; free(current); current=nextnode; } l->head = NULL; l->cur = NULL; l->cnt = 0; 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(void) { return proc_status_mask; } fapolicyd-1.4.3/src/library/rules.h000066400000000000000000000042361513023701500172310ustar00rootroot00000000000000/* * rules.h - Header file for rules.c * Copyright (c) 2016-17,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 */ #ifndef RULES_HEADER #define RULES_HEADER #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; 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; // Pointer to current node unsigned int cnt; // How many items in this list } llist; int rules_create(llist *l); void rules_first(llist *l); lnode *rules_next(llist *l); static inline lnode *rules_get_cur(const llist *l) { return l->cur; } int rules_append(llist *l, char *buf, unsigned int lineno) __wur; __attribute__((hot)) decision_t rule_evaluate(lnode *r, event_t *e); void rules_unsupport_audit(const llist *l); void rules_regen_sets(llist* l); void rules_clear(llist* l); unsigned int rules_get_proc_status_mask(void); #endif fapolicyd-1.4.3/src/library/stack.c000066400000000000000000000036611513023701500172000ustar00rootroot00000000000000/* * 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, void *_data) { if (_stack == NULL) return; list_prepend(_stack, NULL, (void *)_data); } // pop the the top without returning what was on the top void stack_pop(stack_t *_stack) { if (_stack == NULL) return; list_item_t *first = _stack->first; _stack->first = first->next; first->data = NULL; list_destroy_item(&first); _stack->count--; return; } // function returns 1 if stack is emtpy 0 if it's not int stack_is_empty(const stack_t *_stack) { if (_stack == NULL) return -1; if (_stack->count == 0) return 1; return 0; } // return top of the stack without popping const void *stack_top(const stack_t *_stack) { if (_stack == NULL) return NULL; return _stack->first ? _stack->first->data : NULL; } fapolicyd-1.4.3/src/library/stack.h000066400000000000000000000023231513023701500171770ustar00rootroot00000000000000/* * 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, void *_data); void stack_pop(stack_t *_stack); int stack_is_empty(const stack_t *_stack); const void *stack_top(const stack_t *_stack); #endif // STACK_H_ fapolicyd-1.4.3/src/library/string-util.c000066400000000000000000000037411513023701500203530ustar00rootroot00000000000000/* * string-util.c - useful string functions * Copyright (c) 2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Radovan Sroka * Zoltan Fridrich */ #include #include #include #include #include "string-util.h" #pragma GCC optimize("O3") char *fapolicyd_strtrim(char *s) { if (!s) return NULL; // skip leading spaces char *start = s; while (*start && isspace((unsigned char)*start)) start++; // shift left (no-op if start == s) size_t len = strlen(start); memmove(s, start, len + 1); // includes the '\0' // all spaces? if (*s == '\0') return s; // trim trailing char *end = s + len - 1; while (end >= s && isspace((unsigned char)*end)) *end-- = '\0'; return s; } char *fapolicyd_strcat(const char *s1, const char *s2) { size_t s1_len = strlen(s1); size_t s2_len = strlen(s2); char *r = malloc(s1_len + s2_len + 1); if (r == NULL) return NULL; memcpy(r, s1, s1_len); memcpy(r + s1_len, s2, s2_len + 1); // includes null terminator return r; } char *fapolicyd_strnchr(const char *s, int c, size_t len) { unsigned char uc = (unsigned char)c; for (; len--; ++s) { if ((unsigned char)*s == uc) return (char *)s; if (*s == '\0') break; } return NULL; } fapolicyd-1.4.3/src/library/string-util.h000066400000000000000000000026161513023701500203600ustar00rootroot00000000000000/* * 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 fapolicyd-1.4.3/src/library/subject-attr.c000066400000000000000000000051621513023701500205000ustar00rootroot00000000000000/* * subject-attr.c - functions to abstract subject attributes * Copyright (c) 2016,2019,2022 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * Radovan Sroka */ #include "config.h" #include // For NULL #include #include "subject-attr.h" /* * Table1 is used for looking up rule fields written in the old format. * Do not add anything new here. */ static const nv_t table1[] = { { ALL_SUBJ, "all" }, { AUID, "auid" }, { UID, "uid" }, { SESSIONID, "sessionid" }, { PID, "pid" }, { PATTERN, "pattern" }, { COMM, "comm" }, { EXE, "exe" }, { EXE_DIR, "exe_dir" }, { EXE_TYPE, "exe_type" }, }; #define MAX_SUBJECTS1 (sizeof(table1)/sizeof(table1[0])) /* * Table2 is used for looking up rule fields written in the new format */ static const nv_t table2[] = { { ALL_SUBJ, "all" }, { AUID, "auid" }, { UID, "uid" }, { SESSIONID, "sessionid" }, { PID, "pid" }, { PPID, "ppid" }, { PATTERN, "pattern" }, { SUBJ_TRUST, "trust" }, { GID, "gid" }, { COMM, "comm" }, { EXE, "exe" }, { EXE_DIR, "dir" }, { EXE_TYPE, "ftype" }, }; #define MAX_SUBJECTS2 (sizeof(table2)/sizeof(table2[0])) int subj_name_to_val(const char *name, rformat_t format) { unsigned int i = 0; if (format == RULE_FMT_ORIG) { while (i < MAX_SUBJECTS1) { if (strcmp(name, table1[i].name) == 0) return table1[i].value; i++; } } else { while (i < MAX_SUBJECTS2) { if (strcmp(name, table2[i].name) == 0) return table2[i].value; i++; } } return -1; } const char *subj_val_to_name(unsigned int v, rformat_t format) { if (v > SUBJ_END) return NULL; unsigned int index = v - SUBJ_START; if (format == RULE_FMT_ORIG) { if (index < MAX_SUBJECTS1) return table1[index].name; } else { if (index < MAX_SUBJECTS2) return table2[index].name; } return NULL; } fapolicyd-1.4.3/src/library/subject-attr.h000066400000000000000000000030741513023701500205050ustar00rootroot00000000000000/* * 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; size_t gr_index; attr_sets_entry_t * set; }; } subject_attr_t; int subj_name_to_val(const char *name, rformat_t format); const char * subj_val_to_name(unsigned v, rformat_t format); #endif fapolicyd-1.4.3/src/library/subject.c000066400000000000000000000112271513023701500175270ustar00rootroot00000000000000/* * 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 void subject_create(s_array *a) { int i; a->subj = malloc(sizeof(subject_attr_t *) * SUBJ_COUNT); for (i = 0; i < SUBJ_COUNT; i++) a->subj[i] = NULL; a->cnt = 0; a->info = NULL; } #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) { 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; 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) { sanity_check_array(a, "subject_find_exe"); if (a && a->subj[EXE - SUBJ_START]) return a->subj[EXE - SUBJ_START]; return NULL; } subject_attr_t *subject_find_comm(const s_array *a) { sanity_check_array(a, "subject_find_comm"); if (a && 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; 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 borrow dynamically allocated * sets; release both the set contents and container. */ destroy_attr_set(current->set); free(current->set); } else if (current->type >= COMM) free(current->str); free(current); a->subj[i] = NULL; } clear_proc_info(a->info); free(a->info); a->info = NULL; free(a->subj); a->subj = NULL; a->cnt = 0; } void subject_reset(s_array *a, subject_type_t t) { if (a == 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 borrow dynamically allocated * sets; release both the set contents and container. */ destroy_attr_set(current->set); free(current->set); } else if (current->type >= COMM) free(current->str); free(current); a->subj[t - SUBJ_START] = NULL; a->cnt--; sanity_check_array(a, "subject_reset2"); } } fapolicyd-1.4.3/src/library/subject.h000066400000000000000000000032241513023701500175320ustar00rootroot00000000000000/* * 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; void subject_create(s_array *a); subject_attr_t *subject_access(const s_array *a, subject_type_t t); int subject_add(s_array *a, const subject_attr_t *subj); subject_attr_t *subject_find_exe(const s_array *a); subject_attr_t *subject_find_comm(const s_array *a); void subject_reset(s_array *a, subject_type_t t); void subject_clear(s_array* a); static inline int type_is_subj(int type) {if (type < OBJ_START) return 1; else return 0;} #endif fapolicyd-1.4.3/src/library/trust-file.c000066400000000000000000000457411513023701500201760ustar00rootroot00000000000000/* * 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 "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 (4096+1+1+1+10+1+FILE_DIGEST_STRING_WIDTH+1) #define FILE_READ_FORMAT "%4096s %lu %64s" // path size SHA256 #define FILE_WRITE_FORMAT "%s %lu %s\n" // path size SHA256 #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) { char buf[BUFFER_SIZE + 1]; const char *data = (char *)(lptr->data); const char *path = (char *)lptr->index; /* * + 2 because we are omitting source number * "0 12345 ..." * 0 -> filedb source */ hlen = snprintf(buf, sizeof(buf), "%s %s\n", path, data + 2); fwrite(buf, hlen, 1, f); } 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 == 2) { // exit on parse error, 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. * * Returns 0 when parsing succeeds or -1 when the line is malformed. */ static int parse_line_backwards(char *line, char *path, unsigned long *size, char *sha, size_t sha_size) { if (line == NULL || path == NULL || size == NULL || sha == NULL) 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 to arg // right from the last delimiter to the end of the line size_t sha_width = &line[len-1] - delims[0]; if (sha_width >= sha_size) sha_width = sha_size - 1; memcpy(sha, delims[0] + 1, sha_width); sha[sha_width] = '\0'; // save size to arg char number[1024]; size_t number_size = delims[0] - delims[1]+1; memcpy(number, delims[1]+1, number_size); char *endptr; *size = strtol(number, &endptr, 10); // save path to arg size_t path_size = delims[1] - line; if (path_size >= 4097) path_size = 4096; 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[4097], sha[FILE_DIGEST_STRING_MAX], *index = NULL, *data = NULL; char data_buf[BUFFER_SIZE]; unsigned long 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; } } free((char *)lptr->data); lptr->data = make_data_string(lptr->index); ++count; } } if (count) write_out_list(&list, fpath); list_empty(&list); return count; } /* * trust_file_rm_duplicates - Prune CLI additions already present on disk. * @fpath: Trust fragment checked for duplicates. * @list: Pending CLI additions to compare against existing entries. * * Used only by the CLI trust management commands before appending new * entries. Returns 0 after pruning, * or -1 when the trust fragment could not be opened or parsed. */ int trust_file_rm_duplicates(const char *fpath, list_t *list) { list_t trust_file; list_init(&trust_file); int rc = trust_file_load(fpath, &trust_file, -1); switch (rc) { case 1: msg(LOG_ERR, "Cannot open %s", fpath); return -1; case 2: list_empty(&trust_file); return -1; default: break; } for (list_item_t *lptr = trust_file.first; lptr; lptr = lptr->next) { list_remove(list, lptr->index); if (list->count == 0) break; } list_empty(&trust_file); return 0; } /* * ftw_load - nftw callback that aggregates trust fragments. * @fpath: Current file discovered by nftw. * @sb: (unused) file metadata supplied by nftw. * @typeflag: nftw entry type. * @ftwbuf: (unused) traversal context from nftw. */ static int ftw_load(const char *fpath, const struct stat *sb __attribute__ ((unused)), int typeflag, struct FTW *ftwbuf __attribute__ ((unused))) { if (typeflag == FTW_F) trust_file_load(fpath, &_list, _memfd); return FTW_CONTINUE; } /* * ftw_delete_path - nftw callback that deletes matching entries. * @fpath: Current trust fragment examined by nftw. * @sb: (unused) file metadata supplied by nftw. * @typeflag: nftw entry type. * @ftwbuf: (unused) traversal context from nftw. */ static int ftw_delete_path(const char *fpath, const struct stat *sb __attribute__ ((unused)), int typeflag, struct FTW *ftwbuf __attribute__ ((unused))) { if (typeflag == FTW_F) _count += trust_file_delete_path(fpath, _path); return FTW_CONTINUE; } /* * ftw_update_path - nftw callback that updates matching entries. * @fpath: Current trust fragment examined by nftw. * @sb: (unused) file metadata supplied by nftw. * @typeflag: nftw entry type. * @ftwbuf: (unused) traversal context from nftw. */ static int ftw_update_path(const char *fpath, const struct stat *sb __attribute__ ((unused)), int typeflag, struct FTW *ftwbuf __attribute__ ((unused))) { if (typeflag == FTW_F) _count += trust_file_update_path(fpath, _path, _use_filter); return FTW_CONTINUE; } /* * ftw_rm_duplicates - nftw callback removing duplicates from CLI lists. * @fpath: Current trust fragment examined by nftw. * @sb: (unused) file metadata supplied by nftw. * @typeflag: nftw entry type. * @ftwbuf: (unused) traversal context from nftw. */ static int ftw_rm_duplicates(const char *fpath, const struct stat *sb __attribute__ ((unused)), int typeflag, struct FTW *ftwbuf __attribute__ ((unused))) { if (_list.count == 0) return FTW_STOP; if (typeflag == FTW_F) trust_file_rm_duplicates(fpath, &_list); return FTW_CONTINUE; } /* * trust_file_load_all - Aggregate every trust fragment. * @list: Destination list when @memfd is negative. * @memfd: File descriptor that receives streamed entries, or -1. * * Used by both the daemon and CLI to populate either an in-memory list or * a memfd-backed snapshot covering the primary trust file plus the tree * of per-package fragments. */ void trust_file_load_all(list_t *list, int memfd) { list_empty(&_list); _memfd = memfd; /* Populate either the in-memory list or the memfd snapshot. */ trust_file_load(TRUST_FILE_PATH, &_list, memfd); nftw(TRUST_DIR_PATH, &ftw_load, FTW_NOPENFD, FTW_FLAGS); if (memfd < 0) { if (list) list_merge(list, &_list); } else list_empty(&_list); _memfd = -1; } /* * trust_file_delete_path_all - Delete matching entries across all files. * @path: Prefix designating entries to remove. * * Used only by the CLI trust management commands to remove a path from * every trust fragment. Returns the number of entries deleted. */ int trust_file_delete_path_all(const char *path) { _path = strdup(path); _count = trust_file_delete_path(TRUST_FILE_PATH, path); nftw(TRUST_DIR_PATH, &ftw_delete_path, FTW_NOPENFD, FTW_FLAGS); free(_path); return _count; } /* * trust_file_update_path_all - Refresh hashes across every trust file. * @path: Prefix designating entries that must be re-measured. * * Used only by the CLI trust management commands. Returns the number of * entries updated. */ int trust_file_update_path_all(const char *path, bool use_filter) { _path = strdup(path); _use_filter = use_filter; _count = trust_file_update_path(TRUST_FILE_PATH, path, _use_filter); nftw(TRUST_DIR_PATH, &ftw_update_path, FTW_NOPENFD, FTW_FLAGS); free(_path); _use_filter = false; return _count; } /* * trust_file_rm_duplicates_all - Remove duplicates across trust files. * @list: Pending CLI additions to prune before appending. * * Used only by the CLI trust management commands prior to calling * trust_file_append(). */ void trust_file_rm_duplicates_all(list_t *list) { list_empty(&_list); list_merge(&_list, list); trust_file_rm_duplicates(TRUST_FILE_PATH, &_list); nftw(TRUST_DIR_PATH, &ftw_rm_duplicates, FTW_NOPENFD, FTW_FLAGS); list_merge(list, &_list); } fapolicyd-1.4.3/src/library/trust-file.h000066400000000000000000000031501513023701500201670ustar00rootroot00000000000000/* * trust-file.h - Header for managing trust files * Copyright (c) 2020 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Zoltan Fridrich */ #ifndef TRUST_FILE_H #define TRUST_FILE_H #include #include "llist.h" #define TRUST_FILE_PATH "/etc/fapolicyd/fapolicyd.trust" #define TRUST_DIR_PATH "/etc/fapolicyd/trust.d/" int trust_file_append(const char *fpath, list_t *list); int trust_file_load(const char *fpath, list_t *list, int memfd); int trust_file_update_path(const char *fpath, const char *path, bool use_filter); int trust_file_delete_path(const char *fpath, const char *path); int trust_file_rm_duplicates(const char *fpath, list_t *list); void trust_file_load_all(list_t *list, int memfd); int trust_file_update_path_all(const char *path, bool use_filter); int trust_file_delete_path_all(const char *path); void trust_file_rm_duplicates_all(list_t *list); #endif fapolicyd-1.4.3/src/tests/000077500000000000000000000000001513023701500154175ustar00rootroot00000000000000fapolicyd-1.4.3/src/tests/Makefile.am000066400000000000000000000076331513023701500174640ustar00rootroot00000000000000# 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 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 event_test trustdb_format_test lru_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}\" trustdb_format_test_SOURCES = trustdb_format_test.c lru_test_SOURCES = lru_test.c test-stubs.c lru_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la event_test_SOURCES = event_test.c event_test_LDADD = \ ${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 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) fapolicyd-1.4.3/src/tests/attr_sets_test.c000066400000000000000000000011341513023701500206310ustar00rootroot00000000000000#include #include #include #include "attr-sets.h" /* * main - ensure invalid type values are rejected * Returns 0 on success, exits with error on failure */ int main(void) { int ret; size_t idx = 5; ret = init_attr_sets(); if (ret) error(1, 0, "init_attr_sets failed"); ret = add_attr_set("bad", 0, &idx); if (ret == 0) error(1, 0, "add_attr_set accepted invalid type"); if (idx != 5) error(1, 0, "index modified on invalid type"); if (search_attr_set_by_name("bad") != 0) error(1, 0, "invalid set inserted"); destroy_attr_sets(); return 0; } fapolicyd-1.4.3/src/tests/avl_test.c000066400000000000000000000125271513023701500174130ustar00rootroot00000000000000#include #include #include #include "avl.h" typedef struct _avl_int_data { avl_t avl; int num; } avl_int_data_t; static avl_tree_t tree; static avl_tree_t tree2; static int intcmp_cb(void *a, void *b) { return ((avl_int_data_t *)a)->num - ((avl_int_data_t *)b)->num; } /* * destroy_tree - remove all nodes from an AVL tree * @t: tree to empty */ static void destroy_tree(avl_tree_t *t) { avl_t *cur; while ((cur = t->root) != NULL) { avl_int_data_t *tmp; tmp = (avl_int_data_t *)avl_remove(t, cur); free(tmp); } } int append(int num) { avl_int_data_t *data = malloc(sizeof(avl_int_data_t)); if (data == NULL) return 0; data->num = num; avl_t *ret = avl_insert(&tree, (avl_t *)data); if (ret != (avl_t *)data) { free(data); return 1; } return 0; } int append2(int num) { avl_int_data_t *data = malloc(sizeof(avl_int_data_t)); if (!data) return 0; data->num = num; avl_t *ret = avl_insert(&tree2, (avl_t *)data); if (ret != (avl_t *)data) { free(data); return 1; } return 0; } int node_remove(int num) { avl_int_data_t node, *n; node.num = num; n = (avl_int_data_t *)avl_remove(&tree, (avl_t *)&node); if (n) { if (n->num != num) error(1, 0, "Remove wrong item %d looking for %d", n->num, num); else { free(n); return 0; } } else error(1, 0, "Remove didn't find %d", num); return 0; } static int count_cb(void *entry, void *data) { (void)entry; (void)data; return 1; } static void test_search(void) { avl_int_data_t *res; avl_int_data_t tmp; avl_init(&tree, intcmp_cb); append(10); append(20); append(15); tmp.num = 20; res = (avl_int_data_t *)avl_search(&tree, (avl_t *)&tmp); if (!res || res->num != 20) error(1, 0, "avl_search failed to find 20"); tmp.num = 99; res = (avl_int_data_t *)avl_search(&tree, (avl_t *)&tmp); if (res) error(1, 0, "avl_search incorrectly found 99"); destroy_tree(&tree); } static void test_duplicates(void) { int ret; avl_init(&tree, intcmp_cb); ret = append(5); if (ret != 0) error(1, 0, "append(5) failed"); ret = append(5); if (ret != 1) error(1, 0, "append(5) duplicate not detected"); int count = avl_traverse(&tree, count_cb, NULL); if (count != 1) error(1, 0, "duplicate insert created %d nodes (expected 1)", count); destroy_tree(&tree); } static void test_traverse_count(void) { avl_init(&tree, intcmp_cb); append(1); append(2); append(3); append(4); append(5); int count = avl_traverse(&tree, count_cb, NULL); if (count != 5) error(1, 0, "avl_traverse returned %d (expected 5)", count); destroy_tree(&tree); } static void test_intersection(void) { avl_init(&tree, intcmp_cb); avl_init(&tree2, intcmp_cb); append(1); append(2); append(3); append2(3); append2(4); append2(5); if (!avl_intersection(&tree, &tree2)) error(1, 0, "avl_intersection failed to detect common element"); destroy_tree(&tree2); avl_init(&tree2, intcmp_cb); if (avl_intersection(&tree, &tree2)) error(1, 0, "avl_intersection false positive on empty second tree"); destroy_tree(&tree); if (avl_intersection(&tree, &tree2)) error(1, 0, "avl_intersection false positive on two empty trees"); destroy_tree(&tree2); } static void test_iterator_null(void) { avl_init(&tree, intcmp_cb); if (avl_first(NULL, &tree) != NULL) error(1, 0, "avl_first(NULL,…) should return NULL"); if (avl_next(NULL) != NULL) error(1, 0, "avl_next(NULL) should return NULL"); destroy_tree(&tree); } /* https://stackoverflow.com/questions/3955680/how-to-check-if-my-avl-tree-implementation-is-correct */ int main(void) { avl_int_data_t *node; int i; avl_iterator k; avl_init(&tree, intcmp_cb); append(2); append(1); /* force a 1L rotation */ append(3); node = (avl_int_data_t *)tree.root; if (node->num != 2) error(1, 0, "tree not balanced 1"); /* pop the top off to force a rebalance */ node_remove(2); node = (avl_int_data_t *)tree.root; if (node->num != 3) error(1, 0, "tree not balanced 2"); node_remove(1); append(2); /* tree should be 3-2, then add a 1 to force a 1R rotation */ append(1); node = (avl_int_data_t *)tree.root; if (node->num != 2) error(1, 0, "tree not balanced 3"); node_remove(3); node_remove(2); append(3); /* tree should be 1-3, now force a 2L rotation */ append(2); node = (avl_int_data_t *)tree.root; if (node->num != 2) error(1, 0, "tree not balanced 4"); node_remove(1); node_remove(2); append(1); /* tree should be 3-1, now force a 2R rotation */ append(2); node = (avl_int_data_t *)tree.root; if (node->num != 2) error(1, 0, "tree not balanced 5"); node_remove(1); node_remove(2); node_remove(3); if (tree.root != NULL) error(1, 0, "root not NULL when tree should be empty 1"); /* Now let's test the iterator functions */ append(2); append(5); append(1); append(4); append(3); i = 1; for (node = (avl_int_data_t *)avl_first(&k, &tree); node; node = (avl_int_data_t *)avl_next(&k)) { if (node->num != i) error(1, 0, "Iteration expected %d, got %d", i, node->num); else printf("Iterator %d\n", node->num); i++; } node_remove(1); node_remove(2); node_remove(3); node_remove(4); node_remove(5); if (tree.root != NULL) error(1, 0, "root not NULL when tree should be empty 2"); test_search(); test_duplicates(); test_traverse_count(); test_intersection(); test_iterator_null(); destroy_tree(&tree); destroy_tree(&tree2); return 0; } fapolicyd-1.4.3/src/tests/deb_test.c000066400000000000000000000014341513023701500173560ustar00rootroot00000000000000#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; } fapolicyd-1.4.3/src/tests/elf_file_test.c000066400000000000000000000117161513023701500203750ustar00rootroot00000000000000/* * elf_file_test.c - verify gather_elf flag classification * * Each case writes a synthetic object into an anonymous descriptor created * by memfd_create (or an unlinked temporary file when memfd is unavailable), * then checks the returned flag bitmap. Coverage includes 32-bit/64-bit * executables, text and shebang scripts, truncated ELF headers, and an * oversized program header table. The expectations cover IS_ELF, HAS_LOAD, * HAS_ERROR, TEXT_SCRIPT, HAS_SHEBANG, and HAS_RWE_LOAD. */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include "file.h" #include "process.h" #ifndef MFD_CLOEXEC #define MFD_CLOEXEC 0 #endif static void build_ident(unsigned char ident[EI_NIDENT], unsigned char elf_class) { memset(ident, 0, EI_NIDENT); ident[EI_MAG0] = ELFMAG0; ident[EI_MAG1] = ELFMAG1; ident[EI_MAG2] = ELFMAG2; ident[EI_MAG3] = ELFMAG3; ident[EI_CLASS] = elf_class; ident[EI_DATA] = ELFDATA2LSB; ident[EI_VERSION] = EV_CURRENT; } static size_t make_elf32(unsigned char *buf, int with_load) { Elf32_Ehdr *eh = (Elf32_Ehdr *)buf; Elf32_Phdr *ph = (Elf32_Phdr *)(buf + sizeof(Elf32_Ehdr)); memset(buf, 0, sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr)); build_ident(eh->e_ident, ELFCLASS32); eh->e_type = ET_EXEC; eh->e_machine = EM_386; eh->e_version = EV_CURRENT; eh->e_entry = 0x8048000; eh->e_phoff = sizeof(Elf32_Ehdr); eh->e_ehsize = sizeof(Elf32_Ehdr); eh->e_phentsize = sizeof(Elf32_Phdr); eh->e_phnum = with_load ? 1 : 0; if (!with_load) return sizeof(Elf32_Ehdr); ph->p_type = PT_LOAD; ph->p_offset = 0; ph->p_vaddr = 0x8048000; ph->p_paddr = 0x8048000; ph->p_filesz = 0x1000; ph->p_memsz = 0x1000; ph->p_flags = PF_R | PF_X; ph->p_align = 0x1000; return sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr); } static size_t make_elf64(unsigned char *buf, unsigned int flags) { Elf64_Ehdr *eh = (Elf64_Ehdr *)buf; Elf64_Phdr *ph = (Elf64_Phdr *)(buf + sizeof(Elf64_Ehdr)); memset(buf, 0, sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr)); build_ident(eh->e_ident, ELFCLASS64); eh->e_type = ET_EXEC; eh->e_machine = EM_X86_64; eh->e_version = EV_CURRENT; eh->e_entry = 0x400000; eh->e_phoff = sizeof(Elf64_Ehdr); eh->e_ehsize = sizeof(Elf64_Ehdr); eh->e_phentsize = sizeof(Elf64_Phdr); eh->e_phnum = 1; ph->p_type = PT_LOAD; ph->p_offset = 0; ph->p_vaddr = 0x400000; ph->p_paddr = 0x400000; ph->p_filesz = 0x2000; ph->p_memsz = 0x2000; ph->p_flags = flags; ph->p_align = 0x200000; return sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr); } static size_t make_elf64_header_only(unsigned char *buf, unsigned short phnum) { Elf64_Ehdr *eh = (Elf64_Ehdr *)buf; memset(buf, 0, sizeof(Elf64_Ehdr)); build_ident(eh->e_ident, ELFCLASS64); eh->e_type = ET_EXEC; eh->e_machine = EM_X86_64; eh->e_version = EV_CURRENT; eh->e_entry = 0x400000; eh->e_phoff = sizeof(Elf64_Ehdr); eh->e_ehsize = sizeof(Elf64_Ehdr); eh->e_phentsize = sizeof(Elf64_Phdr); eh->e_phnum = phnum; return sizeof(Elf64_Ehdr); } static size_t make_truncated32(unsigned char *buf) { memset(buf, 0, EI_NIDENT + 4); build_ident(buf, ELFCLASS32); return EI_NIDENT + 4; } static int fd_from_buffer(const char *name, const void *buf, size_t len) { int fd = memfd_create(name, MFD_CLOEXEC); if (fd < 0) { char path[] = "/tmp/fapolicyd-elftest-XXXXXX"; fd = mkstemp(path); if (fd < 0) return -1; unlink(path); } if (write(fd, buf, len) != (ssize_t)len) { int saved = errno; close(fd); errno = saved; return -1; } if (lseek(fd, 0, SEEK_SET) < 0) { int saved = errno; close(fd); errno = saved; return -1; } return fd; } static void expect_flags(const char *label, const void *buf, size_t len, uint32_t expect) { int fd = fd_from_buffer(label, buf, len); if (fd < 0) error(1, errno, "%s: unable to obtain descriptor", label); uint32_t got = gather_elf(fd, (off_t)len); close(fd); if (got != expect) error(1, 0, "%s: expected 0x%x got 0x%x", label, expect, got); } int main(void) { unsigned char buf[sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr)]; unsigned char shebang[] = "#!/bin/sh\nexit 0\n"; unsigned char text_script[] = "echo hello world\n"; unsigned char trunc_buf[EI_NIDENT + 4]; size_t sz32 = make_elf32(buf, 1); expect_flags("elf32-load", buf, sz32, IS_ELF | HAS_EXEC | HAS_LOAD); size_t sz64 = make_elf64(buf, PF_R | PF_W | PF_X); expect_flags("elf64-rwe", buf, sz64, IS_ELF | HAS_EXEC | HAS_LOAD | HAS_RWE_LOAD); size_t shebang_len = sizeof(shebang) - 1; expect_flags("shebang", shebang, shebang_len, HAS_SHEBANG); size_t text_len = sizeof(text_script) - 1; expect_flags("text-script", text_script, text_len, TEXT_SCRIPT); size_t bad32 = make_truncated32(trunc_buf); expect_flags("truncated32", trunc_buf, bad32, IS_ELF | HAS_ERROR); size_t head64 = make_elf64_header_only(buf, 4); expect_flags("oversized-ph", buf, head64, IS_ELF | HAS_EXEC | HAS_ERROR); return 0; } fapolicyd-1.4.3/src/tests/escape_test.c000066400000000000000000000046151513023701500200700ustar00rootroot00000000000000/* * 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; } fapolicyd-1.4.3/src/tests/event_test.c000066400000000000000000000440531513023701500177510ustar00rootroot00000000000000/* * event_test.c - unit tests for new_event subject/object cache behavior */ #include #include #include #include #include #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 rules_get_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" }, }; /* --- 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->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 rules_get_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; (void)fields; if (info == NULL) return -1; info->ppid = -1; 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 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, &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, SRC_RPM, 8192UL, 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, SRC_FILE_DB, 4096UL, 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, SRC_FILE_DB, 1024UL, 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); } /* * 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 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 }; o_array *cached_object; 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"); cached_object = first.o; CHECK(cached_object != 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(second.o != cached_object, 36, "[ERROR:36] object cache not flushed"); 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_reopen_skip_path(); if (rc) return rc; rc = test_subject_eviction(); if (rc) return rc; rc = test_needs_flush_resets_object_cache(); if (rc) return rc; return 0; } fapolicyd-1.4.3/src/tests/fd_fgets_test.c000066400000000000000000000164771513023701500204220ustar00rootroot00000000000000#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]; char custom[32]; fd_fgets_state_t *st; assert(pipe(fds) == 0); st = fd_fgets_init(); assert(st); assert(fd_setvbuf_r(st, custom, sizeof(custom), MEM_SELF_MANAGED) == 0); /* 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[33]; const char *line = "0123456789abcdef0123456789abcdefQRSTUVWX\n"; fd_fgets_state_t *st; size_t line_len = strlen(line); size_t capacity = sizeof(custom) - 1; assert(pipe(fds) == 0); st = fd_fgets_init(); assert(st); assert(fd_setvbuf_r(st, custom, capacity, MEM_SELF_MANAGED) == 0); write_all(fds[1], line); close(fds[1]); int len = fd_fgets_r(st, buf, sizeof(buf), fds[0]); assert(len == (int)capacity); 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)); assert(strcmp(buf, line + capacity) == 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); } /* * 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. */ 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_mmap_file_readme(); printf("fd-fgets_r tests: all passed\n"); return 0; } fapolicyd-1.4.3/src/tests/file_filter_test.c000066400000000000000000000035671513023701500211210ustar00rootroot00000000000000/* * file_filter_test.c - ensure filter_prune_list handles basic lists */ #include "filter.h" #include "llist.h" #include #include #include #define FILTER_CONF TEST_BASE "/src/tests/fixtures/filter-minimal.conf" static int has_path(list_t *list, const char *path) { for (list_item_t *lptr = list->first; lptr; lptr = lptr->next) { if (strcmp(lptr->index, path) == 0) return 1; } return 0; } int main(void) { list_t list; list_init(&list); if (list_append(&list, strdup("/usr/bin/allowed"), NULL) || list_append(&list, strdup("/usr/bin/skipped"), NULL) || list_append(&list, strdup("/var/log/public/info"), NULL) || list_append(&list, strdup("/var/log/blocked.log"), NULL) || list_append(&list, strdup("/usr/share/example.tmp"), NULL)) { fprintf(stderr, "[ERROR:1] unable to build list\n"); list_empty(&list); return 1; } if (filter_prune_list(&list, FILTER_CONF)) { fprintf(stderr, "[ERROR:2] filter_prune_list failed\n"); list_empty(&list); return 2; } if (list.count != 2) { fprintf(stderr, "[ERROR:3] expected 2 entries, got %ld\n", list.count); list_empty(&list); return 3; } if (!has_path(&list, "/usr/bin/allowed")) { fprintf(stderr, "[ERROR:4] allowed binary missing\n"); list_empty(&list); return 4; } if (!has_path(&list, "/var/log/public/info")) { fprintf(stderr, "[ERROR:5] allowed log entry missing\n"); list_empty(&list); return 5; } list_empty(&list); return 0; } fapolicyd-1.4.3/src/tests/file_type_detect_test.c000066400000000000000000000061261513023701500221370ustar00rootroot00000000000000/* * file_type_detect_test.c - verify quick file type helpers */ #define _GNU_SOURCE #include #include #include #include #include #include "file.h" 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)"); } int main(void) { const unsigned char png_hdr[] = { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' }; const unsigned char jpg_hdr[] = { 0xFF, 0xD8, 0xFF, 0xE0 }; const unsigned char gzip_hdr[] = { 0x1F, 0x8B, 0x08, 0x00 }; expect_extract("bash", "#!/bin/bash\n", "bash"); expect_extract("env-python", "#! /usr/bin/env -S python3 -u\n", "python3"); 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); return 0; } fapolicyd-1.4.3/src/tests/filter_test.c000066400000000000000000000120061513023701500201060ustar00rootroot00000000000000/* * filter_test.c - comprehensive tests for filter configuration */ #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; } 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; struct timespec s, e; clock_gettime(CLOCK_MONOTONIC, &s); for (int i = 0; i < 1000; i++) { if (filter_init()) { fprintf(stderr, "[ERROR:2] filter_init failed\n"); return 2; } if (filter_load_file(PROD_CONF)) { fprintf(stderr, "[ERROR:3] loading a valid fixture failed\n"); filter_destroy(); return 3; } filter_destroy(); } clock_gettime(CLOCK_MONOTONIC, &e); long avg = ((e.tv_sec - s.tv_sec) * 1000000000L + (e.tv_nsec - s.tv_nsec)) / 1000; // The point of this test is to spot something wrong in the // parser that might loop way too long. Calling it a warning // since build systems vary in speed. if (avg > 2 * BASE_NS) { fprintf(stderr, "[WARNING:4] prod parse %ldns exceeds %dns\n", avg, 2 * BASE_NS); } return 0; } fapolicyd-1.4.3/src/tests/fixtures/000077500000000000000000000000001513023701500172705ustar00rootroot00000000000000fapolicyd-1.4.3/src/tests/fixtures/broken-filter.conf000066400000000000000000000000721513023701500227010ustar00rootroot00000000000000# malformed filter + / - usr/ usr/share/ + usr/dir#name fapolicyd-1.4.3/src/tests/fixtures/filter-cases.txt000066400000000000000000000035231513023701500224150ustar00rootroot00000000000000minimal /usr/include/stdio.h 0 minimal /usr/share/doc.txt 1 minimal /usr/share/cache.tmp 0 minimal /usr/share/script.py 1 minimal /usr/src/kernel123/driver.c 0 minimal /usr/src/kernel123/scripts/build 0 minimal /etc/hosts 1 minimal /var/log/messages 0 minimal /var/log/public/info.log 1 minimal ../foo 0 minimal foo/.. 0 prod ../foo 0 prod foo/.. 0 prod /usr/share/../include/stdio.h 0 prod /usr/includee/stdio.h 1 prod /usr/share/doc.txt 0 prod /usr/share/script.py 1 prod /usr/share/byte.pyc 1 prod /usr/share/byte.pyzz 0 prod /usr/share/space\ file.py 1 prod /usr/share/space\ file.txt 0 prod /usr/share/app/libexec/ 1 prod /usr/share/app/libexecx/tool 0 prod /usr/share/test.rb 1 prod /usr/share/test.rbx 0 prod /usr/share/test.pl 1 prod /usr/share/test.plx 0 prod /usr/share/test.stp 1 prod /usr/share/test.stpx 0 prod /usr//share/test.js 1 prod /usr//share/test.jsx 0 prod /usr/share/test.jar 1 prod /usr/share/test.jarx 0 prod /usr/share/test.m4 1 prod /usr/share/test.m4x 0 prod /usr/share/test.php 1 prod /usr/share/test.phpx 0 prod /usr/share/test.pm 1 prod /usr/share/test.pmx 0 prod /usr/share/手稿.lua 1 prod /usr/share/手稿.luax 0 prod /usr/share/Test.class 1 prod /usr/share/Test.classx 0 prod /usr/share/test.ts 1 prod /usr/share/test.tss 0 prod /usr/share/test.tsx 1 prod /usr/share/test.tsxx 0 prod /usr/share/test.el 1 prod /usr/share/test.elx 0 prod /usr/share/test.elc 1 prod /usr/share/test.elcx 0 prod /usr/src/kernel123/driver.c 0 prod /usr/src/kernel123/scripts/build 1 prod /usr/src/kernel123/scriptz/build 0 prod /usr/src/kernel123/tools/objtool/run 1 prod /usr/src/kernel123/tools/objtool2/run 0 prod /usr/src/kernels/6.17.8-200.fc42.x86_64/arch/x86/kernel/ptrace.c 0 prod /usr/src/kernels/6.17.8-200.fc42.x86_64/scripts/gendwarfksyms/examples/kabi.h 1 prod /usr/src/kernels/6.17.6-200.fc42.x86_64/include/linux/memory.h 0 prod /usr//bin/ls 1 fapolicyd-1.4.3/src/tests/fixtures/filter-minimal.conf000066400000000000000000000001601513023701500230450ustar00rootroot00000000000000# test filter configuration + / - usr/ + bin/allowed + share/ - *.tmp + *.py - var/log/ + public/ fapolicyd-1.4.3/src/tests/fixtures/rules-valid.rules000066400000000000000000000002441513023701500225730ustar00rootroot00000000000000%good=1000,1001 %paths=/bin/ls,/usr/bin/id allow perm=any auid=1000 : path=/bin/ls allow perm=any auid=%good : path=/bin/ls allow perm=any auid=%good : path=%paths fapolicyd-1.4.3/src/tests/gid_proc_test.c000066400000000000000000000067171513023701500204230ustar00rootroot00000000000000#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 (!check_int_attr_set(set, (int64_t)gid)) error(1, 0, "%s group %u not found", label, gid); } /* * 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; 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 = check_int_attr_set(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 (!check_int_attr_set(groups, (int64_t)missing_gid)) break; } if (missing_gid == UINT_MAX) error(1, 0, "Unable to determine missing group for test"); res = check_int_attr_set(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 = init_standalone_set(UNSIGNED); append_int_attr_set(g, (int64_t)missing_gid); append_int_attr_set(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"); append_int_attr_set(g, (int64_t)gid); res = avl_intersection(&(g->tree), &(groups->tree)); if (!res) error(1, 0, "Positive AVL intersection failed"); destroy_attr_set(g); free(g); } destroy_attr_set(groups); free(groups); return 0; } fapolicyd-1.4.3/src/tests/lru_test.c000066400000000000000000000040161513023701500174250ustar00rootroot00000000000000#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_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); } int main(void) { test_reuse_after_evict(); test_pool_exhaustion(); return 0; } fapolicyd-1.4.3/src/tests/rules_test.c000066400000000000000000000134341513023701500177610ustar00rootroot00000000000000/* * 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 and type mismatches * * 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" } }; /* * 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"); subject_create(e->s); object_create(e->o); 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; } /* * 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 (init_attr_sets()) error(1, 0, "init_attr_sets failed"); rules_create(&l); load_fixture(VALID_RULES, &l); rules_regen_sets(&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); destroy_attr_sets(); /* negative parsing scenarios */ for (i = 0; i < sizeof(errors)/sizeof(errors[0]); i++) { const struct err_case *c = &errors[i]; if (init_attr_sets()) error(1, 0, "init_attr_sets failed"); rules_create(&l); 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); destroy_attr_sets(); } return 0; } fapolicyd-1.4.3/src/tests/test-stubs.c000066400000000000000000000005421513023701500177010ustar00rootroot00000000000000/* * 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; unsigned int debug_mode; conf_t config; fapolicyd-1.4.3/src/tests/trustdb_format_test.c000066400000000000000000000013711513023701500216630ustar00rootroot00000000000000// 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, &tsource, &size, parsed_digest) != 3) return 1; if (strcmp(digest, parsed_digest)) return 1; return 0; } fapolicyd-1.4.3/src/tests/uid_proc_test.c000066400000000000000000000037551513023701500204400ustar00rootroot00000000000000#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 (!check_int_attr_set(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"); destroy_attr_set(uids); free(uids); return 0; }