pax_global_header00006660000000000000000000000064152417402330014513gustar00rootroot0000000000000052 comment=9ae44402f4c405f08b9dd5ec070127d0d92afee4 stevegrubb-libcap-ng-9ae4440/000077500000000000000000000000001524174023300160515ustar00rootroot00000000000000stevegrubb-libcap-ng-9ae4440/.gitignore000066400000000000000000000015331524174023300200430ustar00rootroot00000000000000*~ *.[oa] *.lo *.la Makefile Makefile.in NEWS aclocal.m4 bindings/Makefile bindings/Makefile.in bindings/python3/.deps/ bindings/python3/Makefile bindings/python3/Makefile.in bindings/python3/test/Makefile bindings/python3/test/Makefile.in bindings/src/Makefile bindings/src/Makefile.in bindings/test/Makefile bindings/test/Makefile.in compile config.guess config.h config.h.in config.log config.status config.sub configure depcomp docs/Makefile docs/Makefile.in install-sh libtool ltmain.sh m4/Makefile m4/Makefile.in m4/libtool.m4 m4/ltoptions.m4 m4/ltsugar.m4 m4/ltversion.m4 m4/lt~obsolete.m4 missing py-compile src/.deps/ src/Makefile src/Makefile.in src/libcap-ng.pc src/test/.deps/ src/test/Makefile src/test/Makefile.in stamp-h1 test-driver utils/Makefile utils/Makefile.in utils/cap-audit/.deps/ utils/cap-audit/Makefile utils/cap-audit/Makefile.in stevegrubb-libcap-ng-9ae4440/AGENTS.md000066400000000000000000000037461524174023300173660ustar00rootroot00000000000000# Repository Guidelines This project contains a library aimed at manipulating posix capabilities on the Linux operating system. 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 libcap-ng ./autogen.sh ./configure --with-python3 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 libcap-ng is located - `/utils`: This holds the code for pscap, netcap, and filecap - `/docs`: This holds all of the man pages - `/bindings`: This holds swig based python bindings for libcap-ng ## 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. ## Commit Messages - Use a concise one-line summary followed by a blank line and additional details if needed (similar to existing commits). ## Summary - Build with `autogen.sh`, `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. These guidelines should help future contributors and automated tools work consistently within the libcap-ng repository. stevegrubb-libcap-ng-9ae4440/AUTHORS000066400000000000000000000001031524174023300171130ustar00rootroot00000000000000This library has been written by: Steve Grubb stevegrubb-libcap-ng-9ae4440/COPYING000066400000000000000000000431101524174023300171030ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) 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 this service 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 make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. 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. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the 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 a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 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. 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 convey 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 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 Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision 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, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This 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 Library General Public License instead of this License. stevegrubb-libcap-ng-9ae4440/COPYING.LIB000066400000000000000000000636561524174023300175310ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, 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 this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. ^L Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. ^L GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. ^L Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. ^L 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. ^L 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. ^L 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser 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 Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. ^L 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS ^L How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey 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 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 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! stevegrubb-libcap-ng-9ae4440/ChangeLog000066400000000000000000000202441524174023300176250ustar00rootroot000000000000000.9.5 - Fix broken utility_logic_test when run as a 32 bit app - Install the cap-audit man page conditionally - Fix V3 file capability usage - Preserve existing securebits in capng_lock - Make python configuration a little more strict 0.9.4 - Add bluetooth collection to netcap --advanced - Don't drop netcap --advanced if only VSOCK kernel headers are missing - In cap-audit, verify executable is being passed - Add a systemd simulator to cap-audit: use --service to test a service file - Improve cap-audit recommendations by correlating syscall outcomes - Allow supplying vmlinux.h for reproducible cap-audit builds - Restore --with-python/--without-python as generic Python configure options - Fix on-demand bash completion loading for the utilities 0.9.3 - In cap-audit, split capability analysis across init and runtime phases - If vm_sockets.h and others are not available, remove "netcap --advanced" - Add netcap --list-interfaces & --interface to restrict output to 1 interface - Put bash completions in /usr/share/bash-completion/completions/ - capng_change_id now detects it added setpcap and drops only if it added it - Add capng_stage_additional_groups and its support in capng_change_id - Add CAPNG_APPLY_BOUNDING flag to capng_change_id to direct it to apply changes - filecap: add path-to-fd consistency check in capability write path 0.9.2 - Added netcap --advanced option for attack surface discovery and inventory - Added acct name to pscap --tree output - Code cleanups and deep review of all functions for correctness - Add colorized output to netcap --advanced - Improve correctness of cap-audit captures - Update man pages 0.9.1 - Deprecate captest - In cap-audit, if tested app uses file system based capabilities, drop setpcap - In cap-audit, fully resolve paths before classifying - In cap-audit, add JSON escaping to output - In cap-audit, filter pre-exec, startup, and shutdown capability noise - pscap now has a --tree disply mode - More code cleanups - Improve output alignment of various utilities (Miroslav Koškár) 0.9 - Fix python path when invoking py-compile (Jan Palus) - Drop python2 bindings (Rudi Heitbaum) - Optimize capability name translation lookups - Add cap-audit utility 0.8.5 - Remove python global exception handler since it's deprecated - Make the utilities link against just built libraries - Remove unused macro in cap-ng.h 0.8.4 - In capng_change_id, clear PR_SET_KEEPCAPS if returning an error - pscap: add -p option for reporting a specified process (Masatake Yamato) - Annotate function prototypes to warn if results are unused - Drop python2 support 0.8.3 - Fix parameters to capng_updatev python bindings to be signed - Detect capability options at runtime to make containerization easier (ntkme) - Initialize the library when linked statically - Add gcc function attributes for deallocation 0.8.2 - In capng_apply, if we blew up in bounding set, allow setting capabilities - If PR_CAP_AMBIENT is not available, do not build libdrop_ambient - Improve last_cap check 0.8.1 - If procfs is not available, leave last_cap as CAP_LAST_CAP - If bounding and ambient not found in status, try prctl method - In capng_apply, move ambient caps to the end of the transaction - In capng_apply, return errors more aggressively. - In capng_apply, if the action includes the bounding set,resync with the kernel - Fix signed/unsigned warning in cap-ng.c - In capng_apply, return a unique error code to diagnose any failure - In capng_have_capability, return 0 for failure - Add the libdrop_ambient admin tool 0.8 - Add vararg support to python bindings for capng_updatev - Add support for ambient capabilities - Add support for V3 filesystem capabilities 0.7.11 - Really clear bounding set if asked in capng_change_id - Add CAP_PERFMON, CAP_BPF, & CAP_CHECKPOINT_RESTORE - Avoid malloc/free in capng_apply (Natanael Copa) - If procfs is not available, get bounding set via prctl - Cleanup some compiler warnings 0.7.10 - Update capng_change_id man page - Add capng_have_permitted_capabilities function - Update filecap to output which set the capabilities are in - Fix filecap to not output an error when a file has no capabilities - Add udplite support to netcap - Fix usage of pthread_atfork (Joe Orton) - Mark processes in child user namespaces with * (Danila Kiver) 0.7.9 - Fix byte compiling python3 bindings - Detect and output a couple errors in filecap - Use pthread_atfork to optionally reset the pid and related info on fork - Rework spec file to show new python2/3 separation 0.7.8 - Improve Python3 support - Fix the thread separation test - Correct typo in cap_pacct text - Update man page for captest - Fix sscanf string lengths in netcap - Correct linking of python3 module 0.7.7 - Make sure all types used in _lnode are defined in proc-llist.h - Fix python binding test for old kernels - Fix leaked FD in library init 0.7.6 - Fix python3 support 0.7.5 - Make python3 supported - In python bindings test, clamp CAP_LAST_CAP with /proc/.../cap_last_cap - Update table for 3.16 kernel 0.7.4 - In pscap, remove unused code - Add CAPNG_INIT_SUPP_GRP to capng_change_id - Drop CAP_COMPROMISE_KERNEL - Update the autotools components - Dynamically detect last capability (#895105) - Add PR_SET_NO_NEW_PRIVS to capng_lock if kernel supports it 0.7.3 - Make sure stderr is used consistently in utils - Fix logic causing file based capabilities to not be supported when it should 0.7.1 - Add CAP_COMPROMISE_KERNEL - Define FTW_CONTINUE in case its not defined in libc - Use glibc for xattr.h if available 0.7 - Make file opens use the cloexec flag (Cristian Rodríguez) - Add CAP_BLOCK_SUSPEND - Fix possible segfaults when CAP_LAST_CAP is larger than the lookup table - In pscap, don't drop capabilities when running with capabilities 0.6.6 - In netcap, make sure readlink is handled properly - Add CAP_SYSLOG - In netcap and pscap, ensure euid is initialized - Add CAP_WAKE_ALARM 0.6.5 - Fix self test build problem on clean system (Sterling X. Winter) - Only open regular files in filecap - Make building Python bindings optional - Python bindings update (arfrever.fta) - Fix filecap segfault when checking a specific file - Add define for missing XATTR_NAME_CAPS since 2.6.36 makes it private 0.6.4 - Update packet socket code to print interface - Fix effective capabilities read from file descriptor - Use thread ID for capget/set calls 0.6.3 - In netcap and pscap use the effective uid - In capng_change_id, only retain setpcap if clearing the bounding set 0.6.2 - Make pscap drop capabilities so its not listed in report - Review prctl calls to make sure we are passing 5 args - Add package config support 0.6.1 - In netcap, don't complain about missing udp or raw network files - Adjusted data read in for file based capabilities 0.6 - In netcap, don't complain about missing network files - Add python bindings - Add m4 macro file to help developers configure libcap-ng in their apps - Fake applying bounding set for old OS - Ignore setpcap for old OS when changing id - Remove capabilities v1 data handling from reading file attributes - Set the SECURE_NO_SETUID_FIXUP and LOCKED securebits flags in capng_lock 0.5.1 - Remove unnecessary uid check in change_uid when dropping supplemental groups - Add credential printout and other improvements to captest - In the init routine, set hdr.pid to current process - Use bit mask on effective capabilities check in have_capabilities - Numeric printing of bounding set bits were in wrong order - In update function, reverse the order of bounding set vs capabilities - Revise the tests used to determine if bounding set should be updated 0.5 - If attr/xattr.h is not available disable file system capabilities - Initialize capng_have_capability with capng_get_caps_process if unknown - Make capng_change_id drop the gid if given - Fixed cap_update for bounding set - Fix have_capability for bounding set - Added more tests to the make check target - Remove CAPNG_LOCK_PERMS for change_id flags - Added captest program 0.4.2 - Fix missing includes for various OS and platforms - Correct misplaced #ifdef for older OS - Reorder clearing of bounding set in capng_change_id - Make locking a noop in capng_change_id for the moment 0.4.1 - spec file clean ups - Man pages for all library functions 0.4 - Initial public release stevegrubb-libcap-ng-9ae4440/INSTALL000066400000000000000000000377711524174023300171210ustar00rootroot00000000000000Installation Instructions ************************* Basic Installation ================== The following shell commands: test -f configure || ./bootstrap ./configure make make install should configure, build, and install this package. The first line, which bootstraps, is intended for developers; when building from distribution tarballs it does nothing and can be skipped. The following more-detailed instructions are generic; see the ‘README’ file for instructions specific to this package. Some packages provide this ‘INSTALL’ file but do not implement all of the features documented below. The lack of an optional feature in a given package is not necessarily a bug. More recommendations for GNU packages can be found in the GNU Coding Standards. Many packages have scripts meant for developers instead of ordinary builders, as they may use developer tools that are less commonly installed, or they may access the network, which has privacy implications. If the ‘bootstrap’ shell script exists, it attempts to build the ‘configure’ shell script and related files, possibly using developer tools or the network. Because the output of ‘bootstrap’ is system-independent, it is normally run by a package developer so that its output can be put into the distribution tarball and ordinary builders and users need not run ‘bootstrap’. Some packages have commands like ‘./autopull.sh’ and ‘./autogen.sh’ that you can run instead of ‘./bootstrap’, for more fine-grained control over bootstrapping. The ‘configure’ shell script attempts to guess correct values for various system-dependent variables used during compilation. It uses those values to create a ‘Makefile’ in each directory of the package. It may also create one or more ‘.h’ files containing system-dependent definitions. Finally, it creates a shell script ‘config.status’ that you can run in the future to recreate the current configuration, and a file ‘config.log’ containing output useful for debugging ‘configure’. It can also use an optional file (typically called ‘config.cache’ and enabled with ‘--cache-file=config.cache’ or simply ‘-C’) that saves the results of its tests to speed up reconfiguring. Caching is disabled by default to prevent problems with accidental use of stale cache files. If you need to do unusual things to compile the package, please try to figure out how ‘configure’ could check whether to do them, and mail diffs or instructions to the address given in the ‘README’ so they can be considered for the next release. If you are using the cache, and at some point ‘config.cache’ contains results you don’t want to keep, you may remove or edit it. The ‘autoconf’ program generates ‘configure’ from the file ‘configure.ac’. Normally you should edit ‘configure.ac’ instead of editing ‘configure’ directly. The simplest way to compile this package is: 1. ‘cd’ to the directory containing the package’s source code. 2. If this is a developer checkout and file ‘configure’ does not yet exist, type ‘./bootstrap’ to create it. You may need special developer tools and network access to bootstrap, and the network access may have privacy implications. 3. Type ‘./configure’ to configure the package for your system. This might take a while. While running, ‘configure’ prints messages telling which features it is checking for. 4. Type ‘make’ to compile the package. 5. Optionally, type ‘make check’ to run any self-tests that come with the package, generally using the just-built uninstalled binaries. 6. Type ‘make install’ to install the programs and any data files and documentation. When installing into a prefix owned by root, it is recommended that the package be configured and built as a regular user, and only the ‘make install’ phase executed with root privileges. 7. Optionally, type ‘make installcheck’ to repeat any self-tests, but this time using the binaries in their final installed location. This target does not install anything. Running this target as a regular user, particularly if the prior ‘make install’ required root privileges, verifies that the installation completed correctly. 8. You can remove the program binaries and object files from the source code directory by typing ‘make clean’. To also remove the files that ‘configure’ created (so you can compile the package for a different kind of computer), type ‘make distclean’. There is also a ‘make maintainer-clean’ target, but that is intended mainly for the package’s developers. If you use it, you may have to bootstrap again. 9. If the package follows the GNU Coding Standards, you can type ‘make uninstall’ to remove the installed files. Compilers and Options ===================== Some systems require unusual options for compilation or linking that the ‘configure’ script does not know about. Run ‘./configure --help’ for details on some of the pertinent environment variables. You can give ‘configure’ initial values for configuration parameters by setting variables in the command line or in the environment. Here is an example: ./configure CC=gcc CFLAGS=-g LIBS=-lposix See “Defining Variables” for more details. Compiling For Multiple Architectures ==================================== You can compile the package for more than one kind of computer at the same time, by placing the object files for each system in their own directory. To do this, you can use GNU ‘make’. ‘cd’ to the directory where you want the object files and executables to go and run the ‘configure’ script. ‘configure’ automatically checks for the source code in the directory that ‘configure’ is in and in ‘..’. This is known as a “VPATH” build. With a non-GNU ‘make’, it is safer to compile the package for one system at a time in the source code directory. After you have installed the package for one system, use ‘make distclean’ before reconfiguring for another system. Some platforms, notably macOS, support “fat” or “universal” binaries, where a single binary can execute on different architectures. On these platforms you can configure and compile just once, with options specific to that platform. Installation Names ================== By default, ‘make install’ installs the package’s commands under ‘/usr/local/bin’, include files under ‘/usr/local/include’, etc. You can specify an installation prefix other than ‘/usr/local’ by giving ‘configure’ the option ‘--prefix=PREFIX’, where PREFIX must be an absolute file name. You can specify separate installation prefixes for architecture-specific files and architecture-independent files. If you pass the option ‘--exec-prefix=PREFIX’ to ‘configure’, the package uses PREFIX as the prefix for installing programs and libraries. Documentation and other data files still use the regular prefix. In addition, if you use an unusual directory layout you can give options like ‘--bindir=DIR’ to specify different values for particular kinds of files. Run ‘configure --help’ for a list of the directories you can set and what kinds of files go in them. In general, the default for these options is expressed in terms of ‘${prefix}’, so that specifying just ‘--prefix’ will affect all of the other directory specifications that were not explicitly provided. The most portable way to affect installation locations is to pass the correct locations to ‘configure’; however, many packages provide one or both of the following shortcuts of passing variable assignments to the ‘make install’ command line to change installation locations without having to reconfigure or recompile. The first method involves providing an override variable for each affected directory. For example, ‘make install prefix=/alternate/directory’ will choose an alternate location for all directory configuration variables that were expressed in terms of ‘${prefix}’. Any directories that were specified during ‘configure’, but not in terms of ‘${prefix}’, must each be overridden at install time for the entire installation to be relocated. The approach of makefile variable overrides for each directory variable is required by the GNU Coding Standards, and ideally causes no recompilation. However, some platforms have known limitations with the semantics of shared libraries that end up requiring recompilation when using this method, particularly noticeable in packages that use GNU Libtool. The second method involves providing the ‘DESTDIR’ variable. For example, ‘make install DESTDIR=/alternate/directory’ will prepend ‘/alternate/directory’ before all installation names. The approach of ‘DESTDIR’ overrides is not required by the GNU Coding Standards, and does not work on platforms that have drive letters. On the other hand, it does better at avoiding recompilation issues, and works well even when some directory options were not specified in terms of ‘${prefix}’ at ‘configure’ time. Optional Features ================= If the package supports it, you can cause programs to be installed with an extra prefix or suffix on their names by giving ‘configure’ the option ‘--program-prefix=PREFIX’ or ‘--program-suffix=SUFFIX’. Some packages pay attention to ‘--enable-FEATURE’ and ‘--disable-FEATURE’ options to ‘configure’, where FEATURE indicates an optional part of the package. They may also pay attention to ‘--with-PACKAGE’ and ‘--without-PACKAGE’ options, where PACKAGE is something like ‘gnu-ld’. ‘./configure --help’ should mention the ‘--enable-...’ and ‘--with-...’ options that the package recognizes. Some packages offer the ability to configure how verbose the execution of ‘make’ will be. For these packages, running ‘./configure --enable-silent-rules’ sets the default to minimal output, which can be overridden with ‘make V=1’; while running ‘./configure --disable-silent-rules’ sets the default to verbose, which can be overridden with ‘make V=0’. Specifying a System Type ======================== By default ‘configure’ builds for the current system. To create binaries that can run on a different system type, specify a ‘--host=TYPE’ option along with compiler variables that specify how to generate object code for TYPE. For example, to create binaries intended to run on a 64-bit ARM processor: ./configure --host=aarch64-linux-gnu \ CC=aarch64-linux-gnu-gcc \ CXX=aarch64-linux-gnu-g++ If done on a machine that can execute these binaries (e.g., via ‘qemu-aarch64’, ‘$QEMU_LD_PREFIX’, and Linux’s ‘binfmt_misc’ capability), the build behaves like a native build. Otherwise it is a cross-build: ‘configure’ will make cross-compilation guesses instead of running test programs, and ‘make check’ will not work. A system type can either be a short name like ‘mingw64’, or a canonical name like ‘x86_64-pc-linux-gnu’. Canonical names have the form CPU-COMPANY-SYSTEM where SYSTEM is either OS or KERNEL-OS. To canonicalize and validate a system type, you can run the command ‘config.sub’, which is often squirreled away in a subdirectory like ‘build-aux’. For example: $ build-aux/config.sub arm64-linux aarch64-unknown-linux-gnu $ build-aux/config.sub riscv-lnx Invalid configuration 'riscv-lnx': OS 'lnx' not recognized You can look at the ‘config.sub’ file to see which types are recognized. If the file is absent, this package does not need the system type. If ‘configure’ fails with the diagnostic “cannot guess build type”. ‘config.sub’ did not recognize your system’s type. In this case, first fetch the newest versions of these files from the GNU config package (https://savannah.gnu.org/projects/config). If that fixes things, please report it to the maintainers of the package containing ‘configure’. Otherwise, you can try the configure option ‘--build=TYPE’ where TYPE comes close to your system type; also, please report the problem to . For more details about configuring system types, see the Autoconf documentation. Sharing Defaults ================ If you want to set default values for ‘configure’ scripts to share, you can create a site shell script called ‘config.site’ that gives default values for variables like ‘CC’, ‘cache_file’, and ‘prefix’. ‘configure’ looks for ‘PREFIX/share/config.site’ if it exists, then ‘PREFIX/etc/config.site’ if it exists. Or, you can set the ‘CONFIG_SITE’ environment variable to the location of the site script. A warning: not all ‘configure’ scripts look for a site script. Defining Variables ================== Variables not defined in a site shell script can be set in the environment passed to ‘configure’. However, some packages may run configure again during the build, and the customized values of these variables may be lost. In order to avoid this problem, you should set them in the ‘configure’ command line, using ‘VAR=value’. For example: ./configure CC=/usr/local2/bin/gcc causes the specified ‘gcc’ to be used as the C compiler (unless it is overridden in the site shell script). Unfortunately, this technique does not work for ‘CONFIG_SHELL’ due to an Autoconf limitation. Until the limitation is lifted, you can use this workaround: CONFIG_SHELL=/bin/bash ./configure CONFIG_SHELL=/bin/bash ‘configure’ Invocation ====================== ‘configure’ recognizes the following options to control how it operates. ‘--help’ ‘-h’ Print a summary of all of the options to ‘configure’, and exit. ‘--help=short’ ‘--help=recursive’ Print a summary of the options unique to this package’s ‘configure’, and exit. The ‘short’ variant lists options used only in the top level, while the ‘recursive’ variant lists options also present in any nested packages. ‘--version’ ‘-V’ Print the version of Autoconf used to generate the ‘configure’ script, and exit. ‘--cache-file=FILE’ Enable the cache: use and save the results of the tests in FILE, traditionally ‘config.cache’. FILE defaults to ‘/dev/null’ to disable caching. ‘--config-cache’ ‘-C’ Alias for ‘--cache-file=config.cache’. ‘--srcdir=DIR’ Look for the package’s source code in directory DIR. Usually ‘configure’ can determine that directory automatically. ‘--prefix=DIR’ Use DIR as the installation prefix. See “Installation Names” for more details, including other options available for fine-tuning the installation locations. ‘--host=TYPE’ Build binaries for system TYPE. See “Specifying a System Type”. ‘--enable-FEATURE’ ‘--disable-FEATURE’ Enable or disable the optional FEATURE. See “Optional Features”. ‘--with-PACKAGE’ ‘--without-PACKAGE’ Use or omit PACKAGE when building. See “Optional Features”. ‘--quiet’ ‘--silent’ ‘-q’ Do not print messages saying which checks are being made. To suppress all normal output, redirect it to ‘/dev/null’ (any error messages will still be shown). ‘--no-create’ ‘-n’ Run the configure checks, but stop before creating any output files. ‘configure’ also recognizes several environment variables, and accepts some other, less widely useful, options. Run ‘configure --help’ for more details. Copyright notice ================ Copyright © 1994–1996, 1999–2002, 2004–2017, 2020–2024 Free Software Foundation, Inc. Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. This file is offered as-is, without warranty of any kind. stevegrubb-libcap-ng-9ae4440/Makefile.am000066400000000000000000000021671524174023300201130ustar00rootroot00000000000000# Makefile.am -- # Copyright 2009 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 # ACLOCAL_AMFLAGS = -I m4 SUBDIRS = src utils m4 docs if HAVE_SWIG SUBDIRS += bindings endif EXTRA_DIST = ChangeLog AUTHORS README.md INSTALL COPYING COPYING.LIB \ libcap-ng.spec autogen.sh CONFIG_CLEAN_FILES = debug*.list clean-generic: rm -rf autom4te*.cache rm -f *.rej *.orig *.lang stevegrubb-libcap-ng-9ae4440/README.md000066400000000000000000000455621524174023300173440ustar00rootroot00000000000000libcap-ng ========= The libcap-ng library should make programming with POSIX capabilities easier. The library has some utilities to help you analyze a system for apps that may have too much privileges. The included utilities are designed to let admins and developers spot apps from various ways that may be running with too much privilege. For example, any investigation should start with network facing apps since they would be prime targets for intrusion. The **netcap** program will check all running apps that have listening socket and display the results. Sample output from netcap: ``` ppid pid acct command type port capabilities 1 2295 root nasd tcp 8000 full 2323 2383 root dnsmasq tcp 53 net_admin, net_raw + 1 2286 root sshd tcp 22 full 1 2365 root cupsd tcp 631 full 1 2286 root sshd tcp6 22 full 1 2365 root cupsd tcp6 631 full 2323 2383 root dnsmasq udp 53 net_admin, net_raw + 2323 2383 root dnsmasq udp 67 net_admin, net_raw + 1 2365 root cupsd udp 631 full ``` After checking the networking apps, you should check all running apps with **pscap**. If you are a developer and have to give your application CAP_DAC_OVERRIDE, you must be accessing files for which you have no permission to access. This typically can be resolved by having membership in the correct groups. Try to avoid needing CAP_DAC_OVERRIDE...you may as well be root if you need it. Some application developers have chosen to use file system based capabilities rather than be setuid root and have to drop capabilities. Libcap-ng provides **filecap** to recursively search directories and show you which ones have capabilities and exactly what those are. There is a new utility, **cap-audit** which can audit a program for necessary capabilities. It is discussed in more detail below. C Examples ---------- As an application developer, there are probably 6 use cases that you are interested in: drop all capabilities, keep one capability, keep several capabilities, check if you have any capabilities at all, check for certain capabilities, and retain capabilities across a uid change. 1) Drop all capabilities ```c capng_clear(CAPNG_SELECT_BOTH); capng_apply(CAPNG_SELECT_BOTH); ``` 2) Keep one capability ```c capng_clear(CAPNG_SELECT_BOTH); capng_update(CAPNG_ADD, CAPNG_EFFECTIVE|CAPNG_PERMITTED, CAP_CHOWN); capng_apply(CAPNG_SELECT_BOTH); ``` 3) Keep several capabilities ```c capng_clear(CAPNG_SELECT_BOTH); capng_updatev(CAPNG_ADD, CAPNG_EFFECTIVE|CAPNG_PERMITTED, CAP_SETUID, CAP_SETGID, -1); capng_apply(CAPNG_SELECT_BOTH); ``` 4) Check if you have any capabilities ```c if (capng_have_capabilities(CAPNG_SELECT_CAPS) > CAPNG_NONE) do_something(); ``` 5) Check for a specific capability ```c if (capng_have_capability(CAPNG_EFFECTIVE, CAP_CHOWN)) do_something(); ``` 6) Retain capabilities across a uid change ```c capng_clear(CAPNG_SELECT_BOTH); capng_update(CAPNG_ADD, CAPNG_EFFECTIVE|CAPNG_PERMITTED, CAP_CHOWN); if (capng_change_id(99, 99, CAPNG_DROP_SUPP_GRP | CAPNG_CLEAR_BOUNDING)) error(); ``` Now, isn't that a lot simpler? Note that the last example takes about 60 lines of code using the older capabilities library. As of the 0.6 release, there is a m4 macro file to help add libcap-ng to your autotools config system. In configure.ac, add LIBCAP_NG_PATH. Then in Makefile.am locate the apps that link to libcap-ng, add $(CAPNG_LDADD) to their LDADD entries. And lastly, surround the optional capabilities code with #ifdef HAVE_LIBCAP_NG. Python ------ Libcap-ng 0.6 and later has python bindings. (Only python3 is supported from 0.8.4 onward.) You simply add 'import capng' in your script. Here are the same examples as above in python: 1) Drop all capabilities ```python capng.capng_clear(capng.CAPNG_SELECT_BOTH) capng.capng_apply(capng.CAPNG_SELECT_BOTH) ``` 2) Keep one capability ```python capng.capng_clear(capng.CAPNG_SELECT_BOTH) capng.capng_update(capng.CAPNG_ADD, capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED, capng.CAP_CHOWN) capng.capng_apply(capng.CAPNG_SELECT_BOTH) ``` 3) Keep several capabilities ```python capng.capng_clear(capng.CAPNG_SELECT_BOTH) capng.capng_updatev(capng.CAPNG_ADD, capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED, capng.CAP_SETUID, capng.CAP_SETGID, -1) capng.capng_apply(capng.CAPNG_SELECT_BOTH) ``` 4) Check if you have any capabilities ```python if capng.capng_have_capabilities(capng.CAPNG_SELECT_CAPS) > capng.CAPNG_NONE: do_something() ``` 5) Check for a specific capability ```python if capng.capng_have_capability(capng.CAPNG_EFFECTIVE, capng.CAP_CHOWN): do_something() ``` 6) Retain capabilities across a uid change ```python capng.capng_clear(capng.CAPNG_SELECT_BOTH) capng.capng_update(capng.CAPNG_ADD, capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED, capng.CAP_CHOWN) if capng.capng_change_id(99, 99, capng.CAPNG_DROP_SUPP_GRP | capng.CAPNG_CLEAR_BOUNDING) < 0: error() ``` The Python bindings also provide `capng_print_caps_numeric()` and `capng_print_caps_text()` for printing capabilities or returning them as a Python string. Ambient Capabilities -------------------- Ambient capabilities arrived in the 4.3 Linux kernel. Ambient capabilities allow a privileged process to bestow capabilities to a child process. This is how systemd grants capabilities to a daemon running in a service account. The problem with ambient capabilities is they are inherited forever. Every process exec'ed from the original service also has the capabilities. This is a security issue. To find and fix this, you can run the pscap program and grep for '@'. The '@' symbol denotes processes that have ambient capabilities. For example: ``` # pscap | grep @ 1 1655 systemd-oom systemd-oomd dac_override, kill @ + 1 1656 systemd-resolve systemd-resolve net_raw @ + ``` To fix this, libcap-ng 0.8.3 and later ships libdrop_ambient.so.0. It is designed to be used with LD_PRELOAD. It has a constructor function that forces the dropping of ambient capabilities. By the time the application starts, it has both effective and ambient capabilities - meaning is safe to drop ambient capabilities very early. You can either link it to an application run as a systemd service (using ld), or create a wrapper script that then starts the daemon. Building -------- Note: As of the 0.9 release, libcap-ng is no longer being distributed from people.redhat.com/sgrubb/libcap-ng/ please adjust any scripts to watch this github page for new releases. After cloning libcap-ng, run: ``` cd libcap-ng ./autogen.sh ./configure make make install ``` Use `--with-python` or `--with-python3` to enable the Python 3 bindings, or `--without-python` to disable them. The `netcap --advanced` feature also depends on newer Linux kernel headers. When the core inet/netlink diag headers are not available, such as on older build roots, `configure` will automatically opt out of advanced mode and report that in its output while still building the rest of libcap-ng. VSOCK reporting additionally depends on `linux/vm_sockets.h`; if only that header is missing, `netcap --advanced` is still built and VSOCK endpoints are omitted from its reports. The cap-audit program has to be specifically enabled and defaults to not being built. There is also a spec file to use if you are on a rpm based distribution. To do that, run "make dist" instead of make in the above instructions. Then use the resulting tar file with the spec file. When advanced mode is available, `netcap --advanced --list-interfaces` prints the current network namespace interface names. Add `--json` to get a machine-readable list of names. NOTE: to distributions ---------------------- There is a "make check" target. It only works if the available kernel headers roughly match the build root kernel. Iow, if you have a chroot build system that is using a much older kernel, the macros in the kernel header files will describe functionality that does not exist in the build root. The capng_init function will probe the kernel and decide we can only do v1 rather than v3 capabilities instead of what the kernel headers said was possible. If that is your case, just don't do the "make check" as part of the build process. This problem should go away as build roots eventually switch to the 5.0 or later kernels. cap-audit --------- As of the 0.9 release of libcap-ng, there is a new utility **cap-audit**. This program can be used to determine the actual capabilities that a program needs. To do this, use it to run the application kind of the way one would use strace. Use `--` to separate the options to cap-audit from the program being audited. Runtime tracing requires root or the `CAP_BPF`, `CAP_PERFMON`, and `CAP_SYS_PTRACE` capabilities. When testing a daemon directly, pass command line options that keep it in the foreground. The following is an example checking sshd: The `--service` option audits a systemd service unit. It parses and runs `ExecStart` while simulating the unit's credentials, capability sets, and `NoNewPrivileges` setting. By default, cap-audit traces the unit's `ExecStart` command: ``` cap-audit --service /usr/lib/systemd/system/sshd.service ``` Arguments beginning with `-` after `--` replace the parsed `ExecStart` arguments, while a complete command after `--` replaces `ExecStart` entirely: ``` cap-audit --service /usr/lib/systemd/system/sshd.service -- -D cap-audit --service service-file -- /usr/sbin/sshd -D ``` This is a focused service simulator rather than a complete systemd execution environment. In particular, it does not load `Environment` or `EnvironmentFile`; see the cap-audit(8) man page for the supported settings and `ExecStart` prefix limitations. Cap-audit also records the capability masks installed by successful `capset` calls. A capability requested by `capset` without a confirmed granted check is reported separately under `CAPSET-ONLY CAPABILITIES`; it is not classified as required. Deployment snippets retain these capabilities for compatibility with the current binary because removing one from the deployment boundary can make the application's `capset` fail even when the capability is no longer functionally needed. Treat them as application-cleanup candidates: remove the capability from the application's setup, exercise representative code paths in additional runs, and only then remove it from the deployment boundary. Failed `capset` calls do not establish this compatibility constraint. cap-audit is optional and has to be enabled at build time: ``` ./configure --enable-cap-audit ``` The build needs clang, bpftool, libbpf, libaudit, and their development headers. It also needs a `utils/cap-audit/vmlinux.h` describing the kernel's types. By default this is generated from the running kernel's BTF data in `/sys/kernel/btf/vmlinux`, so the build host kernel must provide enough BTF and tracing metadata for the BPF program to compile. Reading `/sys/kernel/btf/vmlinux` at build time makes the build depend on the host kernel and is therefore not reproducible. To avoid this, a pre-generated `vmlinux.h` can be supplied at configure time: ``` ./configure --enable-cap-audit \ --with-vmlinux-h=provided \ --with-vmlinux-h-path=/path/to/vmlinux.h ``` `--with-vmlinux-h` selects how `vmlinux.h` is obtained: * `auto` (default) uses the file given by `--with-vmlinux-h-path` if set, and otherwise generates one from `/sys/kernel/btf/vmlinux`. * `provided` requires `--with-vmlinux-h-path` and uses that file as-is, without touching `/sys`. * `generated` always generates the file from `/sys/kernel/btf/vmlinux`. A `vmlinux.h` for the target architecture can be produced ahead of time with: ``` bpftool btf dump file /sys/kernel/btf/vmlinux format c >vmlinux.h ``` The kernel used to build and run cap-audit needs the BPF and tracing support used by `utils/cap-audit/cap_audit.bpf.c`. On current kernels, check for these options in `/proc/config.gz` or `/boot/config-$(uname -r)`: ``` CONFIG_BPF=y CONFIG_BPF_SYSCALL=y CONFIG_BPF_EVENTS=y CONFIG_DEBUG_INFO_BTF=y CONFIG_FTRACE_SYSCALLS=y CONFIG_KPROBES=y CONFIG_KRETPROBES=y CONFIG_KPROBE_EVENTS=y CONFIG_PERF_EVENTS=y CONFIG_STACKTRACE=y CONFIG_TRACEPOINTS=y CONFIG_TRACING=y ``` `CONFIG_FTRACE_SYSCALLS` is needed for the `raw_syscalls/sys_enter` and `raw_syscalls/sys_exit` tracepoint types. If it is missing, the generated `vmlinux.h` may not contain `struct trace_event_raw_sys_enter` or `struct trace_event_raw_sys_exit`, and `cap_audit.bpf.c` will not compile. A quick check for the tracepoint structs used by cap-audit is: ``` bpftool btf dump file /sys/kernel/btf/vmlinux format c | grep -E 'struct trace_event_raw_(sys_enter|sys_exit|sched_process_(fork|exec|template))' ``` `CONFIG_BPF_JIT` is recommended for performance, but it is not required just to build cap-audit. ``` cap-audit -- /usr/sbin/sshd -D [*] Capability auditor started [*] Tracing application: /usr/sbin/sshd (PID 30581) [*] Press Ctrl-C to stop ^C[*] Analyzing results... ====================================================================== CAPABILITY ANALYSIS FOR: /usr/sbin/sshd (PID 30581) ====================================================================== SYSTEM CONTEXT: ---------------------------------------------------------------------- Kernel version: 6.18.3-100.fc42.x86_64 kernel.yama.ptrace_scope: 1 kernel.kptr_restrict: 1 kernel.dmesg_restrict: 1 kernel.modules_disabled: 0 kernel.perf_event_paranoid: 2 kernel.unprivileged_bpf_disabled: 2 net.core.bpf_jit_enable: 1 net.core.bpf_jit_harden: 1 net.core.bpf_jit_kallsyms: 1 vm.mmap_min_addr: 65536 fs.protected_hardlinks: 1 fs.protected_symlinks: 1 fs.suid_dumpable: 2 REQUIRED CAPABILITIES: ---------------------------------------------------------------------- chown (#0) Checks: 1 granted, 0 denied Reason: Used by chown (syscall 92) dac_read_search (#2) Checks: 2 granted, 1 denied Reason: Used by openat (syscall 257) setgid (#6) Checks: 30 granted, 2 denied Reason: Used by setgroups (syscall 116) setuid (#7) Checks: 13 granted, 4 denied Reason: Used by setresuid (syscall 117) net_bind_service (#10) Checks: 2 granted, 0 denied Reason: Used by bind (syscall 49) net_admin (#12) Checks: 2 granted, 2 denied Reason: Used by setsockopt (syscall 54) sys_chroot (#18) Checks: 1 granted, 0 denied Reason: Used by chroot (syscall 161) sys_admin (#21) Checks: 597 granted, 1656 denied Reason: Used by brk (syscall 12) sys_resource (#24) Checks: 3 granted, 0 denied Reason: Used by write (syscall 1) audit_write (#29) Checks: 27 granted, 2 denied Reason: Used by sendto (syscall 44) mac_admin (#33) Checks: 1 granted, 0 denied Reason: Used by getxattr (syscall 191) CAPSET-ONLY CAPABILITIES: ---------------------------------------------------------------------- None CONDITIONAL CAPABILITIES: ---------------------------------------------------------------------- CAP_DAC_OVERRIDE Needed when fs.protected_symlinks = 1 for symlinks in world-writable directories Current value: 1 (capability needed) CAPABILITIES WITH ONLY NOT-GRANTED CHECKS: ---------------------------------------------------------------------- dac_override (#1) Capability checks returning not granted: 2 Outcomes of syscall invocations containing such a check: readlink: 1 succeeded faccessat2: 1 succeeded Assessment: Associated syscall outcomes: 2 succeeded. The capability was not granted by those checks, but the associated syscall invocations completed. The capability is not automatically recommended. Recommendation: Not added automatically. bpf (#39) Capability checks returning not granted: 2 Outcomes of syscall invocations containing such a check: prctl: 2 succeeded Assessment: Associated syscall outcomes: 2 succeeded. The capability was not granted by those checks, but the associated syscall invocations completed. The capability is not automatically recommended. Recommendation: Not added automatically. SUMMARY: ---------------------------------------------------------------------- Total capability checks: 2349 Required capabilities: 11 Conditional capabilities: 1 Successful capset calls: 0 Capset-only capabilities: 0 Capabilities with only not-granted checks: 2 Assessment counts: Omitted after associated syscall success: 2 RECOMMENDATIONS: ---------------------------------------------------------------------- Capabilities with only not-granted checks: dac_override: Omitted; associated syscalls succeeded bpf: Omitted; associated syscalls succeeded Capabilities in this section are not added automatically. Add one only after confirming that required functionality fails because the capability is absent. Programmatic solution (C with libcap-ng): #include #include #include ... capng_clear(CAPNG_SELECT_BOTH); capng_updatev(CAPNG_ADD, CAPNG_EFFECTIVE|CAPNG_PERMITTED, CAP_CHOWN, CAP_DAC_READ_SEARCH, CAP_SETGID, CAP_SETUID, CAP_NET_BIND_SERVICE, CAP_NET_ADMIN, CAP_SYS_CHROOT, CAP_SYS_ADMIN, CAP_SYS_RESOURCE, CAP_AUDIT_WRITE, CAP_MAC_ADMIN, -1); if (capng_change_id(uid, gid, CAPNG_DROP_SUPP_GRP | CAPNG_CLEAR_BOUNDING)) { perror("capng_change_id"); exit(EXIT_FAILURE); } For systemd service: [Service] User= Group= AmbientCapabilities=chown dac_read_search setgid setuid net_bind_service net_admin sys_chroot sys_admin sys_resource audit_write mac_admin CapabilityBoundingSet=chown dac_read_search setgid setuid net_bind_service net_admin sys_chroot sys_admin sys_resource audit_write mac_admin For file capabilities (via filecap): filecap /path/to/binary chown dac_read_search setgid setuid net_bind_service net_admin sys_chroot sys_admin sys_resource audit_write mac_admin For Docker/Podman: docker run --user $(id -u):$(id -g) \ --cap-drop=ALL \ --cap-add=chown \ --cap-add=dac_read_search \ --cap-add=setgid \ --cap-add=setuid \ --cap-add=net_bind_service \ --cap-add=net_admin \ --cap-add=sys_chroot \ --cap-add=sys_admin \ --cap-add=sys_resource \ --cap-add=audit_write \ --cap-add=mac_admin \ your-image:tag For Kubernetes: securityContext: runAsUser: 1000 runAsGroup: 1000 capabilities: drop: - ALL add: - chown - dac_read_search - setgid - setuid - net_bind_service - net_admin - sys_chroot - sys_admin - sys_resource - audit_write - mac_admin ``` Reporting --------- Report any bugs in this package to: https://github.com/stevegrubb/libcap-ng/issues stevegrubb-libcap-ng-9ae4440/autogen.sh000077500000000000000000000001701524174023300200500ustar00rootroot00000000000000#! /bin/sh set -x -e # --no-recursive is available only in recent autoconf versions touch NEWS autoreconf -fv --install stevegrubb-libcap-ng-9ae4440/bindings/000077500000000000000000000000001524174023300176465ustar00rootroot00000000000000stevegrubb-libcap-ng-9ae4440/bindings/Makefile.am000066400000000000000000000017251524174023300217070ustar00rootroot00000000000000# Makefile.am -- # Copyright 2009,2015 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 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 = *.loT *.rej *.orig SUBDIRS = src if USE_PYTHON3 SUBDIRS += python3 test endif stevegrubb-libcap-ng-9ae4440/bindings/python3/000077500000000000000000000000001524174023300212525ustar00rootroot00000000000000stevegrubb-libcap-ng-9ae4440/bindings/python3/Makefile.am000066400000000000000000000043051524174023300233100ustar00rootroot00000000000000# Makefile.am -- # Copyright 2009,2014-17 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 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 # SUBDIRS = test CONFIG_CLEAN_FILES = *.loT *.rej *.orig AM_CFLAGS = -fPIC -DPIC $(PYTHON3_CFLAGS) AM_CPPFLAGS = -I. -I$(top_builddir) $(PYTHON3_INCLUDES) LIBS = ${top_builddir}/src/libcap-ng.la SWIG_FLAGS = -python SWIG_INCLUDES = ${AM_CPPFLAGS} pyexec_PYTHON = capng.py pyexec_LTLIBRARIES = _capng.la _capng_la_CFLAGS = -shared _capng_la_LDFLAGS = -module -avoid-version -Wl,-z,relro _capng_la_HEADERS: $(top_builddir)/config.h _capng_la_DEPENDENCIES =${top_srcdir}/src/cap-ng.h ${top_builddir}/src/libcap-ng.la _capng_la_LIBADD = ${top_builddir}/src/libcap-ng.la $(PYTHON3_LIBS) nodist__capng_la_SOURCES = capng_wrap.c capng.py capng_wrap.c: ${srcdir}/../src/capng_swig.i caps.h capng.h swig -o capng_wrap.c ${SWIG_FLAGS} ${SWIG_INCLUDES} ${srcdir}/../src/capng_swig.i caps.h: cat $(CAPABILITY_HEADER) | grep '^#define CAP' | grep -v '[()]' > caps.h capng.h: cat ${top_srcdir}/src/cap-ng.h | grep -v '_state' > capng.h CLEANFILES = capng.py* capng_wrap.c caps.h capng.h *~ clean-local: $(RM) -rf __pycache__ check-local: @python3_embed_libs=`"$(PYTHON3_CONFIG)" --embed --libs 2>/dev/null`; \ if test -z "$$python3_embed_libs"; then \ echo "SKIP: $(PYTHON3_CONFIG) --embed --libs is unavailable"; \ exit 77; \ fi; \ $(MAKE) clean >/dev/null; \ $(MAKE) all-am LDFLAGS="$(LDFLAGS) -Wl,--no-undefined" \ PYTHON3_LIBS="$$python3_embed_libs" stevegrubb-libcap-ng-9ae4440/bindings/python3/test/000077500000000000000000000000001524174023300222315ustar00rootroot00000000000000stevegrubb-libcap-ng-9ae4440/bindings/python3/test/Makefile.am000066400000000000000000000020251524174023300242640ustar00rootroot00000000000000# Makefile.am -- # Copyright 2023 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 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 # dist_check_SCRIPTS = capng-test.py TESTS = $(dist_check_SCRIPTS) TESTS_ENVIRONMENT = PYTHONPATH=${top_builddir}/bindings/python3/:${top_builddir}/bindings/python3/.libs stevegrubb-libcap-ng-9ae4440/bindings/python3/test/capng-test.py000077500000000000000000000043171524174023300246600ustar00rootroot00000000000000#!/usr/bin/env python3 import os import sys import time load_path = '../' if False: sys.path.insert(0, load_path) import capng last = capng.CAP_LAST_CAP try: with open('/proc/sys/kernel/cap_last_cap', 'r') as f: last = int(f.readline()) except IOError as e: print("Error opening /proc/sys/kernel/cap_last_cap: {0}".format(e.strerror)) print("Doing basic bit tests...") capng.capng_clear(capng.CAPNG_SELECT_BOTH) if capng.capng_have_capabilities(capng.CAPNG_SELECT_BOTH) != capng.CAPNG_NONE: print("Failed clearing capabilities\n") sys.exit(1) capng.capng_fill(capng.CAPNG_SELECT_BOTH) if capng.capng_have_capabilities(capng.CAPNG_SELECT_BOTH) != capng.CAPNG_FULL: print("Failed filling capabilities") sys.exit(1) text = capng.capng_print_caps_numeric(capng.CAPNG_PRINT_BUFFER, capng.CAPNG_SELECT_CAPS) len = len(text) if len < 80 and last > 30: last = 30 print("Doing advanced bit tests for %d capabilities...\n" % (last)) for i in range(last+1): capng.capng_clear(capng.CAPNG_SELECT_BOTH) rc = capng.capng_update(capng.CAPNG_ADD, capng.CAPNG_EFFECTIVE, i) if rc: print("Failed update test 1") sys.exit(1) rc = capng.capng_have_capability(capng.CAPNG_EFFECTIVE, int(i)) if rc <= capng.CAPNG_NONE: print("Failed have capability test 1") capng.capng_print_caps_numeric(capng.CAPNG_PRINT_STDOUT, capng.CAPNG_SELECT_CAPS) sys.exit(1) if capng.capng_have_capabilities(capng.CAPNG_SELECT_CAPS) != capng.CAPNG_PARTIAL: print("Failed have capabilities test 1") sys.exit(1) capng.capng_fill(capng.CAPNG_SELECT_BOTH) rc = capng.capng_update(capng.CAPNG_DROP, capng.CAPNG_EFFECTIVE, i) if rc: print("Failed update test 3") sys.exit(1) if capng.capng_have_capabilities(capng.CAPNG_SELECT_CAPS)!=capng.CAPNG_PARTIAL: print("Failed have capabilities test 3") capng.capng_print_caps_numeric(capng.CAPNG_PRINT_STDOUT, capng.CAPNG_SELECT_CAPS) sys.exit(1) rc = capng.capng_update(capng.CAPNG_ADD, capng.CAPNG_EFFECTIVE, i) if rc: print("Failed update test 4") sys.exit(1) if capng.capng_have_capabilities(capng.CAPNG_SELECT_CAPS) != capng.CAPNG_FULL: print("Failed have capabilities test 4") capng.capng_print_caps_numeric(capng.CAPNG_PRINT_STDOUT, capng.CAPNG_SELECT_CAPS) sys.exit(1) sys.exit(0) stevegrubb-libcap-ng-9ae4440/bindings/src/000077500000000000000000000000001524174023300204355ustar00rootroot00000000000000stevegrubb-libcap-ng-9ae4440/bindings/src/Makefile.am000066400000000000000000000016521524174023300224750ustar00rootroot00000000000000# Makefile.am -- # Copyright 2015 Red Hat Inc., Durham, North Carolina. # 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 # EXTRA_DIST = capng_swig.i CONFIG_CLEAN_FILES = *.loT *.rej *.orig stevegrubb-libcap-ng-9ae4440/bindings/src/capng_swig.i000066400000000000000000000041441524174023300227330ustar00rootroot00000000000000/* capngswig.i -- * Copyright 2009 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 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 */ %module capng %{ #include "./capng.h" %} #if defined(SWIGPYTHON) /* * SWIG expands varargs into a fixed argument list. Any omitted optional * arguments are passed to capng_updatev() using this default value. * * capng_updatev() requires (unsigned)-1 as the varargs terminator, so the * default must also be -1 or the function keeps consuming arguments past the * generated wrapper list. * * The old cap of 16 optional entries predates current capability counts and * is easy to hit. Allow up to 64 varargs entries in the wrapper. */ %varargs(64, signed capability = -1) capng_updatev; #endif %define __signed__ signed %enddef #define __attribute(X) /*nothing*/ typedef unsigned __u32; typedef unsigned uid_t; #define __extension__ /*nothing*/ %include "./caps.h" /* * The mutable string-returning APIs allocate their result for * CAPNG_PRINT_BUFFER. SWIG copies that result into a Python string, so release * the original C buffer after converting it. */ %typemap(ret) char *capng_print_caps_numeric { free($1); } %typemap(ret) char *capng_print_caps_text { free($1); } %include "./capng.h" /* SWIG does not emit macros containing a C cast. */ #undef CAPNG_UNSET_ROOTID %constant uid_t CAPNG_UNSET_ROOTID = (uid_t)-1; stevegrubb-libcap-ng-9ae4440/bindings/test/000077500000000000000000000000001524174023300206255ustar00rootroot00000000000000stevegrubb-libcap-ng-9ae4440/bindings/test/Makefile.am000066400000000000000000000003141524174023300226570ustar00rootroot00000000000000# Makefile.am -- Test suite for python bindings TESTS = lib_test.py TESTS_ENVIRONMENT = PYTHONPATH=${top_builddir}/bindings/python3/:${top_builddir}/bindings/python3/.libs dist_check_SCRIPTS = $(TESTS) stevegrubb-libcap-ng-9ae4440/bindings/test/lib_test.py000077500000000000000000000123201524174023300230050ustar00rootroot00000000000000#!/usr/bin/env python3 import sys import capng def get_last_cap(): last = capng.CAP_LAST_CAP try: with open('/proc/sys/kernel/cap_last_cap', 'r') as f: last = int(f.readline()) except OSError: pass return last def main(): last = get_last_cap() if capng.CAPNG_UNSET_ROOTID != 0xffffffff: print("CAPNG_UNSET_ROOTID has the wrong value") sys.exit(1) if capng.capng_set_rootid(0x80000000) == 0: if capng.capng_get_rootid() != 0x80000000: print("Failed high rootid round trip") sys.exit(1) if capng.capng_set_rootid(capng.CAPNG_UNSET_ROOTID) != 0: print("Failed clearing rootid") sys.exit(1) print("Doing basic bit tests...") capng.capng_clear(capng.CAPNG_SELECT_BOTH) if capng.capng_have_capabilities(capng.CAPNG_SELECT_BOTH) != capng.CAPNG_NONE: print("Failed clearing capabilities") sys.exit(1) # capng_save_state/capng_restore_state are not available in python bindings capng.capng_fill(capng.CAPNG_SELECT_BOTH) if capng.capng_have_capabilities(capng.CAPNG_SELECT_BOTH) != capng.CAPNG_FULL: print("Failed filling capabilities") sys.exit(1) text = capng.capng_print_caps_numeric(capng.CAPNG_PRINT_BUFFER, capng.CAPNG_SELECT_CAPS) if len(text) < 80 and last > 30: last = 30 print("Doing advanced bit tests for %d capabilities..." % last) for i in range(last + 1): capng.capng_clear(capng.CAPNG_SELECT_BOTH) rc = capng.capng_update(capng.CAPNG_ADD, capng.CAPNG_EFFECTIVE, i) if rc: print("Failed update test 1") sys.exit(1) rc = capng.capng_have_capability(capng.CAPNG_EFFECTIVE, i) if rc <= capng.CAPNG_NONE: print("Failed have capability test 1") capng.capng_print_caps_numeric(capng.CAPNG_PRINT_STDOUT, capng.CAPNG_SELECT_CAPS) sys.exit(1) if capng.capng_have_capabilities(capng.CAPNG_SELECT_CAPS) != capng.CAPNG_PARTIAL: print("Failed have capabilities test 1") capng.capng_print_caps_numeric(capng.CAPNG_PRINT_STDOUT, capng.CAPNG_SELECT_CAPS) sys.exit(1) if capng.CAP_LAST_CAP > 31: rc = capng.capng_update(capng.CAPNG_ADD, capng.CAPNG_BOUNDING_SET, i) if rc: print("Failed bset update test 2") sys.exit(1) rc = capng.capng_have_capability(capng.CAPNG_BOUNDING_SET, i) if rc <= capng.CAPNG_NONE: print("Failed bset have capability test 2") capng.capng_print_caps_numeric(capng.CAPNG_PRINT_STDOUT, capng.CAPNG_SELECT_BOTH) sys.exit(1) if capng.capng_have_capabilities(capng.CAPNG_SELECT_BOUNDS) != capng.CAPNG_PARTIAL: print("Failed bset have capabilities test 2") capng.capng_print_caps_numeric(capng.CAPNG_PRINT_STDOUT, capng.CAPNG_SELECT_BOTH) sys.exit(1) text = capng.capng_print_caps_text(capng.CAPNG_PRINT_BUFFER, capng.CAPNG_EFFECTIVE) name = capng.capng_capability_to_name(i) if text != name: print("Failed print text comparison") print("%s != %s" % (text, name)) sys.exit(1) capng.capng_fill(capng.CAPNG_SELECT_BOTH) rc = capng.capng_update(capng.CAPNG_DROP, capng.CAPNG_EFFECTIVE, i) if rc: print("Failed update test 3") sys.exit(1) if capng.capng_have_capabilities(capng.CAPNG_SELECT_CAPS) != capng.CAPNG_PARTIAL: print("Failed have capabilities test 3") capng.capng_print_caps_numeric(capng.CAPNG_PRINT_STDOUT, capng.CAPNG_SELECT_CAPS) sys.exit(1) rc = capng.capng_update(capng.CAPNG_ADD, capng.CAPNG_EFFECTIVE, i) if rc: print("Failed update test 4") sys.exit(1) if capng.capng_have_capabilities(capng.CAPNG_SELECT_CAPS) != capng.CAPNG_FULL: print("Failed have capabilities test 4") capng.capng_print_caps_numeric(capng.CAPNG_PRINT_STDOUT, capng.CAPNG_SELECT_CAPS) sys.exit(1) capng.capng_clear(capng.CAPNG_SELECT_BOTH) rc = capng.capng_updatev(capng.CAPNG_ADD, capng.CAPNG_EFFECTIVE, capng.CAP_CHOWN, capng.CAP_FOWNER, capng.CAP_KILL, -1) if rc: print("Failed updatev test") sys.exit(1) rc = (capng.capng_have_capability(capng.CAPNG_EFFECTIVE, capng.CAP_CHOWN) and capng.capng_have_capability(capng.CAPNG_EFFECTIVE, capng.CAP_FOWNER) and capng.capng_have_capability(capng.CAPNG_EFFECTIVE, capng.CAP_KILL)) if not rc: print("Failed have updatev capability test") capng.capng_print_caps_numeric(capng.CAPNG_PRINT_STDOUT, capng.CAPNG_SELECT_CAPS) sys.exit(1) sys.exit(0) if __name__ == '__main__': main() stevegrubb-libcap-ng-9ae4440/configure.ac000066400000000000000000000364561524174023300203550ustar00rootroot00000000000000dnl define([AC_INIT_NOTICE], [### Generated automatically using autoconf version] AC_ACVERSION [ ### Copyright 2009-2026 Steve Grubb ### ### Permission is hereby granted, free of charge, to any person obtaining a ### copy of this software and associated documentation files (the "Software"), ### to deal in the Software without restriction, including without limitation ### the rights to use, copy, modify, merge, publish, distribute, sublicense, ### and/or sell copies of the Software, and to permit persons to whom the ### Software is furnished to do so, subject to the following conditions: ### ### The above copyright notice and this permission notice shall be included ### in all copies or substantial portions of the Software. ### ### THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ### IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ### FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL ### THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR ### OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ### ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR ### OTHER DEALINGS IN THE SOFTWARE. ### ### For usage, run `./configure --help' ### For more detailed information on installation, read the file `INSTALL'. ### ### If configuration succeeds, status is in the file `config.status'. ### A log of configuration tests is in `config.log'. ]) AC_REVISION($Revision: 1.3 $)dnl AC_INIT([libcap-ng],[0.9.5]) AC_PREREQ([2.50])dnl AC_CONFIG_HEADERS([config.h]) AC_CONFIG_MACRO_DIR([m4]) echo Configuring libcap-ng $VERSION AC_CANONICAL_TARGET AM_INIT_AUTOMAKE LT_INIT AC_SUBST(LIBTOOL_DEPS) PKG_PROG_PKG_CONFIG echo . echo Checking for programs AC_PROG_CC AC_PROG_INSTALL AC_PROG_AWK AC_MSG_CHECKING([for __atomic_load_n]) AC_LINK_IFELSE( [AC_LANG_PROGRAM( [[unsigned int test_atomic;]], [[return __atomic_load_n(&test_atomic, __ATOMIC_ACQUIRE);]])], [AC_MSG_RESULT(yes)], [AC_MSG_FAILURE([Compiler support for __atomic_load_n is required by libcap-ng])]) LIBBPF_CFLAGS="" LIBBPF_LIBS="" LIBAUDIT_CFLAGS="" LIBAUDIT_LIBS="" CLANG="" BPFTOOL="" BPF_ARCH="" AC_CANONICAL_HOST AC_ARG_WITH([bpf-arch], AS_HELP_STRING([--with-bpf-arch=ARCH], [override detected BPF target arch (x86, arm64, arm, powerpc, s390, riscv, mips, loongarch)]), [BPF_ARCH="$withval"]) AC_ARG_ENABLE([cap-audit], AS_HELP_STRING([--enable-cap-audit],[build cap-audit [[default=no]]]), [enable_cap_audit=$enableval], [enable_cap_audit=no]) dnl For reproducible builds cap-audit's vmlinux.h can be supplied at dnl configure time instead of being generated from the running kernel's BTF dnl data in /sys/kernel/btf/vmlinux. AC_ARG_WITH([vmlinux-h], AS_HELP_STRING([--with-vmlinux-h=MODE], [how to obtain cap-audit's vmlinux.h: auto, provided, or generated [[default=auto]]]), [with_vmlinux_h="$withval"], [with_vmlinux_h=auto]) AC_ARG_WITH([vmlinux-h-path], AS_HELP_STRING([--with-vmlinux-h-path=PATH], [path to a pre-generated vmlinux.h to use for cap-audit]), [with_vmlinux_h_path="$withval"], [with_vmlinux_h_path=""]) VMLINUX_H_PATH="" vmlinux_h_status="n/a (cap-audit disabled)" if test "x$enable_cap_audit" = "xyes"; then missing_cap_audit_deps=no AC_CHECK_PROG([CLANG],[clang],[clang],[no]) AC_CHECK_PROG([BPFTOOL],[bpftool],[bpftool],[no]) PKG_CHECK_MODULES([LIBBPF],[libbpf], [have_libbpf=yes],[have_libbpf=no]) PKG_CHECK_MODULES([LIBAUDIT],[audit], [have_libaudit=yes],[have_libaudit=no]) AC_CHECK_HEADERS([bpf/libbpf.h bpf/bpf.h linux/bpf.h libaudit.h], [], [missing_cap_audit_deps=yes]) if test "$CLANG" = "no" -o "$BPFTOOL" = "no" -o \ "x$have_libbpf" != "xyes" -o "x$have_libaudit" != "xyes" -o \ "x$missing_cap_audit_deps" = "xyes"; then AC_MSG_ERROR([cap-audit requires clang, bpftool, libbpf, and libaudit]) fi AC_MSG_CHECKING([how to obtain vmlinux.h]) case "$with_vmlinux_h" in auto) if test "x$with_vmlinux_h_path" != "x"; then vmlinux_h_source=provided else vmlinux_h_source=generated fi ;; provided) vmlinux_h_source=provided ;; generated) vmlinux_h_source=generated ;; *) AC_MSG_RESULT([error]) AC_MSG_ERROR([invalid --with-vmlinux-h value '$with_vmlinux_h' (expected auto, provided, or generated)]) ;; esac if test "x$vmlinux_h_source" = "xprovided"; then if test "x$with_vmlinux_h_path" = "x"; then AC_MSG_RESULT([error]) AC_MSG_ERROR([--with-vmlinux-h=provided requires --with-vmlinux-h-path=PATH]) fi if test ! -f "$with_vmlinux_h_path"; then AC_MSG_RESULT([error]) AC_MSG_ERROR([provided vmlinux.h not found: $with_vmlinux_h_path]) fi dnl Resolve to an absolute path so the build works from any directory. case "$with_vmlinux_h_path" in /*) VMLINUX_H_PATH="$with_vmlinux_h_path" ;; *) VMLINUX_H_PATH="`pwd`/$with_vmlinux_h_path" ;; esac vmlinux_h_status="provided ($VMLINUX_H_PATH)" AC_MSG_RESULT([provided ($VMLINUX_H_PATH)]) else vmlinux_h_status="generated from /sys/kernel/btf/vmlinux" AC_MSG_RESULT([generated from /sys/kernel/btf/vmlinux]) fi fi if test "x$BPF_ARCH" = "x"; then case "$host_cpu" in x86_64|i?86) BPF_ARCH=x86 ;; aarch64|arm64) BPF_ARCH=arm64 ;; arm*) BPF_ARCH=arm ;; ppc64le|ppc64|powerpc*) BPF_ARCH=powerpc ;; s390x|s390) BPF_ARCH=s390 ;; riscv64|riscv32|riscv*) BPF_ARCH=riscv ;; mips64|mips64el|mips|mipsel) BPF_ARCH=mips ;; loongarch64) BPF_ARCH=loongarch ;; *) BPF_ARCH=x86 ;; esac fi if test "x$BPF_ARCH" = "x"; then AC_MSG_ERROR([failed to determine BPF target arch; use --with-bpf-arch]) fi AC_MSG_CHECKING(enable-deprecated) AC_ARG_ENABLE([deprecated], AS_HELP_STRING([--enable-deprecated], [enable deprecated utilities such as captest [[default=no]]]), [enable_deprecated=$enableval], [enable_deprecated=no]) if test x$enable_deprecated = xno ; then AC_MSG_RESULT(no) else AC_MSG_RESULT(yes) fi AC_SUBST(BPF_ARCH) AC_SUBST(CLANG) AC_SUBST(BPFTOOL) AC_SUBST(LIBBPF_CFLAGS) AC_SUBST(LIBBPF_LIBS) AC_SUBST(LIBAUDIT_CFLAGS) AC_SUBST(LIBAUDIT_LIBS) AC_SUBST(VMLINUX_H_PATH) AM_CONDITIONAL([BUILD_CAP_AUDIT], [test "x$enable_cap_audit" = "xyes"]) AM_CONDITIONAL([PROVIDED_VMLINUX_H], [test "x$VMLINUX_H_PATH" != "x"]) AM_CONDITIONAL([BUILD_DEPRECATED], [test "x$enable_deprecated" = "xyes"]) echo . echo Checking for header files AC_CHECK_HEADERS(linux/capability.h, [], [AC_MSG_ERROR(linux/capability.h is required in order to build libcap-ng.)]) AC_CHECK_HEADERS(sys/xattr.h, [], [ AC_CHECK_HEADERS(attr/xattr.h, [], [AC_MSG_WARN(attr/xattr.h not found, disabling file system capabilities.)]) ]) AC_CHECK_HEADERS(linux/securebits.h, [], []) AC_CHECK_HEADERS(bluetooth/bluetooth.h bluetooth/hci.h, [], []) AC_CHECK_HEADERS(linux/inet_diag.h linux/netlink.h linux/sock_diag.h, [], []) AC_CHECK_HEADERS(sys/socket.h, [], []) AC_CHECK_HEADERS(linux/vm_sockets.h, [], [], [AC_INCLUDES_DEFAULT #ifdef HAVE_SYS_SOCKET_H #include #endif ]) AC_CHECK_HEADERS(linux/vm_sockets_diag.h, [], []) AC_CHECK_HEADERS(pthread.h, [AC_SEARCH_LIBS(pthread_atfork, pthread)], [AC_MSG_WARN(pthread.h not found, disabling pthread_atfork.)]) AC_CHECK_HEADERS(sys/vfs.h, [ AC_CHECK_HEADERS(linux/magic.h, [], [AC_MSG_WARN(linux/magic.h is required in order to verify procfs.)]) ], [AC_MSG_WARN(sys/vfs.h is required in order to verify procfs.)]) build_netcap_advanced=yes netcap_advanced_missing="" for netcap_hdr in \ linux/inet_diag.h:$ac_cv_header_linux_inet_diag_h \ linux/netlink.h:$ac_cv_header_linux_netlink_h \ linux/sock_diag.h:$ac_cv_header_linux_sock_diag_h; do netcap_name=${netcap_hdr%%:*} netcap_have=${netcap_hdr#*:} AS_IF([test "x$netcap_have" != xyes], [ build_netcap_advanced=no AS_IF([test "x$netcap_advanced_missing" = x], [ netcap_advanced_missing="$netcap_name" ], [ netcap_advanced_missing="$netcap_advanced_missing, $netcap_name" ]) ]) done AS_IF([test "x$build_netcap_advanced" = xyes], [ AC_DEFINE([HAVE_NETCAP_ADVANCED], [1], [Define to 1 if netcap advanced mode can be built.]) netcap_advanced_status="enabled" AC_MSG_NOTICE([netcap advanced mode enabled]) ], [ netcap_advanced_status="disabled (missing headers: $netcap_advanced_missing)" AC_MSG_WARN([netcap advanced mode disabled; missing headers: $netcap_advanced_missing]) ]) AC_SUBST([NETCAP_ADVANCED_ENABLED], [$build_netcap_advanced]) AC_SUBST([NETCAP_ADVANCED_STATUS], [$netcap_advanced_status]) AM_CONDITIONAL([BUILD_NETCAP_ADVANCED], [test "x$build_netcap_advanced" = xyes]) AS_IF([test "x$build_netcap_advanced" = xyes], [ AS_IF([test "x$ac_cv_header_linux_vm_sockets_h" = xyes], [ AC_DEFINE([HAVE_NETCAP_VSOCK], [1], [Define to 1 if netcap VSOCK support can be built.]) netcap_vsock_status="enabled" ], [ netcap_vsock_status="disabled (missing header: linux/vm_sockets.h)" AC_MSG_WARN([netcap VSOCK support disabled; missing header: linux/vm_sockets.h]) ]) ], [ netcap_vsock_status="disabled (advanced mode disabled)" ]) AC_ARG_WITH([capability_header], [AS_HELP_STRING([--with-capability_header=path : path to capability.h])], [CAPABILITY_HEADER=$withval], [CAPABILITY_HEADER=/usr/include/linux/capability.h]) AC_SUBST(CAPABILITY_HEADER) AC_MSG_CHECKING(__attr_dealloc_free support) AC_COMPILE_IFELSE( [AC_LANG_SOURCE( [[ #include extern char *test(const char *name) __attr_dealloc_free; int main(void) { return 0; }]])], [DEALLOC="yes"], [DEALLOC="no"] ) AC_MSG_RESULT($DEALLOC) AC_ARG_WITH(warn, [ --with-warn turn on warnings [[default=yes]]], [ if test "x${withval}" = xyes; then WARNS="$ALLWARNS" else WARNS="" fi ],WARNS="$ALLWARNS") AC_SUBST(DEBUG) WFLAGS="" AC_MSG_CHECKING(for -Wformat-truncation) TMPCFLAGS="${CFLAGS}" CFLAGS="${CFLAGS} -Wformat-truncation" AC_LINK_IFELSE([AC_LANG_PROGRAM([[]], [[]])],[WFLAGS="-Wno-format-truncation" AC_MSG_RESULT(yes)], [AC_MSG_RESULT(no)]) CFLAGS="${TMPCFLAGS}" AC_SUBST(WFLAGS) AC_CHECK_HEADER(sys/syscall.h, [AC_DEFINE([HAVE_SYSCALL_H], [1], [Define to 1 if you have .])], [],) dnl; This is to record in the build logs what options are being taken AC_CHECK_DECLS([PR_CAP_AMBIENT], [], [], [[#include ]]) AC_CHECK_DECLS([VFS_CAP_REVISION_2], [], [], [[#include ]]) AC_CHECK_DECLS([VFS_CAP_REVISION_3], [], [], [[#include ]]) AC_CHECK_DECLS([CAP_EPOLLWAKEUP], [], [], [[#include ]]) AC_CHECK_DECLS([CAP_WAKE_ALARM], [], [], [[#include ]]) AC_CHECK_DECLS([CAP_BLOCK_SUSPEND], [], [], [[#include ]]) AC_CHECK_DECLS([CAP_AUDIT_READ], [], [], [[#include ]]) AC_CHECK_DECLS([CAP_PERFMON], [], [], [[#include ]]) AC_CHECK_DECLS([CAP_BPF], [], [], [[#include ]]) AC_CHECK_DECLS([CAP_CHECKPOINT_RESTORE], [], [], [[#include ]]) dnl only build libdrop_ambient if support for ambient capabilities was found (which is normal) if test x"${ac_cv_have_decl_PR_CAP_AMBIENT}" = x"no" ; then AC_MSG_WARN("PR_CAP_AMBIENT not available - libdrop_ambient will not be built") fi AM_CONDITIONAL(BUILD_LIBDROP_AMBIENT, test x"${ac_cv_have_decl_PR_CAP_AMBIENT}" = x"yes") dnl Python 2 is no longer supported, so the generic Python option controls dnl the Python 3 bindings. A version-specific option takes precedence because dnl it is processed after the generic option. AC_ARG_WITH(python, AS_HELP_STRING([--with-python], [enable Python 3 bindings (use --without-python to disable)]), use_python3=$withval, use_python3=auto) AC_MSG_CHECKING(whether to create python3 bindings) AC_ARG_WITH(python3, AS_HELP_STRING([--with-python3],[enable Python 3 bindings]), use_python3=$withval) AC_ARG_VAR([PYTHON3_CONFIG], [path to the Python 3 configuration helper]) python3_failure= if test x$use_python3 = xno ; then AC_MSG_RESULT(no) swig_found=no else AC_MSG_RESULT(investigating) AC_CHECK_PROG(swig_found, swig, yes, no) if test x$swig_found = xno ; then python3_failure="SWIG was not found" fi if test -z "$python3_failure" ; then AM_PATH_PYTHON([3.1], [], [python3_failure="a usable Python 3 interpreter was not found"]) fi if test -z "$python3_failure" ; then dnl Prefer the configuration helper installed beside the selected dnl interpreter. The versioned fallback avoids mixing installations dnl when several Python 3 versions are present in PATH. if test -n "$PYTHON3_CONFIG" ; then AC_MSG_CHECKING([for a Python 3 configuration helper]) AC_MSG_RESULT([$PYTHON3_CONFIG]) elif test -x "${PYTHON}-config" ; then PYTHON3_CONFIG="${PYTHON}-config" AC_MSG_CHECKING([for a Python 3 configuration helper]) AC_MSG_RESULT([$PYTHON3_CONFIG]) else AC_PATH_PROGS([PYTHON3_CONFIG], [python${PYTHON_VERSION}-config python3-config], [no]) fi if test x$PYTHON3_CONFIG = xno ; then python3_failure="a Python 3 configuration helper was not found" fi fi if test -z "$python3_failure" ; then AC_MSG_CHECKING([whether $PYTHON3_CONFIG provides build flags]) if PYTHON3_CFLAGS=`"$PYTHON3_CONFIG" --cflags 2>/dev/null` && PYTHON3_INCLUDES=`"$PYTHON3_CONFIG" --includes 2>/dev/null` && PYTHON3_LIBS=`"$PYTHON3_CONFIG" --libs 2>/dev/null` ; then AC_MSG_RESULT(yes) else AC_MSG_RESULT(no) python3_failure="$PYTHON3_CONFIG could not provide build flags" fi fi if test -z "$python3_failure" ; then python3_major=`"$PYTHON" -c \ 'import sys; print(sys.version_info.major)'` python3_minor=`"$PYTHON" -c \ 'import sys; print(sys.version_info.minor)'` AC_MSG_CHECKING([whether Python.h matches $PYTHON]) save_CPPFLAGS=$CPPFLAGS CPPFLAGS="$CPPFLAGS $PYTHON3_INCLUDES" AC_COMPILE_IFELSE( [AC_LANG_PROGRAM([[#include #if PY_MAJOR_VERSION != $python3_major || \ PY_MINOR_VERSION != $python3_minor #error Python.h does not match the selected interpreter #endif]], [[]])], [AC_MSG_RESULT(yes)], [AC_MSG_RESULT(no) python3_failure="Python.h is missing or does not match $PYTHON"]) CPPFLAGS=$save_CPPFLAGS fi if test -n "$python3_failure" ; then if test x$use_python3 = xyes ; then AC_MSG_ERROR([$python3_failure]) fi AC_MSG_NOTICE([$python3_failure - Python 3 bindings will not be built]) use_python3=no else AC_MSG_NOTICE([Python 3 bindings will be built]) use_python3=yes fi fi AC_SUBST(PYTHON3_CONFIG) AC_SUBST(PYTHON3_CFLAGS) AC_SUBST(PYTHON3_LIBS) AC_SUBST(PYTHON3_INCLUDES) AM_CONDITIONAL(HAVE_SWIG, test x"${swig_found}" = x"yes") AM_CONDITIONAL(USE_PYTHON3, test ${use_python3} = "yes") AC_CONFIG_FILES([Makefile src/Makefile src/libcap-ng.pc src/test/Makefile bindings/Makefile bindings/python3/Makefile bindings/src/Makefile bindings/python3/test/Makefile bindings/test/Makefile utils/Makefile utils/test/Makefile utils/cap-audit/Makefile \ utils/cap-audit/test/Makefile m4/Makefile docs/Makefile]) AC_CONFIG_COMMANDS([utils/netcap.8], [ if test "x$NETCAP_ADVANCED_ENABLED" = xyes; then sed '/^.\\" BEGIN_ADVANCED$/d; /^.\\" END_ADVANCED$/d' \ "$srcdir/utils/netcap.8.in" > utils/netcap.8 else sed '/^.\\" BEGIN_ADVANCED$/,/^.\\" END_ADVANCED$/d' \ "$srcdir/utils/netcap.8.in" > utils/netcap.8 fi ], [NETCAP_ADVANCED_ENABLED="$NETCAP_ADVANCED_ENABLED" srcdir="$srcdir"]) AC_OUTPUT echo . echo " libcap-ng Version: $VERSION Target: $target Installation prefix: $prefix Build Kernel: `uname -r` Compiler: $CC Compiler flags: `echo $CFLAGS | fmt -w 50 | sed 's,^, ,'` __attr_dealloc_free support: $DEALLOC netcap advanced mode: $netcap_advanced_status netcap VSOCK support: $netcap_vsock_status cap-audit vmlinux.h: $vmlinux_h_status " stevegrubb-libcap-ng-9ae4440/docs/000077500000000000000000000000001524174023300170015ustar00rootroot00000000000000stevegrubb-libcap-ng-9ae4440/docs/Makefile.am000066400000000000000000000030351524174023300210360ustar00rootroot00000000000000# Makefile.am -- # Copyright 2009,2020 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 # CONFIG_CLEAN_FILES = *.rej *.orig EXTRA_DIST = $(man_MANS) man_MANS = capng_clear.3 capng_fill.3 capng_setpid.3 \ capng_get_caps_process.3 capng_update.3 capng_updatev.3 \ capng_apply.3 capng_lock.3 capng_change_id.3 capng_get_caps_fd.3 \ capng_stage_additional_groups.3 \ capng_apply_caps_fd.3 capng_have_capabilities.3 \ capng_get_rootid.3 capng_set_rootid.3 capng_have_capability.3 \ capng_print_caps_numeric.3 capng_print_caps_text.3 \ capng_name_to_capability.3 capng_capability_to_name.3 \ capng_save_state.3 capng_restore_state.3 libdrop_ambient.7 dist_check_SCRIPTS = check-manpages.sh TESTS = $(dist_check_SCRIPTS) TESTS_ENVIRONMENT = srcdir='$(srcdir)' top_srcdir='$(top_srcdir)' \ top_builddir='$(top_builddir)' stevegrubb-libcap-ng-9ae4440/docs/capng_apply.3000066400000000000000000000037211524174023300213650ustar00rootroot00000000000000.TH "CAPNG_APPLY" "3" "Sept 2023" "Red Hat" "Libcap-ng API" .SH NAME capng_apply \- apply the stored capabilities settings .SH "SYNOPSIS" .B #include .sp int capng_apply(capng_select_t set); .SH "DESCRIPTION" capng_apply will transfer the specified internal POSIX capabilities settings to the kernel. The options are CAPNG_SELECT_CAPS for the traditional capabilities, CAPNG_SELECT_BOUNDS for the bounding set, CAPNG_SELECT_BOTH if transferring both is desired, CAPNG_SELECT_AMBIENT if only operating on the ambient capabilities, or CAPNG_SELECT_ALL if applying all is desired. .SH "RETURN VALUE" This returns 0 on success and a negative value on failure. The values are: .RS .TP -1 not initialized .TP -2 CAPNG_SELECT_BOUNDS and failure to drop a bounding set capability .TP -3 CAPNG_SELECT_BOUNDS and failure to re-read bounding set .TP -4 CAPNG_SELECT_BOUNDS and process does not have CAP_SETPCAP .TP -5 CAPNG_SELECT_CAPS and failure in capset syscall .TP -6 CAPNG_SELECT_AMBIENT and process has no capabilities and failed clearing ambient capabilities .TP -7 CAPNG_SELECT_AMBIENT and process has capabilities and failed clearing ambient capabilities .TP -8 CAPNG_SELECT_AMBIENT and process has capabilities and failed setting an ambient capability .TP -9 Unable to acquire process capabilities to check if CAP_SETPCAP is set. .RE .SH NOTES If you are doing multi-threaded programming, calling this function will only set capabilities on the calling thread. All other threads are unaffected. If you want to set overall capabilities for a multi-threaded process, you will need to do that before creating any threads. See the capset syscall for more information on this topic. Also, bits in the bounding set can only be dropped. You cannot set them. After dropping bounding set capabilities, the bounding set is synchronized with the kernel to reflect the true state in the kernel. .SH "SEE ALSO" .BR capset (2), .BR capng_update (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_apply_caps_fd.3000066400000000000000000000015421524174023300230430ustar00rootroot00000000000000.TH "CAPNG_APPLY_CAPS_FD" "3" "Sept 2020" "Red Hat" "Libcap-ng API" .SH NAME capng_apply_caps_fd \- write file-based capabilities to extended attributes .SH "SYNOPSIS" .B #include .sp int capng_apply_caps_fd(int fd); .SH "DESCRIPTION" This function will write the file based capabilities to the extended attributes of the file that the descriptor was opened against. The bounding set is not included in file based capabilities operations. Note that this function will only work if compiled on a kernel that supports file based capabilities such as 2.6.2 6 and later. .SH "RETURN VALUE" This returns 0 on success, -1 if something besides a regular file is passed or the extended attribute operation fails, and -2 if the file capability data cannot be serialized. .SH "SEE ALSO" .BR capng_get_caps_fd (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_capability_to_name.3000066400000000000000000000016141524174023300240620ustar00rootroot00000000000000.TH "CAPNG_CAPABILITY_TO_NAME" "3" "June 2009" "Red Hat" "Libcap-ng API" .SH NAME capng_capability_to_name \- convert capability integer to text .SH "SYNOPSIS" .B #include .sp const char *capng_capability_to_name(unsigned int capability); .SH "DESCRIPTION" capng_capability_to_name will take the integer being passed and look it up to see what its text string representation would be. The integer being input must be in the valid range defined in linux/capability.h. The string that is output is the same as the define text from linux/capability.h with the CAP_ prefix removed and lower case. This is useful for taking integer representation and converting it to something more user friendly for display. .SH "RETURN VALUE" This returns a NULL pointer on failure and the correct string otherwise. .SH "SEE ALSO" .BR capng_name_to_capability (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_change_id.3000066400000000000000000000116521524174023300221430ustar00rootroot00000000000000.TH "CAPNG_CHANGE_ID" "3" "Mar 2026" "Red Hat" "Libcap-ng API" .SH NAME capng_change_id \- change the credentials retaining capabilities .SH "SYNOPSIS" .B #include .sp int capng_change_id(int uid, int gid, capng_flags_t flag); .SH "DESCRIPTION" This function will change uid and gid to the ones given while retaining the capabilities previously specified in capng_update. It is also possible to specify -1 for either the uid or gid in which case the function will not change the uid or gid and leave it "as is". This is useful if you just want the flag options to be applied (assuming the option doesn't require more privileges that you currently have). It is not necessary and perhaps better if capng_apply has not been called prior to this function so that all necessary privileges are still intact. The caller may be required to have CAP_SETPCAP capability still active before calling this function or capabilities cannot be changed. Do not specify CAP_SETPCAP in the capability sets unless you intended to have it after the uid change. The library will silently add and remove it if it can. But if its specified, it will leave it alone. This function also takes a flag parameter that helps to tailor the exact actions performed by the function to secure the environment. The option may be or'ed together. The legal values are: .RS .TP .B CAPNG_NO_FLAG Simply change uid and retain specified capabilities and that's all. .TP .B CAPNG_DROP_SUPP_GRP After changing the primary group ID, remove all supplementary groups. If no new primary group ID is supplied (gid == -1), this flag has no effect. .TP .B CAPNG_INIT_SUPP_GRP After changing id, initialize any supplemental groups that may come with the new account. If given with .B CAPNG_DROP_SUPP_GRP it will have no effect. .TP .B CAPNG_APPLY_STAGED_GROUPS Available in libcap-ng 0.9.3 and later. Apply additional groups previously staged with .BR capng_stage_additional_groups (3). If used by itself, the staged list is applied exactly. If combined with .B CAPNG_INIT_SUPP_GRP the account's natural supplemental groups are initialized first and the staged gids are then merged in without duplicates. If combined with .B CAPNG_DROP_SUPP_GRP the call fails. If this flag is set and no staged gids are present, the call fails. Staged gids are intended to be used once and are always cleared before .B capng_change_id returns, even when the flag is omitted or the call fails. .TP .B CAPNG_CLEAR_BOUNDING Clear the bounding set regardless to the internal representation already setup prior to changing the uid/gid. .TP .B CAPNG_APPLY_BOUNDING Available in libcap-ng 0.9.3 and later. Apply the internally prepared bounding set during .BR capng_change_id . This only acts on bounding-set state explicitly prepared with .BR capng_clear (3), .BR capng_fill (3), or .BR capng_update (3) using .BR CAPNG_BOUNDING_SET . If no prepared bounding-set state exists, this flag is a no-op. If combined with .B CAPNG_CLEAR_BOUNDING the call fails. .TP .B CAPNG_CLEAR_AMBIENT Clear ambient capabilities regardless of the internal representation already setup prior to changing the uid/gid. .RE .SH "RETURN VALUE" This returns 0 on success and a negative number on failure. .RS .TP -1 means capng has not been initted properly .TP -2 means a failure requesting to keep capabilities across the uid change .TP -3 means that applying the intermediate capabilities failed .TP -4 means changing gid failed .TP -5 means initializing supplemental groups failed .TP -6 means changing the uid failed .TP -7 means dropping the ability to retain caps across a uid change failed .TP -8 means clearing the bounding set failed .TP -9 means dropping CAP_SETPCAP or ambient capabilities failed .TP -10 means resolving the target account for supplemental groups failed .TP -11 means dropping supplemental groups failed .TP -12 means CAPNG_DROP_SUPP_GRP and CAPNG_APPLY_STAGED_GROUPS were both set .TP -13 means CAPNG_APPLY_STAGED_GROUPS was requested with no staged gids .TP -14 means applying staged additional groups failed .TP -15 means looking up a group account in additional groups failed .TP -16 means merging initialized and staged additional groups failed .TP -17 means CAPNG_APPLY_BOUNDING and CAPNG_CLEAR_BOUNDING were both set .RE Applications that need to support libcap-ng releases older than 0.9.3 should detect the availability of .B CAPNG_APPLY_BOUNDING and .B CAPNG_APPLY_STAGED_SUPP_GRP and .BR capng_stage_additional_groups (3) before using this flow. Callers that need the prepared bounding-set behavior must request it explicitly with .B CAPNG_APPLY_BOUNDING so older callers continue to see the historical .B capng_change_id behavior. Note: the only safe action to do upon failure of this function is to probably exit. This is because you are likely in a situation with partial permissions and not what you intended. .SH "SEE ALSO" .BR capng_update (3), .BR capng_apply (3), .BR capng_stage_additional_groups (3), .BR prctl (2), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_clear.3000066400000000000000000000012071524174023300213230ustar00rootroot00000000000000.TH "CAPNG_CLEAR" "3" "Sept 2020" "Red Hat" "Libcap-ng API" .SH NAME capng_clear \- clear chosen capabilities set .SH "SYNOPSIS" .B #include .sp void capng_clear(capng_select_t set); .SH "DESCRIPTION" capng_clear sets to 0 all bits in the selected POSIX capabilities set. The options are CAPNG_SELECT_CAPS for the traditional capabilities, CAPNG_SELECT_BOUNDS for the bounding set, CAPNG_SELECT_BOTH if clearing both is desired, CAPNG_SELECT_AMBIENT if only operating on the ambient capabilities, or CAPNG_SELECT_ALL if clearing all is desired. .SH "RETURN VALUE" None. .SH "SEE ALSO" .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_fill.3000066400000000000000000000013731524174023300211670ustar00rootroot00000000000000.TH "CAPNG_FILL" "3" "March 2026" "Red Hat" "Libcap-ng API" .SH NAME capng_fill \- fill chosen capabilities set .SH "SYNOPSIS" .B #include .sp void capng_fill(capng_select_t set); .SH "DESCRIPTION" capng_fill sets all bits to a 1 in the selected POSIX capabilities set. The options are CAPNG_SELECT_CAPS for the traditional capabilities, CAPNG_SELECT_BOUNDS for the bounding set, CAPNG_SELECT_BOTH if filling both is desired, CAPNG_SELECT_AMBIENT if only operating on the ambient capabilities, or CAPNG_SELECT_ALL if filling all is desired. .SH "RETURN VALUE" None. .SH NOTES During capng_apply, bits in the bounding set can only be dropped. You cannot set them. .SH "SEE ALSO" .BR capng_apply (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_get_caps_fd.3000066400000000000000000000015051524174023300224740ustar00rootroot00000000000000.TH "CAPNG_GET_CAPS_FD" "3" "March 2026" "Red Hat" "Libcap-ng API" .SH NAME capng_get_caps_fd \- read file-based capabilities from extended attributes .SH "SYNOPSIS" .B #include .sp int capng_get_caps_fd(int fd); .SH "DESCRIPTION" This function will read the file based capabilities stored in extended attributes of the file that the descriptor was opened against. The bounding set is not included in file based capabilities operations. Note that this function will only work if compiled on a kernel that supports file based capabilities such as 2.6.26 and later. If the "magic" bit is set, then all effective capability bits are set. Otherwise the bits are cleared. .SH "RETURN VALUE" This returns 0 on success and -1 on failure. .SH "SEE ALSO" .BR capng_apply_caps_fd (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_get_caps_process.3000066400000000000000000000020031524174023300235530ustar00rootroot00000000000000.TH "CAPNG_GET_CAPS_PROCESS" "3" "June 2009" "Red Hat" "Libcap-ng API" .SH NAME capng_get_caps_process \- get the capabilities from a process .SH "SYNOPSIS" .B #include .sp int capng_get_caps_process(void); .SH "DESCRIPTION" capng_get_caps_process will get the capabilities and bounding set of the pid stored inside libcap-ng's state table. The default is the pid of the running process. This can be changed by using the capng_setpid function. .SH "RETURN VALUE" This returns 0 on success and -1 on failure. .SH NOTES If you are doing multi-threaded programming, calling this function will only get capabilities on the calling thread. If you want to get overall capabilities for a multi-threaded process, you can only do that before creating any threads. Afterwards, threads may be able to independently set capabilities. capng_get_caps_process needs a mounted /proc to read the current bounding set, otherwise it will fail. .SH "SEE ALSO" .BR capng_setpid (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_get_rootid.3000066400000000000000000000011511524174023300223720ustar00rootroot00000000000000.TH "CAPNG_GET_ROOTID" "3" "Sept 2020" "Red Hat" "Libcap-ng API" .SH NAME capng_get_rootid \- get namespace root id .SH "SYNOPSIS" .B #include .sp uid_t capng_get_rootid(void); .SH "DESCRIPTION" capng_get_rootid gets the rootid for file capabilities operations. The value is a host-endian user ID in the caller's user namespace. .SH "RETURN VALUE" If V3 file capabilities have not been selected, or the kernel does not support them, it returns CAPNG_UNSET_ROOTID. Otherwise it returns the namespace root user ID. .SH "SEE ALSO" .BR capng_get_caps_fd (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_have_capabilities.3000066400000000000000000000027451524174023300237010ustar00rootroot00000000000000.TH "CAPNG_HAVE_CAPABILITIES" "3" "Sept 2020" "Red Hat" "Libcap-ng API" .SH NAME .nf capng_have_capabilities, capng_have_permitted_capabilities \- check for capabilities .SH "SYNOPSIS" .B #include .sp capng_results_t capng_have_capabilities(capng_select_t set); capng_results_t capng_have_permitted_capabilities(void); .SH "DESCRIPTION" capng_have_capabilities will check the selected internal capabilities sets to see what the status is. The capabilities sets must be previously setup with calls to capng_get_caps_process, capng_get_caps_fd, or in some other way setup. The options are CAPNG_SELECT_CAPS for the traditional capabilities, CAPNG_SELECT_BOUNDS for the bounding set, CAPNG_SELECT_BOTH if checking both are desired, CAPNG_SELECT_AMBIENT if only checking the ambient capabilities, or CAPNG_SELECT_ALL if testing all sets is desired. When capabilities are checked, it will only look at the effective capabilities. If, however, the source of capabilities comes from a file, then you may need to additionally check the permitted capabilities. It's for this reason that .B capng_have_permitted_capabilities was created. It takes no arguments because it simply checks the permitted set. .SH "RETURN VALUE" This function will return one of the following four self explanatory values: CAPNG_FAIL, CAPNG_NONE, CAPNG_PARTIAL, CAPNG_FULL. .SH "SEE ALSO" .BR capng_get_caps_process (3), .BR capng_get_caps_fd (3), .BR capng_have_capability (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_have_capability.3000066400000000000000000000015511524174023300233630ustar00rootroot00000000000000.TH "CAPNG_HAVE_CAPABILITY" "3" "June 2009" "Red Hat" "Libcap-ng API" .SH NAME capng_have_capability \- check for specific capability .SH "SYNOPSIS" .B #include .sp int capng_have_capability(capng_type_t which, unsigned int capability); .SH "DESCRIPTION" capng_have_capability will check the specified internal capabilities set to see if the specified capability is set. The capabilities sets must be previously setup with calls to capng_get_caps_process, capng_get_caps_fd, or in some other way setup. The values for which should be one of: CAPNG_EFFECTIVE, CAPNG_PERMITTED, CAPNG_INHERITABLE, CAPNG_BOUNDING_SET, or CAPNG_AMBIENT. .SH "RETURN VALUE" This function will return 1 if yes and 0 otherwise. .SH "SEE ALSO" .BR capng_get_caps_process (3), .BR capng_get_caps_fd (3), .BR capng_have_capabilities (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_lock.3000066400000000000000000000024261524174023300211710ustar00rootroot00000000000000.TH "CAPNG_LOCK" "3" "Mar 2026" "Red Hat" "Libcap-ng API" .SH NAME capng_lock \- lock the current process capabilities settings .SH "SYNOPSIS" .B #include .sp int capng_lock(void); .SH "DESCRIPTION" capng_lock will take steps to prevent children of the current process to regain full privileges if the uid is 0. This should be called while possessing the CAP_SETPCAP capability in the kernel. This function will do the following if permitted by the kernel: .IP \[bu] 2 Set the NOROOT option on for PR_SET_SECUREBITS. .IP \[bu] 2 Set the NOROOT_LOCKED option to on for PR_SET_SECUREBITS. .IP \[bu] 2 Set the PR_NO_SETUID_FIXUP option on for PR_SET_SECUREBITS. .IP \[bu] 2 Set the PR_NO_SETUID_FIXUP_LOCKED option on for PR_SET_SECUREBITS. .PP It does not enable PR_SET_KEEPCAPS or the KEEP_CAPS/KEEP_CAPS_LOCKED securebits. Existing securebits set by the caller are preserved, including securebits that were added after this version of libcap-ng was built. .SH "RETURN VALUE" This returns 0 on success and a negative number on failure. -1 means a failure reading or updating the securebits. -2 means failure setting PR_SET_NO_NEW_PRIVS. These are additive meaning -3 is a failure of both. .SH "SEE ALSO" .BR capng_apply (3), .BR prctl (2), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_name_to_capability.3000066400000000000000000000015731524174023300240660ustar00rootroot00000000000000.TH "CAPNG_NAME_TO_CAPABILITY" "3" "June 2009" "Red Hat" "Libcap-ng API" .SH NAME capng_name_to_capability \- convert capability text to integer .SH "SYNOPSIS" .B #include .sp int capng_name_to_capability(const char *name); .SH "DESCRIPTION" capng_name_to_capability will take the string being passed and look it up to see what its integer value would be. The string being input is the same name as the define in linux/capability.h with the CAP_ prefix removed. The string case does not matter. The integer that is output is the same as the define would be from linux/capability.h. This is useful for taking string input and converting to something that can be used with capng_update. .SH "RETURN VALUE" This returns a negative number on failure and the correct define otherwise. .SH "SEE ALSO" .BR capng_capability_to_name (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_print_caps_numeric.3000066400000000000000000000024011524174023300241160ustar00rootroot00000000000000.TH "CAPNG_PRINT_CAPS_NUMERIC" "3" "Sept 2020" "Red Hat" "Libcap-ng API" .SH NAME capng_print_caps_numeric \- print numeric values for capabilities set .SH "SYNOPSIS" .B #include .sp char *capng_print_caps_numeric(capng_print_t where, capng_select_t set); .SH "DESCRIPTION" capng_print_caps_numeric will create a numeric representation of the internal capabilities. The representation can be sent to either stdout or a buffer by passing CAPNG_PRINT_STDOUT or CAPNG_PRINT_BUFFER respectively for the where parameter. If the option was for a buffer, this function will malloc a buffer that the caller must free. The set parameter controls what is included in the representation. The legal options are CAPNG_SELECT_CAPS for the traditional capabilities, CAPNG_SELECT_BOUNDS for the bounding set, CAPNG_SELECT_BOTH if printing both is desired, CAPNG_SELECT_AMBIENT if only printing the ambient capabilities, or CAPNG_SELECT_ALL if printing all is desired. .SH "RETURN VALUE" If CAPNG_PRINT_BUFFER was selected for where, this will be the text buffer and NULL on failure. Don't forget to free it. If CAPNG_PRINT_STDOUT was selected then this value will be NULL no matter what. .SH "SEE ALSO" .BR capng_print_caps_text (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_print_caps_text.3000066400000000000000000000020731524174023300234450ustar00rootroot00000000000000.TH "CAPNG_PRINT_CAPS_TEXT" "3" "March 2026" "Red Hat" "Libcap-ng API" .SH NAME capng_print_caps_text \- print names of values for capabilities set .SH "SYNOPSIS" .B #include .sp char *capng_print_caps_text(capng_print_t where, capng_type_t which); .SH "DESCRIPTION" capng_print_caps_text will create a text string representation of the internal capability set specified. The representation can be sent to either stdout or a buffer by passing CAPNG_PRINT_STDOUT or CAPNG_PRINT_BUFFER respectively for the where parameter. If the option was for a buffer, this function will malloc a buffer that the caller must free. The legal values for the which parameter is CAPNG_EFFECTIVE, CAPNG_PERMITTED, CAPNG_INHERITABLE, CAPNG_BOUNDING_SET, or CAPNG_AMBIENT. .SH "RETURN VALUE" If CAPNG_PRINT_BUFFER was selected for where, this will be the text buffer and NULL on failure. Don't forget to free it. If CAPNG_PRINT_STDOUT was selected then this value will be NULL no matter what. .SH "SEE ALSO" .BR capng_print_caps_numeric (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_restore_state.3000066400000000000000000000014431524174023300231220ustar00rootroot00000000000000.TH "CAPNG_RESTORE_STATE" "3" "March 2026" "Red Hat" "Libcap-ng API" .SH NAME capng_restore_state \- set the internal library state .SH "SYNOPSIS" .B #include .sp void capng_restore_state(void **state); .SH "DESCRIPTION" capng_restore_state is a function that takes the state information previously saved by capng_save_state and restores the libraries internal state. This function is not available in the python bindings. .SH "RETURN VALUE" None. .SH NOTES capng_restore_state free's the previously malloc'd state, thus the state can't be restored multiple times. The working pid is part of the restored state, if restoring the state to a different thread, capng_setpid can be used to update it. .SH "SEE ALSO" .BR capng_save_state (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_save_state.3000066400000000000000000000015421524174023300223750ustar00rootroot00000000000000.TH "CAPNG_SAVE_STATE" "3" "June 2009" "Red Hat" "Libcap-ng API" .SH NAME capng_save_state \- get the internal library state .SH "SYNOPSIS" .B #include .sp void *capng_save_state(void); .SH "DESCRIPTION" capng_save_state is a function that returns a pointer to the internal state of the libcap-ng library. It should be considered opaque and not for alteration directly. This function should be used when you suspect a third party library may use libcap-ng also and want to make sure it doesn't alter something important. This function is not available in the python bindings. .SH "RETURN VALUE" This returns NULL on failure and a non-NULL pointer otherwise. .SH NOTES The structure returned by capng_save_state is malloc'd; it should be free'd if not used. .SH "SEE ALSO" .BR capng_restore_state (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_set_rootid.3000066400000000000000000000015461524174023300224160ustar00rootroot00000000000000.TH "CAPNG_SET_ROOTID" "3" "March 2026" "Red Hat" "Libcap-ng API" .SH NAME capng_set_rootid \- set namespace root id .SH "SYNOPSIS" .B #include .sp int capng_set_rootid(uid_t rootid); .SH "DESCRIPTION" capng_set_rootid sets the root user ID for file capabilities operations. The value is host-endian and relative to the caller's user namespace. Pass CAPNG_UNSET_ROOTID to clear a previously selected namespace root ID and go back to writing regular V2 file capabilities. Other values select V3 file capabilities; the kernel validates and maps the user ID when the capabilities are written. .SH "RETURN VALUE" On success, it returns 0. It returns -1 if there is an internal error or the kernel headers used to build the library do not support V3 file capabilities. .SH "SEE ALSO" .BR capng_apply_caps_fd (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_setpid.3000066400000000000000000000017631524174023300215340ustar00rootroot00000000000000.TH "CAPNG_SETPID" "3" "March 2026" "Red Hat" "Libcap-ng API" .SH NAME capng_setpid \- set working pid .SH "SYNOPSIS" .B #include .sp void capng_setpid(int pid); .SH "DESCRIPTION" capng_setpid sets the working pid for capabilities operations. This is useful if you want to get the capabilities of a different process. .SH NOTES If your process calls .B fork , then the child process will still have the pid of the parent process stored in libcap-ng's internal data. It is disallowed to do any kind of setcap operations because you would be crossing process boundaries. To correct this, if your program links against pthreads, then libcap-ng will use the .B pthread_atfork function (as a weak symbol) to reset the pid information to the new process automatically. You are not required to link against pthreads. You can call .B capng_setpid and adjust the stored pid manually. .SH "RETURN VALUE" None. .SH "SEE ALSO" .BR capng_get_caps_process (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_stage_additional_groups.3000066400000000000000000000024161524174023300251320ustar00rootroot00000000000000.TH "CAPNG_STAGE_ADDITIONAL_GROUPS" "3" "Mar 2026" "Red Hat" \ "Libcap-ng API" .SH NAME capng_stage_additional_groups \- stage additional gids for capng_change_id .SH "SYNOPSIS" .B #include .sp int capng_stage_additional_groups(const gid_t *gids, size_t count); .SH "DESCRIPTION" This function stages an additional gid list for the next .BR capng_change_id (3) call that uses .BR CAPNG_APPLY_STAGED_GROUPS . The gids are copied into libcap-ng internal state. Once .B capng_change_id returns, libcap-ng clears the staged gids whether they were applied, ignored because the flag was not set, or discarded because the call failed. If staged gids remain unused, libcap-ng also clears them when the thread-local state is deinitialized. Passing a .I count of 0 clears any currently staged gids. Normally, you should stick to using the supplemantal groups that are associated with the uid's account that is being changed to. This is provided to simulate the gid model that systemd uses. This interface is available in libcap-ng 0.9.3 and later. .SH "RETURN VALUE" This returns 0 on success and -1 on failure. A non-zero .I count with a NULL .I gids pointer fails with .B errno set to .BR EINVAL . .SH "SEE ALSO" .BR capng_change_id (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_update.3000066400000000000000000000017441524174023300215250ustar00rootroot00000000000000.TH "CAPNG_UPDATE" "3" "June 2009" "Red Hat" "Libcap-ng API" .SH NAME capng_update \- update the stored capabilities settings .SH "SYNOPSIS" .B #include .sp int capng_update(capng_act_t action, capng_type_t type,unsigned int capability); .SH "DESCRIPTION" capng_update will update the internal POSIX capabilities settings based on the options passed to it. The action should be either CAPNG_DROP to set the capability bit to 0, or CAPNG_ADD to set the capability bit to 1. The operation is performed on the capability set specified in the type parameter. The values are: CAPNG_EFFECTIVE, CAPNG_PERMITTED, CAPNG_INHERITABLE, CAPNG_BOUNDING_SET, or CAPNG_AMBIENT. The values may be or'ed together to perform the same operation on multiple sets. The last parameter, capability, is the capability define as given in linux/capability.h. .SH "RETURN VALUE" This returns 0 on success and -1 on failure. .SH "SEE ALSO" .BR capng_updatev (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/capng_updatev.3000066400000000000000000000021621524174023300217060ustar00rootroot00000000000000.TH "CAPNG_UPDATEV" "3" "June 2009" "Red Hat" "Libcap-ng API" .SH NAME capng_updatev \- update the stored capabilities settings .SH "SYNOPSIS" .B #include .sp int capng_updatev(capng_act_t action, capng_type_t type, unsigned int capability, ...); .SH "DESCRIPTION" capng_updatev will update the internal POSIX capabilities settings based on the options passed to it. The action should be either CAPNG_DROP to set the capability bit to 0, or CAPNG_ADD to set the capability bit to 1. The operation is performed on the capability set specified in the type parameter. The values are: CAPNG_EFFECTIVE, CAPNG_PERMITTED, CAPNG_INHERITABLE, CAPNG_BOUNDING_SET, or CAPNG_AMBIENT. The values may be or'ed together to perform the same operation on multiple sets. The last parameter, capability, is the capability define as given in linux/capability.h. This function differs from capng_update in that you may pass a list of capabilities. This list must be terminated with a -1 value. .SH "RETURN VALUE" This returns 0 on success and -1 on failure. .SH "SEE ALSO" .BR capng_update (3), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/docs/check-manpages.sh000077500000000000000000000032761524174023300222160ustar00rootroot00000000000000#!/bin/sh # # Check manual page sources with groff warnings enabled. Under make check, this # also covers the utility man pages outside docs. set -u : "${MAN:=man}" : "${srcdir:=.}" if test "$srcdir" = "."; then case "$0" in */*) srcdir=${0%/*} ;; esac fi if ! command -v "$MAN" >/dev/null 2>&1; then echo "SKIP: man command not found" exit 77 fi if command -v locale >/dev/null 2>&1; then if ! LC_ALL=C.UTF-8 locale charmap >/dev/null 2>&1; then echo "SKIP: C.UTF-8 locale not available" exit 77 fi fi if ! "$MAN" --help 2>&1 | grep -- "--warnings" >/dev/null 2>&1; then echo "SKIP: man command does not support --warnings" exit 77 fi failed=0 found=0 if test "${MANPAGES+x}" = x; then explicit_pages=1 set -- $MANPAGES else explicit_pages=0 set -- "$srcdir"/*.[0-9] if test "${top_srcdir+x}" = x; then set -- "$@" \ "$top_srcdir"/utils/pscap.8 \ "$top_srcdir"/utils/filecap.8 \ "$top_srcdir"/utils/captest.8 \ "$top_srcdir"/utils/cap-audit/cap-audit.8 fi if test "${top_builddir+x}" = x; then set -- "$@" "$top_builddir"/utils/netcap.8 fi fi for page do if ! test -e "$page"; then case "$page" in *\**) test $explicit_pages -eq 1 || continue ;; esac failed=1 echo "$page: not found" continue fi found=1 output=$( LC_ALL=C.UTF-8 MANROFFSEQ='' MANWIDTH=80 \ "$MAN" --warnings -E UTF-8 -l -Tutf8 -Z "$page" \ 2>&1 >/dev/null ) status=$? if test $status -ne 0 || test -n "$output"; then failed=1 echo "$page:" if test -n "$output"; then printf '%s\n' "$output" fi if test $status -ne 0; then echo "man exited with status $status" fi fi done if test $found -eq 0; then echo "No manual pages found" exit 1 fi exit $failed stevegrubb-libcap-ng-9ae4440/docs/libdrop_ambient.7000066400000000000000000000011241524174023300222210ustar00rootroot00000000000000.TH "LIBDROP_AMBIENT" "7" "March 2026" "Red Hat" "Libcap-ng API" .SH NAME libdrop_ambient \- force application started with ambient capabilities to drop them .SH "DESCRIPTION" This library can be used via LD_PRELOAD to force an application started with ambient capabilities to drop them. It leaves other capabilities intact. This can also be linked against and automatically does the right thing. You do not need to make any calls into the library because all the work is done in the constructor which runs before main() is called. .SH "SEE ALSO" .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/libcap-ng.spec000066400000000000000000000100631524174023300205610ustar00rootroot00000000000000%global bpf_supported_arches aarch64 x86_64 ppc64le riscv64 s390x Summary: An alternate POSIX capabilities library Name: libcap-ng Version: 0.9.5 Release: 1%{?dist} License: LGPL-2.0-or-later URL: https://github.com/stevegrubb/libcap-ng Source0: %{name}-%{version}.tar.gz BuildRequires: gcc make BuildRequires: autoconf automake libtool BuildRequires: kernel-headers >= 2.6.11 BuildRequires: libattr-devel %ifarch %{bpf_supported_arches} # These next ones are only if --enable-cap-audit is configured BuildRequires: clang BuildRequires: bpftool libbpf-devel BuildRequires: audit-libs-devel BuildRequires: kernel-devel %endif %description Libcap-ng is a library that makes using POSIX capabilities easier %package devel Summary: Header files for libcap-ng library License: LGPL-2.0-or-later Requires: kernel-headers >= 2.6.11 Requires: %{name} = %{version}-%{release} Requires: pkgconfig %description devel The libcap-ng-devel package contains the files needed for developing applications that need to use the libcap-ng library. %package python3 Summary: Python3 bindings for libcap-ng library License: LGPL-2.0-or-later BuildRequires: python3-devel swig Requires: %{name} = %{version}-%{release} %description python3 The libcap-ng-python3 package contains the bindings so that libcap-ng and can be used by python3 applications. %package utils Summary: Utilities for analyzing and setting file capabilities License: GPL-2.0-or-later Requires: %{name} = %{version}-%{release} %ifarch %{bpf_supported_arches} Provides: %{name}-audit Obsoletes: %{name}-audit < %{version}-%{release} %endif %description utils The libcap-ng-utils package contains applications to analyze the POSIX capabilities of all the program running on a system. It also lets you set the file system based capabilities, and use cap-audit to determine the necessary capabilities for a program. %prep %setup -q touch -d @${SOURCE_DATE_EPOCH:?} NEWS %build # Locate suitable vmlinux.h. In normal builds under mock, # there'll be just one. But in case multiple kernel-devel packages # are installed, sort alphabetically and pick the last version. vmlinux_h="$(ls -1 /usr/src/kernels/*/vmlinux.h | sort -g | tail -n 1)" autoreconf -fv --install %configure --libdir=%{_libdir} \ %ifarch %{bpf_supported_arches} --enable-cap-audit=yes \ --with-vmlinux-h-path="${vmlinux_h}" \ --with-vmlinux-h=provided \ %endif --with-python3 make CFLAGS="%{optflags}" %{?_smp_mflags} %install %make_install # Remove a couple things so they don't get picked up rm -f $RPM_BUILD_ROOT/%{_libdir}/libcap-ng.la rm -f $RPM_BUILD_ROOT/%{_libdir}/libcap-ng.a rm -f $RPM_BUILD_ROOT/%{_libdir}/libdrop_ambient.la rm -f $RPM_BUILD_ROOT/%{_libdir}/libdrop_ambient.a rm -f $RPM_BUILD_ROOT/%{_libdir}/python%{python3_version}/site-packages/_capng.a rm -f $RPM_BUILD_ROOT/%{_libdir}/python%{python3_version}/site-packages/_capng.la %check make check %ldconfig_scriptlets %files %doc COPYING.LIB /%{_libdir}/libcap-ng.so.* /%{_libdir}/libdrop_ambient.so.* %attr(0644,root,root) %{_mandir}/man7/* %files devel %attr(0644,root,root) %{_mandir}/man3/* %attr(0644,root,root) %{_includedir}/cap-ng.h %{_libdir}/libcap-ng.so %{_libdir}/libdrop_ambient.so %attr(0644,root,root) %{_datadir}/aclocal/cap-ng.m4 %{_libdir}/pkgconfig/libcap-ng.pc %files python3 %attr(755,root,root) %{python3_sitearch}/* %files utils %doc COPYING %attr(0755,root,root) %{_bindir}/filecap %attr(0755,root,root) %{_bindir}/netcap %attr(0755,root,root) %{_bindir}/pscap %attr(0644,root,root) %{_mandir}/man8/filecap.8.gz %attr(0644,root,root) %{_mandir}/man8/netcap.8.gz %attr(0644,root,root) %{_mandir}/man8/pscap.8.gz %attr(0644,root,root) %{_datadir}/bash-completion/completions/libcap-ng.bash_completion %{_datadir}/bash-completion/completions/cap-audit %{_datadir}/bash-completion/completions/filecap %{_datadir}/bash-completion/completions/netcap %{_datadir}/bash-completion/completions/pscap %ifarch %{bpf_supported_arches} %attr(0755,root,root) %{_bindir}/cap-audit %attr(0644,root,root) %{_mandir}/man8/cap-audit.8.gz %endif %changelog * Thu Aug 20 2026 Steve Grubb 0.9.5-1 - New upstream release stevegrubb-libcap-ng-9ae4440/m4/000077500000000000000000000000001524174023300163715ustar00rootroot00000000000000stevegrubb-libcap-ng-9ae4440/m4/Makefile.am000066400000000000000000000017161524174023300204320ustar00rootroot00000000000000# Makefile.am -- # Copyright 2009 Red Hat Inc., Durham, North Carolina. # 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 = *.loT *.rej *.orig m4datadir = $(datadir)/aclocal dist_m4data_DATA = cap-ng.m4 stevegrubb-libcap-ng-9ae4440/m4/cap-ng.m4000066400000000000000000000023501524174023300200000ustar00rootroot00000000000000# libcap-ng.m4 - Checks for the libcap-ng support # Copyright (c) 2009 Steve Grubb sgrubb@redhat.com # AC_DEFUN([LIBCAP_NG_PATH], [ AC_ARG_WITH(libcap-ng, [ --with-libcap-ng=[auto/yes/no] Add Libcap-ng support [default=auto]],, with_libcap_ng=auto) # Check for Libcap-ng API # # libcap-ng detection if test x$with_libcap_ng = xno ; then have_libcap_ng=no; else # Start by checking for header file AC_CHECK_HEADER(cap-ng.h, capng_headers=yes, capng_headers=no) # See if we have libcap-ng library AC_CHECK_LIB(cap-ng, capng_clear, CAPNG_LDADD=-lcap-ng,) # Check results are usable if test x$with_libcap_ng = xyes -a x$CAPNG_LDADD = x ; then AC_MSG_ERROR(libcap-ng support was requested and the library was not found) fi if test x$CAPNG_LDADD != x -a $capng_headers = no ; then AC_MSG_ERROR(libcap-ng libraries found but headers are missing) fi fi AC_SUBST(CAPNG_LDADD) AC_MSG_CHECKING(whether to use libcap-ng) if test x$CAPNG_LDADD != x ; then AC_DEFINE(HAVE_LIBCAP_NG,1,[libcap-ng support]) AC_CHECK_DECLS([CAPNG_AMBIENT], [], [], [[#include ]]) AC_MSG_RESULT(yes) else AC_MSG_RESULT(no) fi ]) stevegrubb-libcap-ng-9ae4440/src/000077500000000000000000000000001524174023300166405ustar00rootroot00000000000000stevegrubb-libcap-ng-9ae4440/src/Makefile.am000066400000000000000000000030451524174023300206760ustar00rootroot00000000000000# Makefile.am -- # Copyright 2009,2015,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 program; see the file COPYING.LIB. If not, write to the # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor # Boston, MA 02110-1335, USA. # # Authors: # Steve Grubb # SUBDIRS = test pkgconfigdir = $(libdir)/pkgconfig pkgconfig_DATA = libcap-ng.pc DISTCLEANFILES = $(pkgconfig_DATA) CLEANFILES = $(BUILT_SOURCES) CONFIG_CLEAN_FILES = *.loT *.rej *.orig AM_CFLAGS = -fPIC -DPIC -W -Wall -Wshadow ${WFLAGS} -Wundef -D_GNU_SOURCE AM_CPPFLAGS = -I. -I${top_srcdir} lib_LTLIBRARIES = libcap-ng.la if BUILD_LIBDROP_AMBIENT lib_LTLIBRARIES += libdrop_ambient.la endif include_HEADERS = cap-ng.h noinst_HEADERS = captab.h libcap_ng_la_SOURCES = cap-ng.c lookup_table.c libcap_ng_la_LIBADD = libcap_ng_la_DEPENDENCIES = $(libcap_ng_la_SOURCES) ../config.h libcap_ng_la_LDFLAGS = -Wl,-z,relro -Wl,-z,nodelete libdrop_ambient_la_SOURCES = libdrop_ambient.c stevegrubb-libcap-ng-9ae4440/src/cap-ng.c000066400000000000000000001232461524174023300201610ustar00rootroot00000000000000/* libcap-ng.c -- * Copyright 2009-10, 2013, 2017, 2020-26 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 program; see the file COPYING.LIB. 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 "cap-ng.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_PTHREAD_H #include // For pthread_atfork #endif #ifdef HAVE_SYSCALL_H #include #endif #ifdef HAVE_LINUX_SECUREBITS_H #include #endif #ifdef HAVE_LINUX_MAGIC_H #include #include #endif # define hidden __attribute__ ((visibility ("hidden"))) unsigned int last_cap hidden = 0; /* * Some milestones of when things became available: * 2.6.24 kernel XATTR_NAME_CAPS * 2.6.25 kernel PR_CAPBSET_DROP, CAPABILITY_VERSION_2 * 2.6.26 kernel PR_SET_SECUREBITS, SECURE_*_LOCKED, VERSION_3 * 3.5 kernel PR_SET_NO_NEW_PRIVS * 4.3 kernel PR_CAP_AMBIENT * 4.14 kernel VFS_CAP_REVISION_3 */ #ifdef PR_CAPBSET_DROP static int HAVE_PR_CAPBSET_DROP = 0; #endif #ifdef PR_CAP_AMBIENT static int HAVE_PR_CAP_AMBIENT = 0; #endif /* External syscall prototypes */ extern int capset(cap_user_header_t header, cap_user_data_t data); extern int capget(cap_user_header_t header, const cap_user_data_t data); // Local functions static void update_bounding_set(capng_act_t action, unsigned int capability, unsigned int idx); static void update_ambient_set(capng_act_t action, unsigned int capability, unsigned int idx); // Local defines #define MASK(x) (1U << (x)) #ifdef PR_CAPBSET_DROP #define UPPER_MASK ~((~0U)<<(last_cap-31)) #else // For v1 systems UPPER_MASK will never be used #define UPPER_MASK (unsigned)(~0U) #endif // Re-define cap_valid so its uniform between V1 and V3 #undef cap_valid #define cap_valid(x) ((x) <= last_cap) // If we don't have the xattr library, then we can't // compile-in file system capabilities #if !defined(HAVE_ATTR_XATTR_H) && !defined (HAVE_SYS_XATTR_H) #undef VFS_CAP_U32 #endif #ifdef VFS_CAP_U32 #ifdef HAVE_SYS_XATTR_H #include #else #ifdef HAVE_ATTR_XATTR_H #include #endif #endif #if __BYTE_ORDER == __BIG_ENDIAN #define FIXUP(x) bswap_32(x) #else #define FIXUP(x) (x) #endif #endif #ifndef _LINUX_CAPABILITY_VERSION_1 #define _LINUX_CAPABILITY_VERSION_1 0x19980330 #endif #ifndef _LINUX_CAPABILITY_VERSION_2 #define _LINUX_CAPABILITY_VERSION_2 0x20071026 #endif #ifndef _LINUX_CAPABILITY_VERSION_3 #define _LINUX_CAPABILITY_VERSION_3 0x20080522 #endif // This public API went private in the 2.6.36 kernel - hope it never changes #ifndef XATTR_CAPS_SUFFIX #define XATTR_CAPS_SUFFIX "capability" #endif #ifndef XATTR_SECURITY_PREFIX #define XATTR_SECURITY_PREFIX "security." #endif #ifndef XATTR_NAME_CAPS #define XATTR_NAME_CAPS XATTR_SECURITY_PREFIX XATTR_CAPS_SUFFIX #endif /* Child processes can't get caps back */ #ifndef SECURE_NOROOT #define SECURE_NOROOT 0 #endif #ifndef SECURE_NOROOT_LOCKED #define SECURE_NOROOT_LOCKED 1 /* make bit-0 immutable */ #endif /* Setuid apps run by uid 0 don't get caps back */ #ifndef SECURE_NO_SETUID_FIXUP #define SECURE_NO_SETUID_FIXUP 2 #endif #ifndef SECURE_NO_SETUID_FIXUP_LOCKED #define SECURE_NO_SETUID_FIXUP_LOCKED 3 /* make bit-2 immutable */ #endif #ifndef VFS_CAP_U32 #define VFS_CAP_U32 2 #endif #if (VFS_CAP_U32 != 2) #error VFS_CAP_U32 does not match the library, you need a new version #endif // States: new, allocated, initted, updated, applied typedef enum { CAPNG_NEW, CAPNG_ERROR, CAPNG_ALLOCATED, CAPNG_INIT, CAPNG_UPDATED, CAPNG_APPLIED } capng_states_t; // Create an easy data struct out of the kernel definitions typedef union { struct __user_cap_data_struct v1; struct __user_cap_data_struct v3[VFS_CAP_U32]; } cap_data_t; // This struct keeps all state info struct cap_ng { int cap_ver; int vfs_cap_ver; struct __user_cap_header_struct hdr; cap_data_t data; capng_states_t state; uid_t rootid; // Host endian; only the V3 xattr field is little endian __u32 bounds[VFS_CAP_U32]; __u32 ambient[VFS_CAP_U32]; unsigned char bounds_state_changed; gid_t *add_groups; size_t add_group_cnt; }; // Global variables with per thread uniqueness static __thread struct cap_ng m = { 1, 1, {0, 0}, { {0, 0, 0} }, CAPNG_NEW, CAPNG_UNSET_ROOTID, {0, 0}, {0, 0}, 0, NULL, 0 }; static void clear_staged_additional_groups(struct cap_ng *c) { free(c->add_groups); c->add_groups = NULL; c->add_group_cnt = 0; } static inline void mark_bounding_set_changed(struct cap_ng *c) { c->bounds_state_changed = 1; } static inline void clear_bounding_set_changed(struct cap_ng *c) { c->bounds_state_changed = 0; } static int copy_staged_additional_groups(struct cap_ng *dst, const struct cap_ng *src) { size_t len; dst->add_groups = NULL; dst->add_group_cnt = 0; if (src->add_group_cnt == 0) return 0; len = src->add_group_cnt * sizeof(gid_t); dst->add_groups = malloc(len); if (dst->add_groups == NULL) return -1; memcpy(dst->add_groups, src->add_groups, len); dst->add_group_cnt = src->add_group_cnt; return 0; } static int gid_in_list(const gid_t *gids, size_t count, gid_t gid) { size_t i; for (i = 0; i < count; i++) { if (gids[i] == gid) return 1; } return 0; } static int get_additional_groups(const struct passwd *pw, gid_t gid, gid_t **gids, size_t *count) { gid_t *list; int ngroups = 1; int rc; *gids = NULL; *count = 0; list = malloc(sizeof(gid_t)); if (list == NULL) return -1; rc = getgrouplist(pw->pw_name, gid, list, &ngroups); if (rc == -1) { gid_t *tmp; tmp = realloc(list, sizeof(gid_t) * ngroups); if (tmp == NULL) { free(list); return -1; } list = tmp; rc = getgrouplist(pw->pw_name, gid, list, &ngroups); } if (rc == -1) { free(list); return -1; } *gids = list; *count = ngroups; return 0; } static int merge_additional_groups(const gid_t *base, size_t base_cnt, const gid_t *extra, size_t extra_cnt, gid_t **merged, size_t *merged_cnt) { gid_t *list; size_t i, count = 0, total = base_cnt + extra_cnt; *merged = NULL; *merged_cnt = 0; if (total == 0) return 0; list = malloc(sizeof(gid_t) * total); if (list == NULL) return -1; for (i = 0; i < base_cnt; i++) { if (gid_in_list(list, count, base[i]) == 0) list[count++] = base[i]; } for (i = 0; i < extra_cnt; i++) { if (gid_in_list(list, count, extra[i]) == 0) list[count++] = extra[i]; } *merged = list; *merged_cnt = count; return 0; } /* * Reset the state so that init gets called to erase everything */ static void deinit(void) { clear_staged_additional_groups(&m); m.state = CAPNG_NEW; } static inline int test_cap(unsigned int cap) { // prctl returns 0 or 1 for valid caps, -1 otherwise return prctl(PR_CAPBSET_READ, cap) >= 0; } // The capability storage is fixed by VFS_CAP_U32 #define MAX_CAP_BITS (VFS_CAP_U32 * sizeof(__le32) * 8) #define MAX_CAP_VALUE (MAX_CAP_BITS - 1) static void init_lib(void) __attribute__ ((constructor)); static void init_lib(void) { /* This is so that dynamic or static libraries don't re-init */ static unsigned int run_once; if (__atomic_load_n(&run_once, __ATOMIC_ACQUIRE) == 2) return; if (!__sync_bool_compare_and_swap(&run_once, 0, 1)) { while (__atomic_load_n(&run_once, __ATOMIC_ACQUIRE) != 2) ; return; } #ifdef HAVE_PTHREAD_H pthread_atfork(NULL, NULL, deinit); #endif // Detect last cap if (last_cap == 0) { int fd; // Try to read last cap from procfs fd = open("/proc/sys/kernel/cap_last_cap", O_RDONLY); if (fd >= 0) { #ifdef HAVE_LINUX_MAGIC_H struct statfs st; // Bail out if procfs is invalid or fstatfs fails if (fstatfs(fd, &st) || st.f_type != PROC_SUPER_MAGIC) goto fail; #endif char buf[8]; int num = read(fd, buf, sizeof(buf) - 1); if (num > 0) { buf[num] = 0; errno = 0; unsigned int val = strtoul(buf, NULL, 10); if (errno == 0) last_cap = val; } fail: close(fd); } if (last_cap >= MAX_CAP_BITS) last_cap = MAX_CAP_VALUE; // Run a binary search over capabilities if (last_cap == 0) { // starting with last_cap=MAX_CAP_BITS means we always know // that cap1 is invalid after the first iteration last_cap = MAX_CAP_BITS; unsigned int cap0 = 0, cap1 = MAX_CAP_BITS; while (cap0 < last_cap) { if (test_cap(last_cap)) cap0 = last_cap; else cap1 = last_cap; last_cap = (cap0 + cap1) / 2U; } } } // Detect prctl options at runtime #ifdef PR_CAPBSET_DROP errno = 0; prctl(PR_CAPBSET_READ, 0, 0, 0, 0); if (errno != EINVAL) HAVE_PR_CAPBSET_DROP = 1; #endif #ifdef PR_CAP_AMBIENT errno = 0; prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_IS_SET, CAP_CHOWN, 0, 0); if (errno != EINVAL) HAVE_PR_CAP_AMBIENT = 1; #endif __atomic_store_n(&run_once, 2, __ATOMIC_RELEASE); } static void init(void) { // This is so static libs get initialized init_lib(); if (m.state != CAPNG_NEW) return; memset(&m.hdr, 0, sizeof(m.hdr)); (void)capget(&m.hdr, NULL); // Returns -EINVAL if (m.hdr.version == _LINUX_CAPABILITY_VERSION_3 || m.hdr.version == _LINUX_CAPABILITY_VERSION_2) { m.cap_ver = 3; } else if (m.hdr.version == _LINUX_CAPABILITY_VERSION_1) { m.cap_ver = 1; } else { m.state = CAPNG_ERROR; return; } #if VFS_CAP_REVISION == VFS_CAP_REVISION_1 m.vfs_cap_ver = 1; #else /* * V2 is the default even when V3 is available. The kernel promotes a * V2 write from a user namespace to V3 and records the namespace root. */ m.vfs_cap_ver = 2; #endif memset(&m.data, 0, sizeof(cap_data_t)); #ifdef HAVE_SYSCALL_H m.hdr.pid = (unsigned)syscall(__NR_gettid); #else m.hdr.pid = (unsigned)getpid(); #endif m.rootid = CAPNG_UNSET_ROOTID; m.state = CAPNG_ALLOCATED; } void capng_clear(capng_select_t set) { if (m.state == CAPNG_NEW) init(); if (m.state == CAPNG_ERROR) return; if (set & CAPNG_SELECT_CAPS) memset(&m.data, 0, sizeof(cap_data_t)); #ifdef PR_CAPBSET_DROP if (HAVE_PR_CAPBSET_DROP) { if (set & CAPNG_SELECT_BOUNDS) { memset(m.bounds, 0, sizeof(m.bounds)); mark_bounding_set_changed(&m); } } #endif #ifdef PR_CAP_AMBIENT if (HAVE_PR_CAP_AMBIENT) { if (set & CAPNG_SELECT_AMBIENT) memset(m.ambient, 0, sizeof(m.ambient)); } #endif m.state = CAPNG_INIT; } void capng_fill(capng_select_t set) { if (m.state == CAPNG_NEW) init(); if (m.state == CAPNG_ERROR) return; if (set & CAPNG_SELECT_CAPS) { if (m.cap_ver == 1) { m.data.v1.effective = 0x7FFFFFFFU; m.data.v1.permitted = 0x7FFFFFFFU; m.data.v1.inheritable = 0; } else { m.data.v3[0].effective = 0xFFFFFFFFU; m.data.v3[0].permitted = 0xFFFFFFFFU; m.data.v3[0].inheritable = 0; m.data.v3[1].effective = 0xFFFFFFFFU; m.data.v3[1].permitted = 0xFFFFFFFFU; m.data.v3[1].inheritable = 0; } } #ifdef PR_CAPBSET_DROP if (HAVE_PR_CAPBSET_DROP) { if (set & CAPNG_SELECT_BOUNDS) { unsigned i; for (i=0; i NGROUPS_MAX) { errno = EINVAL; return -1; } if (count) { size_t len = count * sizeof(gid_t); list = malloc(len); if (list == NULL) return -1; memcpy(list, gids, len); } clear_staged_additional_groups(&m); m.add_groups = list; m.add_group_cnt = count; return 0; } #ifdef PR_CAPBSET_DROP static int get_bounding_set(void) { char buf[64]; FILE *f; int rc; snprintf(buf, sizeof(buf), "/proc/%d/status", m.hdr.pid ? m.hdr.pid : #ifdef HAVE_SYSCALL_H (int)syscall(__NR_gettid)); #else (int)getpid()); #endif f = fopen(buf, "re"); if (f) { __fsetlocking(f, FSETLOCKING_BYCALLER); while (fgets(buf, sizeof(buf), f)) { if (strncmp(buf, "CapB", 4)) continue; int num = sscanf(buf, "CapBnd: %08x%08x", &m.bounds[1], &m.bounds[0]); fclose(f); if (num != 2) return -1; clear_bounding_set_changed(&m); return 0; } // Didn't find bounding set, fall through and try prctl way fclose(f); } // Might be in a container with no procfs - do it the hard way memset(m.bounds, 0, sizeof(m.bounds)); unsigned int i = 0; do { rc = prctl(PR_CAPBSET_READ, i, 0, 0, 0); if (rc < 0) return -1; // Just add set bits if (rc) update_bounding_set(CAPNG_ADD, i%32, i>>5); i++; } while (cap_valid(i)); clear_bounding_set_changed(&m); return 0; } #endif #ifdef PR_CAP_AMBIENT static int get_ambient_set(void) { char buf[64]; FILE *f; int rc; snprintf(buf, sizeof(buf), "/proc/%d/status", m.hdr.pid ? m.hdr.pid : #ifdef HAVE_SYSCALL_H (int)syscall(__NR_gettid)); #else (int)getpid()); #endif f = fopen(buf, "re"); if (f) { __fsetlocking(f, FSETLOCKING_BYCALLER); while (fgets(buf, sizeof(buf), f)) { if (strncmp(buf, "CapA", 4)) continue; int num = sscanf(buf, "CapAmb: %08x%08x", &m.ambient[1], &m.ambient[0]); fclose(f); if (num != 2) return -1; return 0; } fclose(f); // Didn't find ambient set, fall through and try prctl way } // Might be in a container with no procfs - do it the hard way memset(m.ambient, 0, sizeof(m.ambient)); unsigned int i = 0; do { rc = prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_IS_SET, i, 0, 0); if (rc < 0) return -1; // Just add set bits if (rc) update_ambient_set(CAPNG_ADD, i%32, i>>5); i++; } while (cap_valid(i)); return 0; } #endif /* * Returns 0 on success and -1 on failure */ int capng_get_caps_process(void) { int rc; if (m.state == CAPNG_NEW) init(); if (m.state == CAPNG_ERROR) return -1; rc = capget((cap_user_header_t)&m.hdr, (cap_user_data_t)&m.data); if (rc == 0) { m.state = CAPNG_INIT; #ifdef PR_CAPBSET_DROP if (HAVE_PR_CAPBSET_DROP) { rc = get_bounding_set(); if (rc < 0) m.state = CAPNG_ERROR; } #endif #ifdef PR_CAP_AMBIENT if (HAVE_PR_CAP_AMBIENT) { rc = get_ambient_set(); if (rc < 0) m.state = CAPNG_ERROR; } #endif } return rc; } #ifdef VFS_CAP_U32 #ifdef VFS_CAP_REVISION_3 static int load_data(const struct vfs_ns_cap_data *filedata, int size) #else static int load_data(const struct vfs_cap_data *filedata, int size) #endif { unsigned int i, magic, words; if (m.cap_ver == 1) return -1; // Should never get here but just in case magic = FIXUP(filedata->magic_etc); switch (magic & VFS_CAP_REVISION_MASK) { case VFS_CAP_REVISION_1: m.vfs_cap_ver = 1; if (size != XATTR_CAPS_SZ_1) return -1; words = 1; break; case VFS_CAP_REVISION_2: m.vfs_cap_ver = 2; if (size != XATTR_CAPS_SZ_2) return -1; words = VFS_CAP_U32; break; #ifdef VFS_CAP_REVISION_3 case VFS_CAP_REVISION_3: m.vfs_cap_ver = 3; if (size != XATTR_CAPS_SZ_3) return -1; words = VFS_CAP_U32; break; #endif default: return -1; } /* * Revision 1 contains only data[0]. Clear fields omitted by older * formats before copying the number of words validated above. */ memset(&m.data, 0, sizeof(m.data)); m.rootid = CAPNG_UNSET_ROOTID; for (i = 0; i < words; i++) { m.data.v3[i].permitted = FIXUP(filedata->data[i].permitted); m.data.v3[i].inheritable = FIXUP(filedata->data[i].inheritable); if (magic & VFS_CAP_FLAGS_EFFECTIVE) m.data.v3[i].effective = m.data.v3[i].permitted | m.data.v3[i].inheritable; } #ifdef VFS_CAP_REVISION_3 if (size == XATTR_CAPS_SZ_3) { struct vfs_ns_cap_data *d = (struct vfs_ns_cap_data *)filedata; m.rootid = FIXUP(d->rootid); } #endif return 0; } #endif int capng_get_caps_fd(int fd) { #ifndef VFS_CAP_U32 return -1; #else int rc; #ifdef VFS_CAP_REVISION_3 struct vfs_ns_cap_data filedata = { 0 }; #else struct vfs_cap_data filedata = { 0 }; #endif if (m.state == CAPNG_NEW) init(); if (m.state == CAPNG_ERROR) return -1; rc = fgetxattr(fd, XATTR_NAME_CAPS, &filedata, sizeof(filedata)); if (rc <= 0) return -1; rc = load_data(&filedata, rc); if (rc == 0) m.state = CAPNG_INIT; else m.state = CAPNG_ERROR; // If load data failed, malformed data return rc; #endif } static void v1_update(capng_act_t action, unsigned int capability, __u32 *data) { if (action == CAPNG_ADD) *data |= MASK(capability); else *data &= ~(MASK(capability)); } static void update_effective(capng_act_t action, unsigned int capability, unsigned int idx) { if (action == CAPNG_ADD) m.data.v3[idx].effective |= MASK(capability); else m.data.v3[idx].effective &= ~(MASK(capability)); } static void update_permitted(capng_act_t action, unsigned int capability, unsigned int idx) { if (action == CAPNG_ADD) m.data.v3[idx].permitted |= MASK(capability); else m.data.v3[idx].permitted &= ~(MASK(capability)); } static void update_inheritable(capng_act_t action, unsigned int capability, unsigned int idx) { if (action == CAPNG_ADD) m.data.v3[idx].inheritable |= MASK(capability); else m.data.v3[idx].inheritable &= ~(MASK(capability)); } static void update_bounding_set(capng_act_t action, unsigned int capability, unsigned int idx) { #ifdef PR_CAPBSET_DROP if (HAVE_PR_CAPBSET_DROP) { if (action == CAPNG_ADD) m.bounds[idx] |= MASK(capability); else m.bounds[idx] &= ~(MASK(capability)); } #endif } static void update_ambient_set(capng_act_t action, unsigned int capability, unsigned int idx) { #ifdef PR_CAP_AMBIENT if (HAVE_PR_CAP_AMBIENT) { if (action == CAPNG_ADD) m.ambient[idx] |= MASK(capability); else m.ambient[idx] &= ~(MASK(capability)); } #endif } int capng_update(capng_act_t action, capng_type_t type, unsigned int capability) { // Before updating, we expect that the data is initialized to something if (m.state < CAPNG_INIT) return -1; if (!cap_valid(capability)) { errno = EINVAL; return -1; } if (m.cap_ver == 1) { if (CAPNG_EFFECTIVE & type) v1_update(action, capability, &m.data.v1.effective); if (CAPNG_PERMITTED & type) v1_update(action, capability, &m.data.v1.permitted); if (CAPNG_INHERITABLE & type) v1_update(action, capability, &m.data.v1.inheritable); } else { unsigned int idx; if (capability > 31) { idx = capability>>5; capability %= 32; } else idx = 0; if (CAPNG_EFFECTIVE & type) update_effective(action, capability, idx); if (CAPNG_PERMITTED & type) update_permitted(action, capability, idx); if (CAPNG_INHERITABLE & type) update_inheritable(action, capability, idx); if (CAPNG_BOUNDING_SET & type) update_bounding_set(action, capability, idx); if (CAPNG_AMBIENT & type) update_ambient_set(action, capability, idx); } if (CAPNG_BOUNDING_SET & type) mark_bounding_set_changed(&m); m.state = CAPNG_UPDATED; return 0; } int capng_updatev(capng_act_t action, capng_type_t type, unsigned int capability, ...) { int rc; unsigned int cap; va_list ap; rc = capng_update(action, type, capability); if (rc) return rc; va_start(ap, capability); cap = va_arg(ap, unsigned int); while (cap_valid(cap)) { rc = capng_update(action, type, cap); if (rc) break; cap = va_arg(ap, unsigned int); } va_end(ap); // See if planned exit or invalid if (cap == (unsigned)-1) rc = 0; else { rc = -1; errno = EINVAL; } return rc; } int capng_apply(capng_select_t set) { int rc = 0; // Before updating, we expect that the data is initialized to something if (m.state < CAPNG_INIT) return -1; if (set == 0) { errno = EINVAL; return -1; } if (set & CAPNG_SELECT_BOUNDS) { #ifdef PR_CAPBSET_DROP if (HAVE_PR_CAPBSET_DROP) { struct cap_ng state; memcpy(&state, &m, sizeof(state)); /* save state */ if (capng_get_caps_process()) return -9; if (capng_have_capability(CAPNG_EFFECTIVE, CAP_SETPCAP)) { unsigned int i; memcpy(&m, &state, sizeof(m)); /* restore state */ for (i=0; i <= last_cap; i++) { if (capng_have_capability(CAPNG_BOUNDING_SET, i) == 0) { if (prctl(PR_CAPBSET_DROP, i, 0, 0, 0) <0) { rc = -2; goto try_caps; } } } if (get_bounding_set() < 0) { rc = -3; goto try_caps; } clear_bounding_set_changed(&m); m.state = CAPNG_APPLIED; } else { memcpy(&m, &state, sizeof(m)); /* restore state */ rc = -4; goto try_caps; } } #endif } // Try caps is here so that if someone had SELECT_BOTH and we blew up // doing the bounding set, we at least try to set any capabilities // before returning in case the caller also doesn't bother checking // the return code. try_caps: if (set & CAPNG_SELECT_CAPS) { if (capset((cap_user_header_t)&m.hdr, (cap_user_data_t)&m.data) == 0) m.state = CAPNG_APPLIED; else rc = -5; } // Most programs do not and should not mess with ambient capabilities. // Instead of returning here if rc is set, we'll let it try to // do something with ambient capabilities in hopes that it's lowering // capabilities. Again, this is for people that don't check their // return codes. // // Do ambient last so that inheritable and permitted are set by the // time we get here. if (set & CAPNG_SELECT_AMBIENT) { #ifdef PR_CAP_AMBIENT if (HAVE_PR_CAP_AMBIENT) { if (capng_have_capabilities(CAPNG_SELECT_AMBIENT) == CAPNG_NONE) { if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) < 0) { rc = -6; goto out; } } else { unsigned int i; // Clear them all if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) < 0) { rc = -7; goto out; } for (i=0; i <= last_cap; i++) { if (capng_have_capability(CAPNG_AMBIENT, i)) if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, i, 0, 0) < 0){ rc = -8; goto out; } } } m.state = CAPNG_APPLIED; } #endif } out: return rc; } #ifdef VFS_CAP_U32 #ifdef VFS_CAP_REVISION_3 static int save_data(struct vfs_ns_cap_data *filedata, int *size) #else static int save_data(struct vfs_cap_data *filedata, int *size) #endif { // Now stuff the data structures if (m.vfs_cap_ver == 1) { filedata->data[0].permitted = FIXUP(m.data.v1.permitted); filedata->data[0].inheritable = FIXUP(m.data.v1.inheritable); filedata->magic_etc = FIXUP(VFS_CAP_REVISION_1); *size = XATTR_CAPS_SZ_1; } else if (m.vfs_cap_ver == 2 || m.vfs_cap_ver == 3) { int eff; if (m.data.v3[0].effective || m.data.v3[1].effective) eff = VFS_CAP_FLAGS_EFFECTIVE; else eff = 0; filedata->data[0].permitted = FIXUP(m.data.v3[0].permitted); filedata->data[0].inheritable = FIXUP(m.data.v3[0].inheritable); filedata->data[1].permitted = FIXUP(m.data.v3[1].permitted); filedata->data[1].inheritable = FIXUP(m.data.v3[1].inheritable); #ifdef VFS_CAP_REVISION_3 /* * V3 adds rootid to the V2 capability words. Its revision, rootid, * and 24-byte size must be written together as one xattr format. */ if (m.vfs_cap_ver == 3) { filedata->magic_etc = FIXUP(VFS_CAP_REVISION_3 | eff); filedata->rootid = FIXUP(m.rootid); *size = XATTR_CAPS_SZ_3; } else #endif { filedata->magic_etc = FIXUP(VFS_CAP_REVISION_2 | eff); *size = XATTR_CAPS_SZ_2; } } return 0; } #endif int capng_apply_caps_fd(int fd) { #ifndef VFS_CAP_U32 return -1; #else int rc, size = 0; #ifdef VFS_CAP_REVISION_3 struct vfs_ns_cap_data filedata; #else struct vfs_cap_data filedata; #endif struct stat buf; // Before updating, we expect that the data is initialized to something if (m.state < CAPNG_INIT) return -1; if (fstat(fd, &buf) != 0) return -1; if (S_ISLNK(buf.st_mode) || !S_ISREG(buf.st_mode)) { errno = EINVAL; return -1; } if (capng_have_capabilities(CAPNG_SELECT_CAPS) == CAPNG_NONE) rc = fremovexattr(fd, XATTR_NAME_CAPS); else { if (save_data(&filedata, &size)) { m.state = CAPNG_ERROR; errno = EINVAL; return -2; } rc = fsetxattr(fd, XATTR_NAME_CAPS, &filedata, size, 0); } if (rc == 0) m.state = CAPNG_APPLIED; return rc; #endif } // Change uids keeping/removing only certain capabilities int capng_change_id(int uid, int gid, capng_flags_t flag) { enum { TMP_SETPCAP_E = 1 << 0, TMP_SETPCAP_P = 1 << 1, TMP_SETUID_E = 1 << 2, TMP_SETUID_P = 1 << 3, TMP_SETGID_E = 1 << 4, TMP_SETGID_P = 1 << 5, }; unsigned int tmp_caps = 0; int rc, ret; struct passwd *pw = NULL; gid_t *gids = NULL, *merged = NULL; size_t gid_cnt = 0, merged_cnt = 0; // Before updating, we expect that the data is initialized to something if (m.state < CAPNG_INIT) { ret = -1; goto out; } // Validate mutually exclusive staged-group combinations up front. if ((flag & CAPNG_APPLY_STAGED_GROUPS) && (flag & CAPNG_DROP_SUPP_GRP)) { ret = -12; goto out; } // Validate mutually exclusive bounding-set operations up front. if ((flag & CAPNG_APPLY_BOUNDING) && (flag & CAPNG_CLEAR_BOUNDING)) { ret = -17; goto out; } // Staged application only makes sense when a list was staged earlier. if ((flag & CAPNG_APPLY_STAGED_GROUPS) && m.add_group_cnt == 0) { ret = -13; goto out; } // Make sure the temporary transition caps we may need are present. #ifdef PR_CAPBSET_DROP if (HAVE_PR_CAPBSET_DROP) { // If newer kernel, we need setpcap to change the bounding set if ((flag & CAPNG_CLEAR_BOUNDING) || ((flag & CAPNG_APPLY_BOUNDING) && m.bounds_state_changed)) { if (capng_have_capability(CAPNG_EFFECTIVE, CAP_SETPCAP) == 0) { tmp_caps |= TMP_SETPCAP_E; capng_update(CAPNG_ADD, CAPNG_EFFECTIVE, CAP_SETPCAP); } if (capng_have_capability(CAPNG_PERMITTED, CAP_SETPCAP) == 0) { tmp_caps |= TMP_SETPCAP_P; capng_update(CAPNG_ADD, CAPNG_PERMITTED, CAP_SETPCAP); } } } #endif if (gid != -1) { if (capng_have_capability(CAPNG_EFFECTIVE, CAP_SETGID) == 0) { tmp_caps |= TMP_SETGID_E; capng_update(CAPNG_ADD, CAPNG_EFFECTIVE, CAP_SETGID); } if (capng_have_capability(CAPNG_PERMITTED, CAP_SETGID) == 0) { tmp_caps |= TMP_SETGID_P; capng_update(CAPNG_ADD, CAPNG_PERMITTED, CAP_SETGID); } } if (uid != -1) { if (capng_have_capability(CAPNG_EFFECTIVE, CAP_SETUID) == 0) { tmp_caps |= TMP_SETUID_E; capng_update(CAPNG_ADD, CAPNG_EFFECTIVE, CAP_SETUID); } if (capng_have_capability(CAPNG_PERMITTED, CAP_SETUID) == 0) { tmp_caps |= TMP_SETUID_P; capng_update(CAPNG_ADD, CAPNG_PERMITTED, CAP_SETUID); } } // Enable keepcaps before changing credentials so final caps survive. if (prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0) < 0) { ret = -2; goto out; } // Apply the temporary working set before touching ids or groups. rc = capng_apply(CAPNG_SELECT_CAPS); if (rc < 0) { ret = -3; goto err_out; } // If we are clearing ambient, only clear since its applied at the end if (flag & CAPNG_CLEAR_AMBIENT) capng_clear(CAPNG_SELECT_AMBIENT); // Clear bounding set if needed while we have CAP_SETPCAP if (flag & CAPNG_CLEAR_BOUNDING) { capng_clear(CAPNG_SELECT_BOUNDS); rc = capng_apply(CAPNG_SELECT_BOUNDS); if (rc) { ret = -8; goto err_out; } } // Apply a caller-prepared bounding set only when it is explicitly dirty. if ((flag & CAPNG_APPLY_BOUNDING) && m.bounds_state_changed) { rc = capng_apply(CAPNG_SELECT_BOUNDS); if (rc) { ret = -8; goto err_out; } } // Change gid before supplemental groups so kernel permission checks // see the target primary group. if (gid != -1) { rc = setresgid(gid, gid, gid); if (rc) { ret = -4; goto err_out; } } // Resolve the passwd entry once when natural group setup is requested. if ((flag & CAPNG_INIT_SUPP_GRP) && uid != -1) { pw = getpwuid(uid); if (pw == NULL) { ret = -10; goto err_out; } } // There are four supplemental/additional group modes: // init+staged merge, init only, drop only, or staged only. if ((flag & CAPNG_INIT_SUPP_GRP) && (flag & CAPNG_APPLY_STAGED_GROUPS)) { if (uid != -1) { gid_t base_gid = gid != -1 ? (gid_t)gid : pw->pw_gid; // Build the natural target account group list first. rc = get_additional_groups(pw, base_gid, &gids, &gid_cnt); if (rc) { ret = -15; goto err_out; } } // Then append staged gids without duplicates and apply the result. rc = merge_additional_groups(gids, gid_cnt, m.add_groups, m.add_group_cnt, &merged, &merged_cnt); if (rc) { ret = -16; goto err_out; } if (setgroups(merged_cnt, merged)) { ret = -14; goto err_out; } } else if ((flag & CAPNG_INIT_SUPP_GRP) && uid != -1) { gid_t base_gid = gid != -1 ? (gid_t)gid : pw->pw_gid; // Preserve the long-standing initgroups-only behavior. if (initgroups(pw->pw_name, base_gid)) { ret = -5; goto err_out; } } else if ((flag & CAPNG_DROP_SUPP_GRP) && gid != -1) { // Drop all supplemental groups for the target identity. if (setgroups(0, NULL)) { ret = -11; goto err_out; } } else if (flag & CAPNG_APPLY_STAGED_GROUPS) { // Apply exactly the caller-staged gid list. if (setgroups(m.add_group_cnt, m.add_groups)) { ret = -14; goto err_out; } } // Change uid if (uid != -1) { rc = setresuid(uid, uid, uid); if (rc) { ret = -6; goto err_out; } } // Credential changes are complete, so disable keepcaps again. rc = prctl(PR_SET_KEEPCAPS, 0, 0, 0, 0); if (rc < 0) { ret = -7; goto out; } // Now throw away CAP_SETPCAP so no more changes if (tmp_caps & TMP_SETGID_E) capng_update(CAPNG_DROP, CAPNG_EFFECTIVE, CAP_SETGID); if (tmp_caps & TMP_SETGID_P) capng_update(CAPNG_DROP, CAPNG_PERMITTED, CAP_SETGID); if (tmp_caps & TMP_SETUID_E) capng_update(CAPNG_DROP, CAPNG_EFFECTIVE, CAP_SETUID); if (tmp_caps & TMP_SETUID_P) capng_update(CAPNG_DROP, CAPNG_PERMITTED, CAP_SETUID); // Drop any temporary transition caps and install the final sets. if (tmp_caps & TMP_SETPCAP_E) capng_update(CAPNG_DROP, CAPNG_EFFECTIVE, CAP_SETPCAP); if (tmp_caps & TMP_SETPCAP_P) capng_update(CAPNG_DROP, CAPNG_PERMITTED, CAP_SETPCAP); rc = capng_apply(CAPNG_SELECT_CAPS|CAPNG_SELECT_AMBIENT); if (rc < 0) { ret = -9; goto out; } ret = 0; goto out; err_out: prctl(PR_SET_KEEPCAPS, 0, 0, 0, 0); out: // Staged gids are one-shot state, so always clear them before return. free(gids); free(merged); clear_staged_additional_groups(&m); return ret; } int capng_lock(void) { int rc = 0; #if defined(PR_GET_SECUREBITS) && defined(PR_SET_SECUREBITS) int securebits; unsigned int locked; securebits = prctl(PR_GET_SECUREBITS, 0UL, 0UL, 0UL, 0UL); if (securebits < 0) { rc = -1; } else { /* * PR_SET_SECUREBITS replaces the complete word. Preserve bits set * by the caller, including policy bits added by newer kernels. */ locked = (unsigned int)securebits | 1U << SECURE_NOROOT | 1U << SECURE_NOROOT_LOCKED | 1U << SECURE_NO_SETUID_FIXUP | 1U << SECURE_NO_SETUID_FIXUP_LOCKED; if (locked != (unsigned int)securebits && prctl(PR_SET_SECUREBITS, (unsigned long)locked, 0UL, 0UL, 0UL) < 0) rc = -1; } #endif #ifdef PR_SET_NO_NEW_PRIVS if (prctl(PR_SET_NO_NEW_PRIVS, 1UL, 0UL, 0UL, 0UL) < 0) rc += -2; #endif return rc; } // -1 - error, 0 - no caps, 1 partial caps, 2 full caps capng_results_t capng_have_capabilities(capng_select_t set) { int empty = 0, full = 0; // First, try to init with current set if (m.state < CAPNG_INIT) { if (capng_get_caps_process()) return CAPNG_FAIL; } // If we still don't have anything, error out if (m.state < CAPNG_INIT) return CAPNG_FAIL; if (set & CAPNG_SELECT_CAPS) { if (m.cap_ver == 1) { if (m.data.v1.effective == 0) empty = 1; // after fill, 30 bits starts from upper to lower else if (m.data.v1.effective == 0x7FFFFFFFU) full = 1; // actual capabilities read from system else if (m.data.v1.effective == 0xFFFFFEFFU) full = 1; else return CAPNG_PARTIAL; } else { if (m.data.v3[0].effective == 0) empty = 1; else if (m.data.v3[0].effective == 0xFFFFFFFFU) full = 1; else return CAPNG_PARTIAL; if ((m.data.v3[1].effective & UPPER_MASK) == 0 && !full) empty = 1; else if ((m.data.v3[1].effective & UPPER_MASK) == UPPER_MASK && !empty) full = 1; else return CAPNG_PARTIAL; } } #ifdef PR_CAPBSET_DROP if (HAVE_PR_CAPBSET_DROP) { if (set & CAPNG_SELECT_BOUNDS) { if (m.bounds[0] == 0) empty = 1; else if (m.bounds[0] == 0xFFFFFFFFU) full = 1; else return CAPNG_PARTIAL; if ((m.bounds[1] & UPPER_MASK) == 0) empty = 1; else if ((m.bounds[1] & UPPER_MASK) == UPPER_MASK) full = 1; else return CAPNG_PARTIAL; } } else if (set & CAPNG_SELECT_BOUNDS) empty = 1; // Only report empty if they asked about it #endif #ifdef PR_CAP_AMBIENT if (HAVE_PR_CAP_AMBIENT) { if (set & CAPNG_SELECT_AMBIENT) { if (m.ambient[0] == 0) empty = 1; else if (m.ambient[0] == 0xFFFFFFFFU) full = 1; else return CAPNG_PARTIAL; if ((m.ambient[1] & UPPER_MASK) == 0) empty = 1; else if ((m.ambient[1] & UPPER_MASK) == UPPER_MASK) full = 1; else return CAPNG_PARTIAL; } } else if (set & CAPNG_SELECT_AMBIENT) empty = 1; // Only report empty if they asked about it #endif if (empty == 1 && full == 0) return CAPNG_NONE; else if (empty == 0 && full == 1) return CAPNG_FULL; return CAPNG_PARTIAL; } // -1 - error, 0 - no caps, 1 partial caps, 2 full caps capng_results_t capng_have_permitted_capabilities(void) { int empty = 0; int lower_full = 0; // First, try to init with current set if (m.state < CAPNG_INIT) { if (capng_get_caps_process()) return CAPNG_FAIL; } // If we still don't have anything, error out if (m.state < CAPNG_INIT) return CAPNG_FAIL; if (m.data.v3[0].permitted == 0) empty = 1; else if (m.data.v3[0].permitted == 0xFFFFFFFFU) lower_full = 1; else return CAPNG_PARTIAL; // At this point, lower 32 bits are either full or empty if ((m.data.v3[1].permitted & UPPER_MASK) == 0 && !lower_full) empty = 1; else if ((m.data.v3[1].permitted & UPPER_MASK) != UPPER_MASK || empty) return CAPNG_PARTIAL; // Partial is already handled, it's either empty or full now if (empty) return CAPNG_NONE; return CAPNG_FULL; } static int check_effective(unsigned int capability, unsigned int idx) { return MASK(capability) & m.data.v3[idx].effective ? 1 : 0; } static int check_permitted(unsigned int capability, unsigned int idx) { return MASK(capability) & m.data.v3[idx].permitted ? 1 : 0; } static int check_inheritable(unsigned int capability, unsigned int idx) { return MASK(capability) & m.data.v3[idx].inheritable ? 1 : 0; } static int bounds_bit_check(unsigned int capability, unsigned int idx) { #ifdef PR_CAPBSET_DROP if (HAVE_PR_CAPBSET_DROP) { return MASK(capability) & m.bounds[idx] ? 1 : 0; } #endif return 0; } static int ambient_bit_check(unsigned int capability, unsigned int idx) { #ifdef PR_CAP_AMBIENT if (HAVE_PR_CAP_AMBIENT) { return MASK(capability) & m.ambient[idx] ? 1 : 0; } #endif return 0; } static int v1_check(unsigned int capability, __u32 data) { return MASK(capability) & data ? 1 : 0; } int capng_have_capability(capng_type_t which, unsigned int capability) { // First, try to init with current set if (m.state < CAPNG_INIT) { if (capng_get_caps_process()) return 0; } // If we still don't have anything, error out if (m.state < CAPNG_INIT) return 0; if (m.cap_ver == 1 && capability > 31) return 0; if (!cap_valid(capability)) return 0; if (m.cap_ver == 1) { if (which == CAPNG_EFFECTIVE) return v1_check(capability, m.data.v1.effective); else if (which == CAPNG_PERMITTED) return v1_check(capability, m.data.v1.permitted); else if (which == CAPNG_INHERITABLE) return v1_check(capability, m.data.v1.inheritable); } else { unsigned int idx; if (capability > 31) { idx = capability>>5; capability %= 32; } else idx = 0; if (which == CAPNG_EFFECTIVE) return check_effective(capability, idx); else if (which == CAPNG_PERMITTED) return check_permitted(capability, idx); else if (which == CAPNG_INHERITABLE) return check_inheritable(capability, idx); else if (which == CAPNG_BOUNDING_SET) return bounds_bit_check(capability, idx); else if (which == CAPNG_AMBIENT) return ambient_bit_check(capability, idx); } return 0; } char *capng_print_caps_numeric(capng_print_t where, capng_select_t set) { char *ptr = NULL; if (m.state < CAPNG_INIT) return ptr; if (where == CAPNG_PRINT_STDOUT) { if (set & CAPNG_SELECT_CAPS) { if (m.cap_ver == 1) { printf( "Effective: %08X\n" "Permitted: %08X\n" "Inheritable: %08X\n", m.data.v1.effective, m.data.v1.permitted, m.data.v1.inheritable); } else { printf( "Effective: %08X, %08X\n" "Permitted: %08X, %08X\n" "Inheritable: %08X, %08X\n", m.data.v3[1].effective & UPPER_MASK, m.data.v3[0].effective, m.data.v3[1].permitted & UPPER_MASK, m.data.v3[0].permitted, m.data.v3[1].inheritable & UPPER_MASK, m.data.v3[0].inheritable); } } #ifdef PR_CAPBSET_DROP if (HAVE_PR_CAPBSET_DROP) { if (set & CAPNG_SELECT_BOUNDS) printf("Bounding Set: %08X, %08X\n", m.bounds[1] & UPPER_MASK, m.bounds[0]); } #endif #ifdef PR_CAP_AMBIENT if (HAVE_PR_CAP_AMBIENT) { if (set & CAPNG_SELECT_AMBIENT) printf("Ambient: %08X, %08X\n", m.ambient[1] & UPPER_MASK, m.ambient[0]); } #endif } else if (where == CAPNG_PRINT_BUFFER) { if (set & CAPNG_SELECT_CAPS) { // Make it big enough for bounding & ambient set, too size_t buf_size = 180; ptr = malloc(buf_size); if (ptr == NULL) return ptr; if (m.cap_ver == 1) { // 22 * 3 + 1 snprintf(ptr, buf_size, "Effective: %08X\n" "Permitted: %08X\n" "Inheritable: %08X\n", m.data.v1.effective, m.data.v1.permitted, m.data.v1.inheritable); } else { // 35 * 5 + 1 (bounding is 35) snprintf(ptr, buf_size, "Effective: %08X, %08X\n" "Permitted: %08X, %08X\n" "Inheritable: %08X, %08X\n", m.data.v3[1].effective & UPPER_MASK, m.data.v3[0].effective, m.data.v3[1].permitted & UPPER_MASK, m.data.v3[0].permitted, m.data.v3[1].inheritable & UPPER_MASK, m.data.v3[0].inheritable); } } if (set & CAPNG_SELECT_BOUNDS) { #ifdef PR_CAPBSET_DROP if (HAVE_PR_CAPBSET_DROP) { char *s; // If ptr is NULL, we only room for bounding and ambient if (ptr == NULL ) { ptr = malloc(80); if (ptr == NULL) return ptr; *ptr = 0; s = ptr; } else s = ptr + strlen(ptr); // prints 34 + 1 chars snprintf(s, 40, "Bounding Set: %08X, %08X\n", m.bounds[1] & UPPER_MASK, m.bounds[0]); } #endif } if (set & CAPNG_SELECT_AMBIENT) { #ifdef PR_CAP_AMBIENT if (HAVE_PR_CAP_AMBIENT) { char *s; // If ptr is NULL, we only have room for ambient if (ptr == NULL ) { ptr = malloc(40); if (ptr == NULL) return ptr; *ptr = 0; s = ptr; } else s = ptr + strlen(ptr); // prints 33 + 1 chars snprintf(s, 40, "Ambient Set: %08X, %08X\n", m.ambient[1] & UPPER_MASK, m.ambient[0]); } #endif } } return ptr; } char *capng_print_caps_text(capng_print_t where, capng_type_t which) { unsigned int i, cnt = 0; size_t total = 1; int found = 0; char *ptr = NULL; if (m.state < CAPNG_INIT) return ptr; if (where == CAPNG_PRINT_BUFFER) { for (i=0; i<=last_cap; i++) { if (capng_have_capability(which, i)) { const char *n = capng_capability_to_name(i); size_t len; if (n == NULL) n = "unknown"; len = strlen(n); total += len; if (found) total += 2; found = 1; } } ptr = malloc(found ? total : sizeof("none")); if (ptr == NULL) return ptr; *ptr = 0; found = 0; } for (i=0; i<=last_cap; i++) { if (capng_have_capability(which, i)) { const char *n = capng_capability_to_name(i); size_t len; if (n == NULL) n = "unknown"; len = strlen(n); if (where == CAPNG_PRINT_STDOUT) { if (found == 0) printf("%s", n); else printf(", %s", n); } else if (where == CAPNG_PRINT_BUFFER) { if (ptr == NULL) return NULL; if (found) { ptr[cnt++] = ','; ptr[cnt++] = ' '; } memcpy(ptr + cnt, n, len + 1); cnt += len; } found = 1; } } if (found == 0) { if (where == CAPNG_PRINT_STDOUT) printf("none"); else if (where == CAPNG_PRINT_BUFFER && ptr) strcpy(ptr, "none"); else return NULL; } return ptr; } void *capng_save_state(void) { void *ptr = malloc(sizeof(m)); if (ptr) { memcpy(ptr, &m, sizeof(m)); if (copy_staged_additional_groups(ptr, &m)) { free(ptr); ptr = NULL; } } return ptr; } void capng_restore_state(void **state) { if (state) { struct cap_ng *ptr = *state; if (ptr) { clear_staged_additional_groups(&m); memcpy(&m, ptr, sizeof(m)); if (copy_staged_additional_groups(&m, ptr)) m.state = CAPNG_ERROR; clear_staged_additional_groups(ptr); } free(ptr); *state = NULL; } } stevegrubb-libcap-ng-9ae4440/src/cap-ng.h000066400000000000000000000072641524174023300201670ustar00rootroot00000000000000/* libcap-ng.h -- * Copyright 2009,2013,2020-26 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 program; see the file COPYING.LIB. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef LIBCAP_NG_HEADER #define LIBCAP_NG_HEADER #include #include #include #include #include // The next macro originates in sys/cdefs.h // Warn unused result #ifndef __wur # define __wur #endif #ifdef __cplusplus extern "C" { #endif typedef enum { CAPNG_DROP, CAPNG_ADD } capng_act_t; typedef enum { CAPNG_EFFECTIVE=1, CAPNG_PERMITTED=2, CAPNG_INHERITABLE=4, CAPNG_BOUNDING_SET=8, CAPNG_AMBIENT=16 } capng_type_t; typedef enum { CAPNG_SELECT_CAPS = 16, CAPNG_SELECT_BOUNDS = 32, CAPNG_SELECT_BOTH = 48, CAPNG_SELECT_AMBIENT = 64, CAPNG_SELECT_ALL = 112 } capng_select_t; typedef enum { CAPNG_FAIL=-1, CAPNG_NONE, CAPNG_PARTIAL, CAPNG_FULL } capng_results_t; typedef enum { CAPNG_PRINT_STDOUT, CAPNG_PRINT_BUFFER } capng_print_t; typedef enum { CAPNG_NO_FLAG=0, CAPNG_DROP_SUPP_GRP=1, CAPNG_CLEAR_BOUNDING=2, CAPNG_INIT_SUPP_GRP=4, CAPNG_CLEAR_AMBIENT=8, CAPNG_APPLY_STAGED_GROUPS=16, CAPNG_APPLY_BOUNDING=32 } capng_flags_t; #define CAPNG_UNSET_ROOTID ((uid_t)-1) #define CAPNG_SUPPORTS_AMBIENT 1 // These functions manipulate process capabilities void capng_clear(capng_select_t set); void capng_fill(capng_select_t set); void capng_setpid(int pid); int capng_get_caps_process(void) __wur; int capng_update(capng_act_t action, capng_type_t type,unsigned int capability); int capng_updatev(capng_act_t action, capng_type_t type, unsigned int capability, ...); // These functions apply the capabilities previously setup to a process int capng_apply(capng_select_t set) __wur; int capng_lock(void) __wur; int capng_change_id(int uid, int gid, capng_flags_t flag) __wur; int capng_stage_additional_groups(const gid_t *gids, size_t count) __wur; // These functions are used for file based capabilities uid_t capng_get_rootid(void); int capng_set_rootid(uid_t rootid); int capng_get_caps_fd(int fd) __wur; int capng_apply_caps_fd(int fd) __wur; // These functions check capability bits capng_results_t capng_have_capabilities(capng_select_t set); capng_results_t capng_have_permitted_capabilities(void); int capng_have_capability(capng_type_t which, unsigned int capability); // These functions printout capabilities char *capng_print_caps_numeric(capng_print_t where, capng_select_t set); char *capng_print_caps_text(capng_print_t where, capng_type_t which); // These functions convert between numeric and text string int capng_name_to_capability(const char *name); const char *capng_capability_to_name(unsigned int capability); // These function should be used when you suspect a third party library // may use libcap-ng also and want to make sure it doesn't alter something // important. Otherwise you shouldn't need to call these. void capng_restore_state(void **state); void *capng_save_state(void); #ifdef __cplusplus } #endif #endif stevegrubb-libcap-ng-9ae4440/src/captab.h000066400000000000000000000055341524174023300202520ustar00rootroot00000000000000/* captab.h -- * Copyright 2009,2011-14,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 program; see the file COPYING.LIB. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ _S(CAP_CHOWN, "chown" ) _S(CAP_DAC_OVERRIDE, "dac_override" ) _S(CAP_DAC_READ_SEARCH, "dac_read_search" ) _S(CAP_FOWNER, "fowner" ) _S(CAP_FSETID, "fsetid" ) _S(CAP_KILL, "kill" ) _S(CAP_SETGID, "setgid" ) _S(CAP_SETUID, "setuid" ) _S(CAP_SETPCAP, "setpcap" ) _S(CAP_LINUX_IMMUTABLE, "linux_immutable" ) _S(CAP_NET_BIND_SERVICE, "net_bind_service" ) _S(CAP_NET_BROADCAST, "net_broadcast" ) _S(CAP_NET_ADMIN, "net_admin" ) _S(CAP_NET_RAW, "net_raw" ) _S(CAP_IPC_LOCK, "ipc_lock" ) _S(CAP_IPC_OWNER, "ipc_owner" ) _S(CAP_SYS_MODULE, "sys_module" ) _S(CAP_SYS_RAWIO, "sys_rawio" ) _S(CAP_SYS_CHROOT, "sys_chroot" ) _S(CAP_SYS_PTRACE, "sys_ptrace" ) _S(CAP_SYS_PACCT, "sys_pacct" ) _S(CAP_SYS_ADMIN, "sys_admin" ) _S(CAP_SYS_BOOT, "sys_boot" ) _S(CAP_SYS_NICE, "sys_nice" ) _S(CAP_SYS_RESOURCE, "sys_resource" ) _S(CAP_SYS_TIME, "sys_time" ) _S(CAP_SYS_TTY_CONFIG, "sys_tty_config" ) _S(CAP_MKNOD, "mknod" ) _S(CAP_LEASE, "lease" ) _S(CAP_AUDIT_WRITE, "audit_write" ) _S(CAP_AUDIT_CONTROL, "audit_control" ) #ifdef CAP_SETFCAP _S(CAP_SETFCAP, "setfcap" ) #endif #ifdef CAP_MAC_OVERRIDE _S(CAP_MAC_OVERRIDE, "mac_override" ) #endif #ifdef CAP_MAC_ADMIN _S(CAP_MAC_ADMIN, "mac_admin" ) #endif #ifdef CAP_SYSLOG _S(CAP_SYSLOG, "syslog" ) #endif #if defined(CAP_EPOLLWAKEUP) && defined(CAP_BLOCK_SUSPEND) #error "Both CAP_EPOLLWAKEUP and CAP_BLOCK_SUSPEND are defined" #endif #ifdef CAP_EPOLLWAKEUP _S(CAP_EPOLLWAKEUP, "epollwakeup" ) #endif #ifdef CAP_WAKE_ALARM _S(CAP_WAKE_ALARM, "wake_alarm" ) #endif #ifdef CAP_BLOCK_SUSPEND _S(CAP_BLOCK_SUSPEND, "block_suspend" ) #endif #ifdef CAP_AUDIT_READ _S(CAP_AUDIT_READ, "audit_read" ) #endif #ifdef CAP_PERFMON _S(CAP_PERFMON, "perfmon" ) #endif #ifdef CAP_BPF _S(CAP_BPF, "bpf" ) #endif #ifdef CAP_CHECKPOINT_RESTORE _S(CAP_CHECKPOINT_RESTORE, "checkpoint_restore") #endif stevegrubb-libcap-ng-9ae4440/src/libcap-ng.pc.in000066400000000000000000000003731524174023300214300ustar00rootroot00000000000000prefix=@prefix@ exec_prefix=@exec_prefix@ libdir=@libdir@ includedir=@includedir@ Name: libcap-ng Description: An alternate POSIX capabilities library. Version: @VERSION@ Libs: -L${libdir} -lcap-ng Cflags: -I${includedir} License: LGPL-2.1-or-later stevegrubb-libcap-ng-9ae4440/src/libdrop_ambient.c000066400000000000000000000020741524174023300221410ustar00rootroot00000000000000/* libdrop_ambient.c -- * 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 program; see the file COPYING.LIB. 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 static void __attribute__ ((constructor)) drop_ambient_init(void) { prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0); } stevegrubb-libcap-ng-9ae4440/src/lookup_table.c000066400000000000000000000073511524174023300214720ustar00rootroot00000000000000/* lookup_table.c -- * Copyright 2009, 2013, 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 program; see the file COPYING.LIB. 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 "cap-ng.h" #pragma GCC optimize("O3") #define hidden __attribute__ ((visibility ("hidden"))) extern unsigned int last_cap hidden; #undef cap_valid #define cap_valid(x) ((x) <= last_cap) struct transtab { unsigned int value; unsigned int offset; }; #define MSGSTRFIELD(line) MSGSTRFIELD1(line) #define MSGSTRFIELD1(line) str##line /* To create the following tables in a DSO-friendly way we split them in two separate variables: a long string which is created by concatenating all strings referenced in the table and the table itself, which uses offsets instead of string pointers. To do this without increasing the maintenance burden we use a lot of preprocessor magic. All the maintainer has to do is to add a new entry to the included file and recompile. */ static const union captab_msgstr_t { struct { #define _S(n, s) char MSGSTRFIELD(__LINE__)[sizeof (s)]; #include "captab.h" #undef _S }; char str[0]; } captab_msgstr = { { #define _S(n, s) s, #include "captab.h" #undef _S } }; static const struct transtab captab[] = { #define _S(n, s) { n, offsetof(union captab_msgstr_t, \ MSGSTRFIELD(__LINE__)) }, #include "captab.h" #undef _S }; #define CAP_NG_CAPABILITY_NAMES (sizeof(captab)/sizeof(captab[0])) static inline int capng_lookup_name(const char *name) { // brute force search for (size_t i = 0; i < CAP_NG_CAPABILITY_NAMES; i++) { if (!strcasecmp(captab_msgstr.str + captab[i].offset, name)) return captab[i].value; } return -1; } static inline const char *capng_lookup_number(unsigned int number) { if (number >= CAP_NG_CAPABILITY_NAMES) return NULL; if (captab[number].value == number) return captab_msgstr.str + captab[number].offset; // Fallback to old search in case a capability is retired for (size_t i = 0; i < CAP_NG_CAPABILITY_NAMES; i++) { if (captab[i].value == number) return captab_msgstr.str + captab[i].offset; } return NULL; } /* * capng_name_to_capability - given a string with the name of the capabilty * return its number. * @name - the name of the capability to lookup * * returns the number associated with the string. */ int capng_name_to_capability(const char *name) { return capng_lookup_name(name); } /* * capng_capability_to_name - given a number, return a string with the name of * the capabilty. * @capability - the number of the capability to lookup * * returns the string associated with the number. */ static __thread char ptr2[32]; const char *capng_capability_to_name(unsigned int capability) { const char *ptr; if (!cap_valid(capability)) return NULL; ptr = capng_lookup_number(capability); if (ptr == NULL) { snprintf(ptr2, sizeof(ptr2), "cap_%u", capability); ptr = ptr2; } return ptr; } stevegrubb-libcap-ng-9ae4440/src/test/000077500000000000000000000000001524174023300176175ustar00rootroot00000000000000stevegrubb-libcap-ng-9ae4440/src/test/Makefile.am000066400000000000000000000027171524174023300216620ustar00rootroot00000000000000# Makefile.am -- # Copyright 2009,10,15 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 program; see the file COPYING.LIB. If not, write to the # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor # Boston, MA 02110-1335, USA. # # Authors: # Steve Grubb # AM_CPPFLAGS = -I${top_srcdir} -I${top_srcdir}/src check_PROGRAMS = lib_test thread_test change_id_test file_caps_test \ securebits_test TESTS = $(check_PROGRAMS) lib_test_LDADD = ${top_builddir}/src/libcap-ng.la thread_test_LDADD = ${top_builddir}/src/libcap-ng.la -lpthread change_id_test_LDADD = ${top_builddir}/src/libcap-ng.la file_caps_test_LDADD = ${top_builddir}/src/libcap-ng.la securebits_test_LDADD = ${top_builddir}/src/libcap-ng.la clean-local: rm -f utils_test$(EXEEXT) utils_test.log utils_test.trs \ cap_audit_util_test$(EXEEXT) cap_audit_util_test.log \ cap_audit_util_test.trs stevegrubb-libcap-ng-9ae4440/src/test/change_id_test.c000066400000000000000000000345341524174023300227340ustar00rootroot00000000000000/* change_id_test.c -- capng_change_id additional group tests * Copyright 2026 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 program; see the file COPYING.LIB. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. */ #include "config.h" #include "../cap-ng.h" #include #include #include #include #include #include #include #include static void fail(const char *msg) { fprintf(stderr, "%s\n", msg); _exit(EXIT_FAILURE); } static int gid_in_list(const gid_t *gids, size_t count, gid_t gid) { size_t i; for (i = 0; i < count; i++) { if (gids[i] == gid) return 1; } return 0; } static int rc_in_list(int rc, const int *allowed, size_t count) { size_t i; for (i = 0; i < count; i++) { if (rc == allowed[i]) return 1; } return 0; } static int get_current_groups(gid_t **gids, size_t *count) { int ngroups; *gids = NULL; *count = 0; ngroups = getgroups(0, NULL); if (ngroups < 0) return -1; if (ngroups == 0) return 0; *gids = malloc(sizeof(gid_t) * ngroups); if (*gids == NULL) return -1; ngroups = getgroups(ngroups, *gids); if (ngroups < 0) { free(*gids); *gids = NULL; return -1; } *count = ngroups; return 0; } static int get_natural_groups(uid_t uid, gid_t gid, gid_t **gids, size_t *count) { struct passwd *pw; gid_t *list; int ngroups = 1; int rc; *gids = NULL; *count = 0; pw = getpwuid(uid); if (pw == NULL) return -1; list = malloc(sizeof(gid_t)); if (list == NULL) return -1; rc = getgrouplist(pw->pw_name, gid, list, &ngroups); if (rc == -1) { gid_t *tmp; tmp = realloc(list, sizeof(gid_t) * ngroups); if (tmp == NULL) { free(list); return -1; } list = tmp; rc = getgrouplist(pw->pw_name, gid, list, &ngroups); } if (rc == -1) { free(list); return -1; } *gids = list; *count = ngroups; return 0; } static int merge_groups(const gid_t *base, size_t base_cnt, const gid_t *extra, size_t extra_cnt, gid_t **merged, size_t *merged_cnt) { gid_t *list; size_t i, count = 0, total = base_cnt + extra_cnt; *merged = NULL; *merged_cnt = 0; if (total == 0) return 0; list = malloc(sizeof(gid_t) * total); if (list == NULL) return -1; for (i = 0; i < base_cnt; i++) { if (gid_in_list(list, count, base[i]) == 0) list[count++] = base[i]; } for (i = 0; i < extra_cnt; i++) { if (gid_in_list(list, count, extra[i]) == 0) list[count++] = extra[i]; } *merged = list; *merged_cnt = count; return 0; } static void check_groups(const gid_t *expected, size_t expected_cnt) { gid_t *actual; size_t actual_cnt; size_t i; if (get_current_groups(&actual, &actual_cnt)) fail("Failed to get current additional groups"); if (actual_cnt != expected_cnt) fail("Unexpected additional group count"); for (i = 0; i < expected_cnt; i++) { if (actual[i] != expected[i]) fail("Unexpected additional group value"); } free(actual); } static int choose_extra_gid(const gid_t *skip, size_t skip_cnt, gid_t *gid) { struct group *gr; setgrent(); while ((gr = getgrent()) != NULL) { if (gid_in_list(skip, skip_cnt, gr->gr_gid) == 0) { *gid = gr->gr_gid; endgrent(); return 0; } } endgrent(); return -1; } static int read_bounding_cap(unsigned int cap) { #ifdef PR_CAPBSET_READ return prctl(PR_CAPBSET_READ, cap, 0, 0, 0); #else (void)cap; return -1; #endif } static int find_drop_test_bounding_cap(unsigned int *cap) { unsigned int i; for (i = 0; cap_valid(i); i++) { int rc; if (i == CAP_SETPCAP || i == CAP_SETUID || i == CAP_SETGID) continue; rc = read_bounding_cap(i); if (rc < 0) return -1; if (rc == 1) { *cap = i; return 0; } } for (i = 0; cap_valid(i); i++) { int rc; rc = read_bounding_cap(i); if (rc < 0) return -1; if (rc == 1) { *cap = i; return 0; } } return -1; } static void test_staged_only(void) { static const int allowed[] = { -2, -3, -14 }; gid_t staged[2]; gid_t *natural = NULL; size_t natural_cnt = 0; size_t staged_cnt = 1; int rc; if (capng_get_caps_process()) fail("Failed to initialize libcap-ng state"); if (get_natural_groups(getuid(), getgid(), &natural, &natural_cnt)) fail("Failed to resolve natural groups"); staged[0] = getgid(); if (choose_extra_gid(natural, natural_cnt, &staged[1]) == 0) staged_cnt = 2; free(natural); rc = capng_stage_additional_groups(staged, staged_cnt); if (rc) fail("Failed to stage additional groups"); rc = capng_change_id(-1, -1, CAPNG_APPLY_STAGED_GROUPS); if (rc == 0) check_groups(staged, staged_cnt); else if (rc_in_list(rc, allowed, sizeof(allowed) / sizeof(int)) == 0) fail("Unexpected staged-only return code"); } static void test_init_only(void) { static const int allowed[] = { -2, -3, -5 }; gid_t *natural = NULL; size_t natural_cnt = 0; int rc; if (capng_get_caps_process()) fail("Failed to initialize libcap-ng state"); if (get_natural_groups(getuid(), getgid(), &natural, &natural_cnt)) fail("Failed to resolve natural groups"); rc = capng_change_id(getuid(), -1, CAPNG_INIT_SUPP_GRP); if (rc == 0) check_groups(natural, natural_cnt); else if (rc_in_list(rc, allowed, sizeof(allowed) / sizeof(int)) == 0) fail("Unexpected init-only return code"); free(natural); } static void test_init_and_staged(void) { static const int allowed[] = { -2, -3, -14 }; gid_t staged[2]; gid_t *natural = NULL, *merged = NULL; size_t natural_cnt = 0, merged_cnt = 0; size_t staged_cnt = 1; int rc; if (capng_get_caps_process()) fail("Failed to initialize libcap-ng state"); if (get_natural_groups(getuid(), getgid(), &natural, &natural_cnt)) fail("Failed to resolve natural groups"); staged[0] = getgid(); if (choose_extra_gid(natural, natural_cnt, &staged[1]) == 0) staged_cnt = 2; if (merge_groups(natural, natural_cnt, staged, staged_cnt, &merged, &merged_cnt)) fail("Failed to merge expected additional groups"); rc = capng_stage_additional_groups(staged, staged_cnt); if (rc) fail("Failed to stage additional groups"); rc = capng_change_id(getuid(), -1, CAPNG_INIT_SUPP_GRP | CAPNG_APPLY_STAGED_GROUPS); if (rc == 0) check_groups(merged, merged_cnt); else if (rc_in_list(rc, allowed, sizeof(allowed) / sizeof(int)) == 0) fail("Unexpected init+staged return code"); free(natural); free(merged); } static void test_invalid_drop_and_staged(void) { gid_t gid = getgid(); int rc; if (capng_get_caps_process()) fail("Failed to initialize libcap-ng state"); rc = capng_stage_additional_groups(&gid, 1); if (rc) fail("Failed to stage additional groups"); rc = capng_change_id(-1, -1, CAPNG_DROP_SUPP_GRP | CAPNG_APPLY_STAGED_GROUPS); if (rc != -12) fail("Unexpected drop+staged return code"); } static void test_staged_ignored_without_flag(void) { static const int allowed[] = { -2, -3 }; gid_t gid = getgid(); int rc; if (capng_get_caps_process()) fail("Failed to initialize libcap-ng state"); rc = capng_stage_additional_groups(&gid, 1); if (rc) fail("Failed to stage additional groups"); rc = capng_change_id(-1, -1, CAPNG_NO_FLAG); if (rc && rc_in_list(rc, allowed, sizeof(allowed) / sizeof(int)) == 0) fail("Unexpected return code when staged groups are ignored"); rc = capng_change_id(-1, -1, CAPNG_APPLY_STAGED_GROUPS); if (rc != -13) fail("Staged groups were not cleared when ignored"); } static void test_staged_cleared_after_use(void) { static const int allowed[] = { -2, -3, -14 }; gid_t gid = getgid(); int rc; if (capng_get_caps_process()) fail("Failed to initialize libcap-ng state"); rc = capng_stage_additional_groups(&gid, 1); if (rc) fail("Failed to stage additional groups"); rc = capng_change_id(-1, -1, CAPNG_APPLY_STAGED_GROUPS); if (rc && rc_in_list(rc, allowed, sizeof(allowed) / sizeof(int)) == 0) fail("Unexpected return code when consuming staged groups"); rc = capng_change_id(-1, -1, CAPNG_APPLY_STAGED_GROUPS); if (rc != -13) fail("Staged groups were not cleared after use"); } static void test_apply_bounding_during_change_id(void) { static const int allowed[] = { -2, -3, -8, -9 }; unsigned int cap; int rc; if (capng_get_caps_process()) fail("Failed to initialize libcap-ng state"); if (find_drop_test_bounding_cap(&cap)) return; if (capng_update(CAPNG_DROP, CAPNG_BOUNDING_SET, cap)) fail("Failed to prepare bounding set change"); rc = capng_change_id(-1, -1, CAPNG_APPLY_BOUNDING); if (rc == 0) { if (read_bounding_cap(cap) != 0) fail("Prepared bounding set was not applied"); } else if (rc_in_list(rc, allowed, sizeof(allowed) / sizeof(int)) == 0) fail("Unexpected apply-bounding return code"); } static void test_apply_bounding_noop_without_state(void) { static const int allowed[] = { -2, -3 }; unsigned int cap; int before, rc; if (capng_get_caps_process()) fail("Failed to initialize libcap-ng state"); if (find_drop_test_bounding_cap(&cap)) return; before = read_bounding_cap(cap); if (before != 1) fail("Expected selected bounding capability to be present"); rc = capng_change_id(-1, -1, CAPNG_APPLY_BOUNDING); if (rc == 0) { if (read_bounding_cap(cap) != before) fail("Unprepared bounding set should be a no-op"); } else if (rc_in_list(rc, allowed, sizeof(allowed) / sizeof(int)) == 0) fail("Unexpected no-op apply-bounding return code"); } static void test_invalid_apply_and_clear_bounding(void) { if (capng_get_caps_process()) fail("Failed to initialize libcap-ng state"); if (capng_change_id(-1, -1, CAPNG_APPLY_BOUNDING | CAPNG_CLEAR_BOUNDING) != -17) fail("Unexpected apply+clear bounding return code"); } static void test_apply_bounding_preserves_requested_helper_cap(void) { static const int allowed[] = { -2, -3, -8, -9 }; unsigned int cap; int rc; if (capng_get_caps_process()) fail("Failed to initialize libcap-ng state"); if (find_drop_test_bounding_cap(&cap)) return; if (capng_have_capability(CAPNG_PERMITTED, CAP_SETPCAP) == 0) return; if (capng_update(CAPNG_ADD, CAPNG_EFFECTIVE|CAPNG_PERMITTED, CAP_SETPCAP)) fail("Failed to request CAP_SETPCAP"); if (capng_update(CAPNG_DROP, CAPNG_BOUNDING_SET, cap)) fail("Failed to prepare bounding set change"); rc = capng_change_id(-1, -1, CAPNG_APPLY_BOUNDING); if (rc == 0) { if (capng_have_capability(CAPNG_EFFECTIVE, CAP_SETPCAP) == 0) fail("Requested CAP_SETPCAP was removed from effective"); if (capng_have_capability(CAPNG_PERMITTED, CAP_SETPCAP) == 0) fail("Requested CAP_SETPCAP was removed from permitted"); } else if (rc_in_list(rc, allowed, sizeof(allowed) / sizeof(int)) == 0) fail("Unexpected helper capability cleanup return code"); } static void test_apply_bounding_preserves_permitted_only_helper_cap(void) { static const int allowed[] = { -2, -3, -8, -9 }; unsigned int cap; int rc; if (capng_get_caps_process()) fail("Failed to initialize libcap-ng state"); if (find_drop_test_bounding_cap(&cap)) return; if (capng_have_capability(CAPNG_PERMITTED, CAP_SETPCAP) == 0) return; if (capng_update(CAPNG_DROP, CAPNG_EFFECTIVE, CAP_SETPCAP)) fail("Failed to clear effective CAP_SETPCAP request"); if (capng_update(CAPNG_ADD, CAPNG_PERMITTED, CAP_SETPCAP)) fail("Failed to request permitted CAP_SETPCAP"); if (capng_update(CAPNG_DROP, CAPNG_BOUNDING_SET, cap)) fail("Failed to prepare bounding set change"); rc = capng_change_id(-1, -1, CAPNG_APPLY_BOUNDING); if (rc == 0) { if (capng_have_capability(CAPNG_EFFECTIVE, CAP_SETPCAP)) fail("Permitted-only CAP_SETPCAP became effective"); if (capng_have_capability(CAPNG_PERMITTED, CAP_SETPCAP) == 0) fail("Permitted-only CAP_SETPCAP was removed"); } else if (rc_in_list(rc, allowed, sizeof(allowed) / sizeof(int)) == 0) fail("Unexpected permitted-only helper cleanup return code"); } static void test_bounding_state_ignored_without_flag(void) { static const int allowed[] = { -2, -3 }; unsigned int cap; int rc; if (capng_get_caps_process()) fail("Failed to initialize libcap-ng state"); if (find_drop_test_bounding_cap(&cap)) return; if (capng_update(CAPNG_DROP, CAPNG_BOUNDING_SET, cap)) fail("Failed to prepare bounding set change"); rc = capng_change_id(-1, -1, CAPNG_NO_FLAG); if (rc == 0) { if (read_bounding_cap(cap) != 1) fail("Bounding set changed without CAPNG_APPLY_BOUNDING"); } else if (rc_in_list(rc, allowed, sizeof(allowed) / sizeof(int)) == 0) fail("Unexpected backward compatibility return code"); } static void run_test(const char *name, void (*test)(void)) { pid_t pid; int status; fflush(NULL); pid = fork(); if (pid < 0) { perror("fork"); abort(); } if (pid == 0) { test(); _exit(EXIT_SUCCESS); } if (waitpid(pid, &status, 0) < 0) { perror("waitpid"); abort(); } if (WIFEXITED(status) == 0 || WEXITSTATUS(status) != EXIT_SUCCESS) { printf("Failed %s test\n", name); abort(); } } int main(void) { puts("Doing capng_change_id additional group tests..."); run_test("staged-only additional groups", test_staged_only); run_test("init-only additional groups", test_init_only); run_test("init+staged additional groups", test_init_and_staged); run_test("invalid drop+staged combination", test_invalid_drop_and_staged); run_test("staged groups ignored without flag", test_staged_ignored_without_flag); run_test("staged state cleared after use", test_staged_cleared_after_use); run_test("apply prepared bounding set", test_apply_bounding_during_change_id); run_test("apply bounding is no-op without prepared state", test_apply_bounding_noop_without_state); run_test("invalid apply+clear bounding combination", test_invalid_apply_and_clear_bounding); run_test("helper cleanup keeps requested capability", test_apply_bounding_preserves_requested_helper_cap); run_test("helper cleanup keeps permitted-only capability", test_apply_bounding_preserves_permitted_only_helper_cap); run_test("prepared bounding state ignored without flag", test_bounding_state_ignored_without_flag); return EXIT_SUCCESS; } stevegrubb-libcap-ng-9ae4440/src/test/file_caps_test.c000066400000000000000000000153051524174023300227530ustar00rootroot00000000000000/* file_caps_test.c -- filesystem capability loading tests * Copyright 2026 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 program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA. */ #include "config.h" #include "../cap-ng.h" #include #include #include #include #include #include #include #include #ifdef HAVE_SYS_XATTR_H # include #elif defined(HAVE_ATTR_XATTR_H) # include #endif #if defined(HAVE_SYS_XATTR_H) || defined(HAVE_ATTR_XATTR_H) enum mock_revision { MOCK_REVISION_1, MOCK_REVISION_2, #ifdef VFS_CAP_REVISION_3 MOCK_REVISION_3, #endif }; static enum mock_revision mock_revision; #ifdef VFS_CAP_REVISION_3 static struct vfs_ns_cap_data written_data; #else static struct vfs_cap_data written_data; #endif static size_t written_size; static unsigned int write_count; static __le32 cpu_to_le32(uint32_t value) { #if __BYTE_ORDER == __BIG_ENDIAN return bswap_32(value); #else return value; #endif } static uint32_t le32_to_cpu(__le32 value) { #if __BYTE_ORDER == __BIG_ENDIAN return bswap_32(value); #else return value; #endif } /* * Return only the selected ABI prefix. Poisoning the remainder makes any * access beyond the returned length deterministic, like a dirty stack slot. */ ssize_t fgetxattr(int fd, const char *name, void *value, size_t size) { #ifdef VFS_CAP_REVISION_3 struct vfs_ns_cap_data data = { 0 }; #else struct vfs_cap_data data = { 0 }; #endif size_t xattr_size; (void)fd; if (strcmp(name, "security.capability") != 0) { errno = ENODATA; return -1; } #ifdef VFS_CAP_REVISION_3 if (mock_revision == MOCK_REVISION_3) { data.magic_etc = cpu_to_le32(VFS_CAP_REVISION_3 | VFS_CAP_FLAGS_EFFECTIVE); data.data[0].permitted = cpu_to_le32(1U << CAP_CHOWN); data.data[1].permitted = cpu_to_le32(1U); data.data[1].inheritable = cpu_to_le32(2U); data.rootid = cpu_to_le32(4242U); xattr_size = XATTR_CAPS_SZ_3; } else #endif if (mock_revision == MOCK_REVISION_2) { data.magic_etc = cpu_to_le32(VFS_CAP_REVISION_2); data.data[0].permitted = cpu_to_le32(1U << CAP_CHOWN); data.data[0].inheritable = cpu_to_le32(1U << CAP_KILL); data.data[1].permitted = cpu_to_le32(4U); data.data[1].inheritable = cpu_to_le32(8U); xattr_size = XATTR_CAPS_SZ_2; } else { data.magic_etc = cpu_to_le32(VFS_CAP_REVISION_1 | VFS_CAP_FLAGS_EFFECTIVE); data.data[0].permitted = cpu_to_le32(1U << CAP_CHOWN); data.data[0].inheritable = cpu_to_le32(1U << CAP_KILL); xattr_size = XATTR_CAPS_SZ_1; } if (size < xattr_size) { errno = ERANGE; return -1; } memset(value, 0xFF, size); memcpy(value, &data, xattr_size); return xattr_size; } int fsetxattr(int fd, const char *name, const void *value, size_t size, int flags) { (void)fd; (void)flags; if (strcmp(name, "security.capability") != 0 || size > sizeof(written_data)) { errno = EINVAL; return -1; } memset(&written_data, 0, sizeof(written_data)); memcpy(&written_data, value, size); written_size = size; write_count++; return 0; } static void check_numeric_caps(const char *expected) { char *actual; actual = capng_print_caps_numeric(CAPNG_PRINT_BUFFER, CAPNG_SELECT_CAPS); if (actual == NULL || strcmp(actual, expected) != 0) { fprintf(stderr, "Unexpected capability state:\n%s", actual == NULL ? "(null)\n" : actual); free(actual); abort(); } free(actual); } static void check_written_caps(int fd, uid_t rootid, uint32_t revision, size_t expected_size) { uint32_t magic; capng_clear(CAPNG_SELECT_CAPS); if (capng_update(CAPNG_ADD, CAPNG_EFFECTIVE | CAPNG_PERMITTED, CAP_CHOWN) != 0) abort(); if (capng_set_rootid(rootid) != 0) abort(); if (capng_get_rootid() != rootid) abort(); written_size = 0; write_count = 0; if (capng_apply_caps_fd(fd) != 0) abort(); if (write_count != 1 || written_size != expected_size) abort(); magic = le32_to_cpu(written_data.magic_etc); if (magic != (revision | VFS_CAP_FLAGS_EFFECTIVE)) abort(); if (le32_to_cpu(written_data.data[0].permitted) != (1U << CAP_CHOWN)) abort(); if (le32_to_cpu(written_data.data[0].inheritable) != 0) abort(); #ifdef VFS_CAP_REVISION_3 if (revision == VFS_CAP_REVISION_3 && le32_to_cpu(written_data.rootid) != (uint32_t)rootid) abort(); #endif } int main(void) { FILE *file; int fd; #ifdef VFS_CAP_REVISION_3 mock_revision = MOCK_REVISION_3; if (capng_get_caps_fd(-1) != 0) { puts("Failed loading revision 3 file capabilities"); abort(); } check_numeric_caps("Effective: 00000003, 00000001\n" "Permitted: 00000001, 00000001\n" "Inheritable: 00000002, 00000000\n"); if (capng_get_rootid() != 4242) { puts("Failed loading revision 3 rootid"); abort(); } #endif mock_revision = MOCK_REVISION_1; if (capng_get_caps_fd(-1) != 0) { puts("Failed loading revision 1 file capabilities"); abort(); } check_numeric_caps("Effective: 00000000, 00000021\n" "Permitted: 00000000, 00000001\n" "Inheritable: 00000000, 00000020\n"); if (capng_get_rootid() != CAPNG_UNSET_ROOTID) { puts("Revision 1 load retained a rootid"); abort(); } mock_revision = MOCK_REVISION_2; if (capng_get_caps_fd(-1) != 0) { puts("Failed loading revision 2 file capabilities"); abort(); } check_numeric_caps("Effective: 00000000, 00000000\n" "Permitted: 00000004, 00000001\n" "Inheritable: 00000008, 00000020\n"); if (capng_get_rootid() != CAPNG_UNSET_ROOTID) { puts("Revision 2 load retained a rootid"); abort(); } file = tmpfile(); if (file == NULL) abort(); fd = fileno(file); check_written_caps(fd, CAPNG_UNSET_ROOTID, VFS_CAP_REVISION_2, XATTR_CAPS_SZ_2); #ifdef VFS_CAP_REVISION_3 check_written_caps(fd, 0, VFS_CAP_REVISION_3, XATTR_CAPS_SZ_3); check_written_caps(fd, 4242, VFS_CAP_REVISION_3, XATTR_CAPS_SZ_3); check_written_caps(fd, (uid_t)UINT32_C(0x80000000), VFS_CAP_REVISION_3, XATTR_CAPS_SZ_3); check_written_caps(fd, CAPNG_UNSET_ROOTID, VFS_CAP_REVISION_2, XATTR_CAPS_SZ_2); #endif fclose(file); return EXIT_SUCCESS; } #else int main(void) { return 77; } #endif stevegrubb-libcap-ng-9ae4440/src/test/lib_test.c000066400000000000000000000236651524174023300216040ustar00rootroot00000000000000/* lib_test.c -- simple libcap-ng test suite * Copyright 2009,2012-13 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 program; see the file COPYING.LIB. 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 "../cap-ng.h" #include #include #include #include #include #include #include #include struct proc_caps { uint64_t effective; uint64_t permitted; uint64_t inheritable; }; static unsigned int get_last_cap(void) { int fd; fd = open("/proc/sys/kernel/cap_last_cap", O_RDONLY); if (fd == -1) { return CAP_LAST_CAP; } else { char buf[8]; int num = read(fd, buf, sizeof(buf)); if (num > 0) { errno = 0; unsigned int val = strtoul(buf, NULL, 10); if (errno == 0) { close(fd); return val; } } close(fd); } return CAP_LAST_CAP; } static int read_process_caps(struct proc_caps *caps) { FILE *f; char buf[128]; unsigned long long val; int found = 0; memset(caps, 0, sizeof(*caps)); f = fopen("/proc/self/status", "re"); if (f == NULL) return -1; while (fgets(buf, sizeof(buf), f)) { if (sscanf(buf, "CapEff:\t%llx", &val) == 1) { caps->effective = val; found |= 1; } else if (sscanf(buf, "CapPrm:\t%llx", &val) == 1) { caps->permitted = val; found |= 2; } else if (sscanf(buf, "CapInh:\t%llx", &val) == 1) { caps->inheritable = val; found |= 4; } } fclose(f); return found == 7 ? 0 : -1; } static int expected_cap(uint64_t set, unsigned int capability) { return (set & (1ULL << capability)) ? 1 : 0; } static capng_results_t expected_caps_result(uint64_t set, unsigned int last) { unsigned int i; int found = 0, missing = 0; for (i = 0; i <= last; i++) { if (expected_cap(set, i)) found = 1; else missing = 1; } if (!found) return CAPNG_NONE; if (!missing) return CAPNG_FULL; return CAPNG_PARTIAL; } int main(void) { int rc; unsigned int i, len, last = get_last_cap(); struct proc_caps caps; char expected[128]; char *text; const char *name; void *saved; puts("Doing process capability tests..."); if (read_process_caps(&caps)) { puts("Failed reading process capabilities from procfs"); abort(); } if (capng_get_caps_process()) { puts("Failed getting process capabilities"); abort(); } for (i = 0; i <= last; i++) { if (capng_have_capability(CAPNG_PERMITTED, i) != expected_cap(caps.permitted, i)) { puts("Failed process permitted capabilities test"); abort(); } if (capng_have_capability(CAPNG_EFFECTIVE, i) != expected_cap(caps.effective, i)) { puts("Failed process effective capabilities test"); abort(); } if (capng_have_capability(CAPNG_INHERITABLE, i) != expected_cap(caps.inheritable, i)) { puts("Failed process inheritable capabilities test"); abort(); } } if (capng_have_permitted_capabilities() != expected_caps_result(caps.permitted, last)) { puts("Failed process permitted capabilities aggregate test"); abort(); } if (capng_have_capabilities(CAPNG_SELECT_CAPS) != expected_caps_result(caps.effective, last)) { puts("Failed process effective capabilities aggregate test"); abort(); } text = capng_print_caps_numeric(CAPNG_PRINT_BUFFER, CAPNG_SELECT_CAPS); if (text == NULL) { puts("Failed getting process numeric capabilities"); abort(); } snprintf(expected, sizeof(expected), "Effective: %08X, %08X\n" "Permitted: %08X, %08X\n" "Inheritable: %08X, %08X\n", (unsigned int)(caps.effective >> 32), (unsigned int)(caps.effective & 0xFFFFFFFFU), (unsigned int)(caps.permitted >> 32), (unsigned int)(caps.permitted & 0xFFFFFFFFU), (unsigned int)(caps.inheritable >> 32), (unsigned int)(caps.inheritable & 0xFFFFFFFFU)); if (strcmp(text, expected)) { snprintf(expected, sizeof(expected), "Effective: %08X\n" "Permitted: %08X\n" "Inheritable: %08X\n", (unsigned int)caps.effective, (unsigned int)caps.permitted, (unsigned int)caps.inheritable); if (strcmp(text, expected)) { puts("Failed process numeric capabilities test"); free(text); abort(); } } free(text); puts("Doing basic bit tests..."); capng_clear(CAPNG_SELECT_BOTH); errno = 0; rc = capng_apply(0); if (rc != -1 || errno != EINVAL) { puts("Failed apply empty selection test"); abort(); } if (capng_have_permitted_capabilities() != CAPNG_NONE) { puts("Failed permitted capabilities none test"); abort(); } saved = capng_save_state(); capng_fill(CAPNG_SELECT_BOTH); if (capng_have_permitted_capabilities() != CAPNG_FULL) { puts("Failed permitted capabilities full test"); abort(); } capng_restore_state(&saved); capng_clear(CAPNG_SELECT_BOTH); rc = capng_update(CAPNG_ADD, CAPNG_PERMITTED, CAP_CHOWN); if (rc) { puts("Failed update permitted test"); abort(); } if (capng_have_permitted_capabilities() != CAPNG_PARTIAL) { puts("Failed permitted capabilities partial test"); abort(); } if (capng_have_capabilities(CAPNG_SELECT_BOTH) != CAPNG_NONE) { puts("Failed clearing capabilities"); abort(); } saved = capng_save_state(); capng_fill(CAPNG_SELECT_BOTH); if (capng_have_capabilities(CAPNG_SELECT_BOTH) != CAPNG_FULL) { puts("Failed filling capabilities"); abort(); } // Need to detect if version 1 or 2 capabilities text = capng_print_caps_numeric(CAPNG_PRINT_BUFFER, CAPNG_SELECT_CAPS); len = strlen(text); free(text); if (len < 80 && last > 30) // The kernel & headers are mismatched last = 30; // Now test that restore still works capng_restore_state(&saved); if (capng_have_capabilities(CAPNG_SELECT_BOTH) != CAPNG_NONE) { puts("Failed restoring capabilities"); abort(); } printf("Doing advanced bit tests for %u capabilities...\n", last); for (i=0; i<=last; i++) { capng_clear(CAPNG_SELECT_BOTH); rc = capng_update(CAPNG_ADD, CAPNG_EFFECTIVE, i); if (rc) { puts("Failed update test 1"); abort(); } rc = capng_have_capability(CAPNG_EFFECTIVE, i); if (rc == 0) { puts("Failed have capability test 1"); capng_print_caps_numeric(CAPNG_PRINT_STDOUT, CAPNG_SELECT_CAPS); abort(); } if(capng_have_capabilities(CAPNG_SELECT_CAPS)!=CAPNG_PARTIAL){ puts("Failed have capabilities test 1"); capng_print_caps_numeric(CAPNG_PRINT_STDOUT, CAPNG_SELECT_CAPS); abort(); } #if CAP_LAST_CAP > 31 rc = capng_update(CAPNG_ADD, CAPNG_BOUNDING_SET, i); if (rc) { puts("Failed bset update test 2"); abort(); } rc = capng_have_capability(CAPNG_BOUNDING_SET, i); if (rc == 0) { puts("Failed bset have capability test 2"); capng_print_caps_numeric(CAPNG_PRINT_STDOUT, CAPNG_SELECT_BOTH); abort(); } if(capng_have_capabilities(CAPNG_SELECT_BOUNDS)!=CAPNG_PARTIAL){ puts("Failed bset have capabilities test 2"); capng_print_caps_numeric(CAPNG_PRINT_STDOUT, CAPNG_SELECT_BOTH); abort(); } #endif text=capng_print_caps_text(CAPNG_PRINT_BUFFER, CAPNG_EFFECTIVE); if (text == NULL) { puts("Failed getting print text to buffer"); abort(); } name = capng_capability_to_name(i); if (name == NULL) { printf("Failed converting capability %u to name\n", i); abort(); } if (strcmp(text, name)) { puts("Failed print text comparison"); printf("%s != %s\n", text, name); abort(); } free(text); // Now make sure the mask part is working capng_fill(CAPNG_SELECT_BOTH); rc = capng_update(CAPNG_DROP, CAPNG_EFFECTIVE, i); if (rc) { puts("Failed update test 3"); abort(); } // Should be partial if(capng_have_capabilities(CAPNG_SELECT_CAPS)!=CAPNG_PARTIAL){ puts("Failed have capabilities test 3"); capng_print_caps_numeric(CAPNG_PRINT_STDOUT, CAPNG_SELECT_CAPS); abort(); } // Add back the bit and should be full capabilities rc = capng_update(CAPNG_ADD, CAPNG_EFFECTIVE, i); if (rc) { puts("Failed update test 4"); abort(); } if (capng_have_capabilities(CAPNG_SELECT_CAPS) != CAPNG_FULL){ puts("Failed have capabilities test 4"); capng_print_caps_numeric(CAPNG_PRINT_STDOUT, CAPNG_SELECT_CAPS); abort(); } } // Verify text formatting when many capabilities are present capng_clear(CAPNG_SELECT_BOTH); for (i=0; i<=last; i++) { rc = capng_update(CAPNG_ADD, CAPNG_EFFECTIVE, i); if (rc) { puts("Failed setup test for print buffer length"); abort(); } } text = capng_print_caps_text(CAPNG_PRINT_BUFFER, CAPNG_EFFECTIVE); if (text == NULL) { puts("Failed getting full print text to buffer"); abort(); } len = 0; for (i=0; i<=last; i++) { if (capng_have_capability(CAPNG_EFFECTIVE, i)) { name = capng_capability_to_name(i); if (name == NULL) name = "unknown"; if (len) len += 2; len += strlen(name); } } if (strlen(text) != len) { puts("Failed print text length comparison"); printf("%zu != %u\n", strlen(text), len); abort(); } free(text); // Now test the updatev function capng_clear(CAPNG_SELECT_BOTH); rc = capng_updatev(CAPNG_ADD, CAPNG_EFFECTIVE, CAP_CHOWN, CAP_FOWNER, CAP_KILL, -1); if (rc) { puts("Failed updatev test"); abort(); } rc = capng_have_capability(CAPNG_EFFECTIVE, CAP_CHOWN) && capng_have_capability(CAPNG_EFFECTIVE, CAP_FOWNER) && capng_have_capability(CAPNG_EFFECTIVE, CAP_KILL); if (rc == 0) { puts("Failed have updatev capability test"); capng_print_caps_numeric(CAPNG_PRINT_STDOUT, CAPNG_SELECT_CAPS); abort(); } return EXIT_SUCCESS; } stevegrubb-libcap-ng-9ae4440/src/test/securebits_test.c000066400000000000000000000102001524174023300231630ustar00rootroot00000000000000/* securebits_test.c -- capng_lock securebits tests * Copyright 2026 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 program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA. */ #include "config.h" #include "../cap-ng.h" #include #include #include #include #include #ifdef HAVE_LINUX_SECUREBITS_H #include #endif #ifndef SECURE_NOROOT #define SECURE_NOROOT 0 #endif #ifndef SECURE_NOROOT_LOCKED #define SECURE_NOROOT_LOCKED 1 #endif #ifndef SECURE_NO_SETUID_FIXUP #define SECURE_NO_SETUID_FIXUP 2 #endif #ifndef SECURE_NO_SETUID_FIXUP_LOCKED #define SECURE_NO_SETUID_FIXUP_LOCKED 3 #endif #if defined(PR_GET_SECUREBITS) && defined(PR_SET_SECUREBITS) #define FUTURE_SECUREBIT (1U << 20) #define LOCK_SECUREBITS (1U << SECURE_NOROOT | \ 1U << SECURE_NOROOT_LOCKED | \ 1U << SECURE_NO_SETUID_FIXUP | \ 1U << SECURE_NO_SETUID_FIXUP_LOCKED) static unsigned int current_securebits; static unsigned long written_securebits; static unsigned int get_count; static unsigned int set_count; static unsigned int no_new_privs_count; static int fail_get; static int fail_set; static int fail_no_new_privs; int prctl(int option, ...) { va_list args; unsigned long arg2; int rc = 0; va_start(args, option); arg2 = va_arg(args, unsigned long); va_end(args); if (option == PR_GET_SECUREBITS) { get_count++; if (fail_get) { errno = EIO; return -1; } return (int)current_securebits; } if (option == PR_SET_SECUREBITS) { set_count++; written_securebits = arg2; if (fail_set) { errno = EPERM; return -1; } current_securebits = (unsigned int)arg2; #ifdef PR_SET_NO_NEW_PRIVS } else if (option == PR_SET_NO_NEW_PRIVS) { no_new_privs_count++; if (fail_no_new_privs) { errno = EPERM; return -1; } #endif } else { errno = EINVAL; rc = -1; } return rc; } static void reset_mock(unsigned int securebits) { current_securebits = securebits; written_securebits = 0; get_count = 0; set_count = 0; no_new_privs_count = 0; fail_get = 0; fail_set = 0; fail_no_new_privs = 0; } static void check(int condition, const char *message) { if (condition == 0) { fprintf(stderr, "%s\n", message); abort(); } } int main(void) { unsigned int expected = FUTURE_SECUREBIT | LOCK_SECUREBITS; reset_mock(FUTURE_SECUREBIT); check(capng_lock() == 0, "Failed locking securebits"); check(get_count == 1, "Securebits were not read"); check(set_count == 1, "Securebits were not written"); check(written_securebits == expected, "Existing securebits were not preserved"); #ifdef PR_SET_NO_NEW_PRIVS check(no_new_privs_count == 1, "No-new-privileges was not set"); #endif reset_mock(expected); check(capng_lock() == 0, "Idempotent lock failed"); check(get_count == 1, "Idempotent lock did not read securebits"); check(set_count == 0, "Idempotent lock rewrote securebits"); reset_mock(0); fail_get = 1; check(capng_lock() == -1, "Securebits read failure was not reported"); check(set_count == 0, "Securebits were written after a read failure"); reset_mock(0); fail_set = 1; check(capng_lock() == -1, "Securebits write failure was not reported"); #ifdef PR_SET_NO_NEW_PRIVS reset_mock(expected); fail_no_new_privs = 1; check(capng_lock() == -2, "No-new-privileges failure was not reported"); reset_mock(0); fail_get = 1; fail_no_new_privs = 1; check(capng_lock() == -3, "Combined failures were not reported"); #endif return EXIT_SUCCESS; } #else int main(void) { return 77; } #endif stevegrubb-libcap-ng-9ae4440/src/test/thread_test.c000066400000000000000000000027001524174023300222700ustar00rootroot00000000000000#include "config.h" #include #include #include #include //#define DEBUG 1 pthread_t thread1, thread2; void *thread1_main(void *arg) { capng_fill(CAPNG_SELECT_BOTH); #ifdef DEBUG printf("thread1 filled capabilities\n"); #endif sleep(2); if (capng_have_capabilities(CAPNG_SELECT_CAPS) < CAPNG_FULL) { printf("Capabilities missing when there should be some\n"); exit(1); } #ifdef DEBUG printf("SUCCESS: Full capabilities reported\n"); #endif return NULL; } void *thread2_main(void *arg) { sleep(1); #ifdef DEBUG printf("thread2 getting capabilities\n"); #endif if (capng_get_caps_process()) { printf("Unable to get process capabilities"); exit(1); } if (capng_have_capabilities(CAPNG_SELECT_CAPS) != CAPNG_NONE) { printf("Detected capabilities when there should not be any\n"); exit(1); } capng_clear(CAPNG_SELECT_BOTH); #ifdef DEBUG printf("SUCCESS: No capabilities reported\n"); #endif return NULL; } int main(void) { // This test must be run as root which naturally has all capabilities // set. So, we need to clear the capabilities so that we can see if // the test works. capng_clear(CAPNG_SELECT_CAPS); if (capng_apply(CAPNG_SELECT_CAPS)) { printf("Clearing capabilities failed"); return 1; } printf("Testing thread separation of capabilities\n"); pthread_create(&thread1, NULL, thread1_main, NULL); pthread_create(&thread2, NULL, thread2_main, NULL); sleep(3); return 0; } stevegrubb-libcap-ng-9ae4440/utils/000077500000000000000000000000001524174023300172115ustar00rootroot00000000000000stevegrubb-libcap-ng-9ae4440/utils/Makefile.am000066400000000000000000000050561524174023300212530ustar00rootroot00000000000000# Makefile.am -- # Copyright 2009,2015,2022,2024,2026 Red Hat Inc. # All Rights Reserved. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; 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 = *.loT *.rej *.orig DISTCLEANFILES = netcap.8 SUBDIRS = test cap-audit DIST_SUBDIRS = test cap-audit EXTRA_DIST = pscap.8 filecap.8 netcap.8.in captest.c captest.8 \ libcap-ng.bash_completion noinst_HEADERS = gcc-attributes.h AM_CPPFLAGS = -I${top_srcdir} -I${top_srcdir}/src LDADD = ${top_builddir}/src/libcap-ng.la AM_CFLAGS = -W -Wall -Wshadow ${WFLAGS} -Wundef -D_GNU_SOURCE NETCAP_ADVANCED_ENABLED = @NETCAP_ADVANCED_ENABLED@ bin_PROGRAMS = pscap netcap filecap man_MANS = pscap.8 netcap.8 filecap.8 bashcompletiondir = ${datadir}/bash-completion/completions bashcompletioncommands = cap-audit filecap netcap pscap pscap_SOURCES = pscap.c proc-account.c proc-account.h \ proc-output.c proc-output.h proc-sanitize.c proc-sanitize.h \ proc-status.c proc-status.h netcap_SOURCES = netcap.c netcap-advanced.h proc-llist.c \ proc-llist.h proc-account.c proc-account.h proc-sanitize.c \ proc-sanitize.h if BUILD_NETCAP_ADVANCED netcap_SOURCES += netcap-advanced.c proc-output.c proc-output.h \ proc-status.c proc-status.h endif filecap_SOURCES = filecap.c filecap-path.c filecap-path.h if BUILD_DEPRECATED bin_PROGRAMS += captest man_MANS += captest.8 captest_SOURCES = captest.c endif install-exec-hook: $(MKDIR_P) ${DESTDIR}${bashcompletiondir} $(INSTALL_DATA) ${srcdir}/libcap-ng.bash_completion \ ${DESTDIR}${bashcompletiondir}/libcap-ng.bash_completion cd ${DESTDIR}${bashcompletiondir} && \ for command in $(bashcompletioncommands); do \ $(LN_S) -f libcap-ng.bash_completion $$command || exit 1; \ done uninstall-hook: for command in $(bashcompletioncommands); do \ rm -f ${DESTDIR}${bashcompletiondir}/$$command; \ done rm -f ${DESTDIR}${bashcompletiondir}/libcap-ng.bash_completion stevegrubb-libcap-ng-9ae4440/utils/cap-audit/000077500000000000000000000000001524174023300210605ustar00rootroot00000000000000stevegrubb-libcap-ng-9ae4440/utils/cap-audit/Makefile.am000066400000000000000000000047311524174023300231210ustar00rootroot00000000000000# Makefile.am -- cap-audit utility # # Copyright 2026 Red Hat Inc. # All Rights Reserved. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public # License as published by the Free Software Foundation; either # version 2.1 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. # # Authors: # Steve Grubb # SUBDIRS = test DIST_SUBDIRS = test AM_CPPFLAGS = -I${top_srcdir} -I${top_srcdir}/src \ -I$(top_srcdir)/utils \ -I$(top_builddir)/utils/cap-audit ${LIBBPF_CFLAGS} ${LIBAUDIT_CFLAGS} AM_CFLAGS = -W -Wall -Wshadow ${WFLAGS} -Wundef -D_GNU_SOURCE if BUILD_CAP_AUDIT bin_PROGRAMS = cap-audit cap_audit_SOURCES = cap_audit.c \ cap_audit_analyze.c \ cap_audit_capset.c \ cap_audit_event.c \ cap_audit_json.c \ cap_audit_outcome.c \ cap_audit_path.c \ cap_audit_path.h \ cap_audit_service.c \ classify_app.c \ classify_app.h \ cap_audit_util.c \ cap_audit_yaml.c \ cap_audit.h nodist_cap_audit_SOURCES = cap_audit.skel.h cap_audit_LDADD = ${top_builddir}/src/libcap-ng.la ${LIBBPF_LIBS} ${LIBAUDIT_LIBS} endif EXTRA_DIST = cap_audit.bpf.c cap-audit.8 CLEANFILES = cap_audit.bpf.o cap_audit.skel.h vmlinux.h if BUILD_CAP_AUDIT BUILT_SOURCES = cap_audit.bpf.o cap_audit.skel.h vmlinux.h cap_audit.$(OBJEXT) cap_audit_analyze.$(OBJEXT) \ cap_audit_capset.$(OBJEXT) cap_audit_event.$(OBJEXT) \ cap_audit_json.$(OBJEXT) \ cap_audit_outcome.$(OBJEXT) cap_audit_service.$(OBJEXT) \ cap_audit_util.$(OBJEXT) \ cap_audit_yaml.$(OBJEXT): cap_audit.skel.h endif BPF_ARCH = @BPF_ARCH@ BPFTOOL = @BPFTOOL@ CLANG = @CLANG@ VMLINUX_H_PATH = @VMLINUX_H_PATH@ BPF_CFLAGS = -g -O2 -target bpf -D__TARGET_ARCH_${BPF_ARCH} ${AM_CPPFLAGS} cap_audit.skel.h: cap_audit.bpf.o $(AM_V_GEN)$(BPFTOOL) gen skeleton $< > $@ cap_audit.bpf.o: cap_audit.bpf.c vmlinux.h $(AM_V_CC)$(CLANG) $(BPF_CFLAGS) -c $< -o $@ if PROVIDED_VMLINUX_H # Reproducible builds: use the vmlinux.h supplied at configure time via # --with-vmlinux-h-path instead of reading the running kernel's BTF data. vmlinux.h: $(VMLINUX_H_PATH) Makefile $(AM_V_GEN)cp $< $@ else vmlinux.h: Makefile $(AM_V_GEN)$(BPFTOOL) btf dump file /sys/kernel/btf/vmlinux format c > $@ endif if BUILD_CAP_AUDIT man_MANS = cap-audit.8 endif stevegrubb-libcap-ng-9ae4440/utils/cap-audit/cap-audit.8000066400000000000000000000155411524174023300230260ustar00rootroot00000000000000.\" SPDX-License-Identifier: GPL-2.0-or-later .\" Copyright (c) 2026 Red Hat Inc. .TH CAP-AUDIT "8" "August 2026" "Red Hat" "System Administration Utilities" .SH NAME cap-audit \- discover the minimal capability set for a program .SH SYNOPSIS .B cap-audit .RI [ options ] .B -- .I command .RI [ args ...] .br .B cap-audit .BR --service .I file .RI [ -- " command-or-args" ...] .SH DESCRIPTION \fBcap-audit\fP traces a target process and its descendants to record every kernel capability check it performs. The tool uses eBPF to hook capability verification paths in the kernel and libcap-ng to format the results. Events are filtered to only the traced process tree to reduce overhead. .PP The auditor fork/execs the requested command, registers its PID for tracing before exec, and automatically tracks child PIDs. At the end of execution it reports which capability checks returned granted or not granted and presents deployment snippets showing how to grant the minimal set. .PP For syscall-associated checks returning not granted, cap-audit correlates the check with the raw kernel syscall return value when available. A syscall invocation may contain more than one such check, so capability-check and syscall-outcome counts are independent. Successful syscalls show that the not-granted check did not prevent that invocation from completing. EPERM and EACCES results are reported as permission-related failures requiring manual investigation. Other failures are retained as diagnostic evidence. EINTR and kernel restart results are treated as interruption evidence: interruption alone is inconclusive, while a mix of interrupted and successful invocations requires manual investigation because cap-audit cannot distinguish call sites or arguments. Capabilities with only not-granted checks are never added to automatic recommendations; users should add one only after confirming that required functionality fails because it is absent. .PP The capability masks installed by successful .BR capset (2) calls are also recorded. A capability requested by capset without a confirmed granted check is reported under CAPSET-ONLY CAPABILITIES rather than being classified as required. Deployment snippets retain it as a current-binary compatibility constraint because removing it from the deployment boundary can cause the application's capset to fail. To remove such a capability, first remove it from the application's capability setup, exercise representative code paths in additional runs, and then narrow the deployment boundary. Failed capset calls do not establish this constraint. .PP Runtime tracing requires root or the CAP_BPF, CAP_PERFMON, and CAP_SYS_PTRACE capabilities, along with kernel BTF data in \fI/sys/kernel/btf/vmlinux\fP. .SH OPTIONS .TP .BR -h , " --help" Show a help message and exit. .TP .BR -v , " --verbose" Print each capability check as it occurs. .TP .BR -j , " --json" Emit the analysis as JSON. .TP .BR -y , " --yaml" Emit the analysis as YAML. .TP .BR -s , " --service " \fIFILE\fP Parse a systemd service unit file, simulate the service user, group, supplementary group, capability, and NoNewPrivileges settings in the traced child, and emit service-specific recommendations. Without an explicit command after .BR -- , the parsed .B ExecStart= command is traced. With .BR -- , a command can be supplied explicitly; if the first supplied word starts with .BR - , it is treated as replacement arguments for the parsed .B ExecStart= executable. .PP Capabilities explicitly configured in the unit but not observed during the trace are retained in a current-service-compatible recommendation. They are also identified as removal candidates because the relevant functionality may not have been exercised. Cap-audit does not generate a configuration that removes them; targeted testing or manual review must first confirm that each configured capability is unnecessary. .PP The generated service configuration preserves the .B NoNewPrivileges= setting used for the audit. When the setting is disabled, enabling it has not been validated by the trace and must be tested separately, including service functionality that starts child processes or helper programs. .PP Cap-audit does not load the environment configured by .B Environment= or .BR EnvironmentFile= . A standalone .B $NAME argument in .B ExecStart= is therefore treated as unset and omitted. Supply an explicit command after .B -- when a non-empty expansion is needed for the audit. .PP A single .B ! prefix on the .B ExecStart= executable is supported. It leaves the configured user, group, and supplementary group changes to the executed program while continuing to apply the configured capability and .B NoNewPrivileges= settings. The fully privileged .B + prefix and the legacy .B !! prefix are rejected because cap-audit cannot safely simulate their execution semantics. .PP If a capability check targets a user namespace different from the namespace in which cap-audit started, the observation is retained in the diagnostic output but automatic capability recommendations are suppressed. The safe scope of a capability grant cannot be determined from the target namespace alone. .TP .BR -- End option parsing and treat the rest of the command line as the program to execute under audit. .SH EXIT STATUS The program returns 0 on success. Non-zero values indicate an error occurred while setting up tracing or executing the target command. .SH NOTES 1. This utility observes the application behavior to decide what is needed. If you do not exercise all functionality, it can result in an incomplete inventory of needed capabilities. If it can't see it, cap-audit can't record it. 2. There are hard memory constraints for event collection. The limit for the number of children that can be traced is 8192. If an application is fork heavy, such as shell scripts or spawns a lot of short lived helper applications, the tracer will stop collecting events on new children once the buffer fills. Also, the tracer only has space to hold 20k unique stack traces. This is not as problematic as the other constraint as the same code paths often repeat. If this fills up, new code paths will not be collected. This buffer is used to map a capability request to the syscall needing it. Knowing the syscall causing the needed capability is a "nice to have". What's more important is the capability needed - which is in a different memory area and safe from the described issue. In both cases, there is no indication to user space that the tracer is out of memory. 3. When auditing a daemon, pass options to keep it in the foreground. If it forks and exits, that will end the session and you will only get the capabilities traced up to the exit. Here's an example keeping the daemon in the foreground: .nf .B cap-audit /usr/sbin/sshd -D .fi .PP \fBEXPERIMENTAL:\fP cap-audit output is experimental, but very close. .SH FILES .I /sys/kernel/btf/vmlinux .SH "SEE ALSO" .BR capabilities (7), .BR filecap (8). .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/utils/cap-audit/cap_audit.bpf.c000066400000000000000000000621561524174023300237350ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* * cap_audit.bpf.c - Capture capability checks for a target application * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Authors: * Steve Grubb * Portions of this code were made with codex 5.2 */ #ifdef __clang__ /* * bpftool can emit nameless BTF declarations in vmlinux.h. Clang warns about * those generated declarations even though this program does not use them. */ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wmissing-declarations" #endif #include "vmlinux.h" #ifdef __clang__ #pragma clang diagnostic pop #endif #ifndef CAP_OPT_NOAUDIT #define CAP_OPT_NOAUDIT 2 #endif #include #include #include #define CAP_VERSION_1 0x19980330 #define CAP_VERSION_2 0x20071026 #define CAP_VERSION_3 0x20080522 struct cap_user_header { __u32 version; int pid; }; struct cap_user_data { __u32 effective; __u32 permitted; __u32 inheritable; }; const volatile int capset_syscall_nr = -1; /* * BPF overview: * The BPF side attaches to capability helpers (cap_capable, ns_capable, * capable) and syscall tracepoints to capture capability checks only for a * target process tree. * * The design challenge is noise filtering: distinguish capability checks * caused by the target application from kernel-internal checks running under * the same PID. The first filtering layer lives entirely in this BPF program * and is encoded in target_pids map values. * * target_pids values are phases, not just booleans: * 0 = not traced * 1 = pre-exec (PID registered, but exec transition not complete) * 2 = post-exec (target image is running; capability events are recordable) * * Phase 1 suppresses all capability events for the initial child so checks * between fork() and execve() are dropped. That removes pre-exec noise such * as DAC_READ_SEARCH from PATH traversal while resolving the target binary. * * The transition point is sched_process_exec. That tracepoint fires in * begin_new_exec() after the point-of-no-return where the old image is gone * and the new executable is committed. This is a kernel-level signal, so it * works regardless of libc/toolchain choice (glibc, musl, static, scripts). * * Fork inherits the parent's phase value. Children of an already running * target (phase 2) begin recording immediately, while children spawned before * the initial exec remain in phase 1 until their own exec transition. * * raw_syscalls/sys_enter and sys_exit use should_trace_pid() (phase > 0) * so syscall context and completion results are available the instant the * exec gate opens. * Capability hooks use should_record_pid() (phase >= 2), so only post-exec * capability checks are emitted. * * For traced tasks, the program builds cap_event records with task identity, * syscall context, namespace inode, the CAP_OPT_* flags passed to * cap_capable(), and stack id; tracks per-capability statistics; and * streams finalized events to userspace through a ring buffer. A syscall * completion event correlates denied checks from one invocation with its raw * kernel return value. Successful capset completion events also carry the * requested masks so userspace can distinguish a current-binary compatibility * constraint from confirmed capability use. The cap_opts field carries the * CAP_OPT_* flags so userspace can identify known advisory call sites without * treating * CAP_OPT_NOAUDIT as a standalone filter. CAP_OPT_NOAUDIT means "do not * audit" and is only a confirming signal alongside syscall and capability * matching. Fork/exit tracepoints keep the PID filter in sync so children * are traced and exits are pruned. */ #ifndef PERF_MAX_STACK_DEPTH #define PERF_MAX_STACK_DEPTH 127 #endif #if !defined(__TARGET_ARCH_x86) && !defined(__TARGET_ARCH_arm64) && \ !defined(__TARGET_ARCH_arm) && !defined(__TARGET_ARCH_powerpc) && \ !defined(__TARGET_ARCH_s390) && !defined(__TARGET_ARCH_riscv) && \ !defined(__TARGET_ARCH_mips) && !defined(__TARGET_ARCH_loongarch) #if defined(__x86_64__) || defined(__i386__) #define __TARGET_ARCH_x86 #elif defined(__aarch64__) #define __TARGET_ARCH_arm64 #elif defined(__arm__) #define __TARGET_ARCH_arm #elif defined(__powerpc__) #define __TARGET_ARCH_powerpc #elif defined(__s390x__) || defined(__s390__) #define __TARGET_ARCH_s390 #elif defined(__riscv) #define __TARGET_ARCH_riscv #elif defined(__mips__) #define __TARGET_ARCH_mips #elif defined(__loongarch64) #define __TARGET_ARCH_loongarch #else #define __TARGET_ARCH_x86 #endif #endif char LICENSE[] SEC("license") = "GPL"; enum cap_event_type { CAP_EVENT_CHECK, CAP_EVENT_SYSCALL_RESULT, CAP_EVENT_CAPSET, }; struct cap_event { __u64 timestamp_ns; __u32 pid; __u32 tid; __u32 uid; __u32 gid; int capability; int result; int syscall_nr; char comm[TASK_COMM_LEN]; __u64 stack_id; __u32 targ_ns_inum; __u32 cap_opts; __s64 syscall_ret; __u64 denied_caps; __u64 capset_effective; __u64 capset_permitted; __u64 capset_inheritable; __u32 event_type; }; struct cap_stats { __u64 checks; __u64 granted; __u64 denied; }; // This sets the limit for how many child processes can be traced. // Because of this limit, the tracer may not be suitable for shell // scripts or long running process that fork child handlers that // terminate soon after launching. When this fills up, no more // children will be traced. This is the breaking point for long // running apps. The other limits aren't as likely to be broken. // This is approx 16 bytes per entry. (Default uses 128K of memory) struct { __uint(type, BPF_MAP_TYPE_HASH); __type(key, __u32); __type(value, __u8); __uint(max_entries, 8192); } target_pids SEC(".maps"); // This declares the size of the ring buf that holds events for // userspace to access. struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024); } cap_events SEC(".maps"); // This declares how many unique stack traces to hold. This is used // to determine which syscall a capability was requested from. If this // fills up, no more stack traces will be collected. This is about // 1K per entry. (Default uses 20 MB of memory) struct { __uint(type, BPF_MAP_TYPE_STACK_TRACE); __uint(key_size, sizeof(__u32)); __uint(value_size, PERF_MAX_STACK_DEPTH * sizeof(__u64)); __uint(max_entries, 20000); } stack_traces SEC(".maps"); // This declares how many capabilities can be watched. As of the 6.18 // kernel, it only uses 40. So, 64 is future proof as none have been added // in a while. struct { __uint(type, BPF_MAP_TYPE_ARRAY); __type(key, __u32); __type(value, struct cap_stats); __uint(max_entries, 64); } capability_stats SEC(".maps"); // The cap_events_inflight map uses pid_tgid as the key. There is a race // scenario when deep syscall chains that check multiple capabilities // or nested function calls where each checks a capability. In these cases // because it uses the same pid_tgid, it can overwrite a previous event. // example: // // In kernel, during mount(): // sys_mount() { // First check // if (!capable(CAP_SYS_ADMIN)) // ← kprobe #1 fires // return -EPERM; // // Path resolution might trigger // if (!capable(CAP_DAC_OVERRIDE)) // ← kprobe #2 fires BEFORE kretprobe #1! // return -EACCES; // ... more work ... // // return 0; // ← kretprobe #1 and #2 fire //} // // Statistics are SAFE: The capability_stats map is updated immediately in // the kprobe using atomic operations, so counts are always accurate. The // probability is higher for complex syscalls like mount, setuid, or network // operations. // // Possible solutions // Option 1: Per-CPU Map - change to BPF_MAP_TYPE_PERCPU_HASH. Drawback is // it uses a map for each CPU so if 64 cores, map is 64KB of memory. // // Option 2: Include Stack Pointer in Key - // key[0] = bpf_get_current_pid_tgid(); // key[1] = PT_REGS_SP(ctx); // Stack pointer makes it unique // bpf_map_update_elem(&cap_events_inflight, &key, ev, BPF_ANY); // // Option 3: Accept the Race (Current Approach) // Rationale: // * The capability_stats map is always correct (atomic updates) // * Individual event details might be wrong, but aggregate data is right // * For the tool's purpose (determining required capabilities), statistics // are what matter // * Individual events are mainly for debugging/verbose mode struct { __uint(type, BPF_MAP_TYPE_HASH); __type(key, __u64); __type(value, struct cap_event); __uint(max_entries, 1024); } cap_events_inflight SEC(".maps"); // In theory, if sys_exit is not called, a syscall can leak. This can // happen due to SIGKILL or a core dump. This might matter if this is // tracing a long running with many threads some of which get SIGKILL. // Might be more likely if its a child process rather than a thread. // Because each run of the tracer is a new instance, the only concern // is long tracing sessions. If this really was a concern, we could // change to BPF_MAP_TYPE_PERCPU_HASH so that a leak on CPU0 doesn't // affect tracing on CPU1. This is just mentioned here because it is // an esoteric problem and not likely to show up. But this documents // it and a possible solution. The drawback is that it uses more memory. struct syscall_state { __u64 denied_caps; __u64 capset_effective; __u64 capset_permitted; __u64 capset_inheritable; int nr; __u8 capset_valid; }; struct { __uint(type, BPF_MAP_TYPE_HASH); __type(key, __u64); __type(value, struct syscall_state); __uint(max_entries, 4096); } current_syscalls SEC(".maps"); static __always_inline int get_pid_phase(__u32 pid) { __u8 *val = bpf_map_lookup_elem(&target_pids, &pid); return val ? *val : 0; } /* * should_trace_pid - check if the current PID is in the target set. * @pid: process ID of the current task. * * Looks up the PID in target_pids and returns 1 when tracing is enabled for * it, otherwise 0. */ static __always_inline int should_trace_pid(__u32 pid) { return get_pid_phase(pid) > 0; } /* * should_record_pid - check if the current PID is post-exec. * @pid: process ID of the current task. * * Returns 1 for traced PIDs that have completed exec. */ static __always_inline int should_record_pid(__u32 pid) { return get_pid_phase(pid) >= 2; } /* * record_stats - increment capability check count. * @cap: capability number. * * Increments the "checks" counter for the capability in capability_stats. * Out-of-range capability numbers are ignored. Returns nothing. */ static __always_inline void record_stats(int cap) { __u32 key; struct cap_stats *stats; if (cap < 0 || cap >= 64) return; key = (__u32)cap; stats = bpf_map_lookup_elem(&capability_stats, &key); if (stats) __sync_fetch_and_add(&stats->checks, 1); else { struct cap_stats new_stats = { 0 }; new_stats.checks = 1; if (!bpf_map_update_elem(&capability_stats, &key, &new_stats, BPF_NOEXIST)) return; stats = bpf_map_lookup_elem(&capability_stats, &key); if (stats) __sync_fetch_and_add(&stats->checks, 1); } } /* * update_result_stats - record whether a capability check succeeded. * @cap: capability number from the in-flight event. * @ret: return value from the capability helper (0 = granted). * * Updates the granted/denied counters for the capability when a matching * entry already exists. Out-of-range capability numbers are ignored. */ static __always_inline void update_result_stats(int cap, int ret) { __u32 key; struct cap_stats *stats; if (cap < 0 || cap >= 64) return; key = (__u32)cap; stats = bpf_map_lookup_elem(&capability_stats, &key); if (!stats) return; if (!ret) __sync_fetch_and_add(&stats->granted, 1); else __sync_fetch_and_add(&stats->denied, 1); } /* * read_syscall - fetch the syscall number for the current task. * @ctx: pt_regs provided by the kprobe. * * Uses a per-thread map populated by sys_enter tracepoints when available, * and falls back to architecture-specific pt_regs fields. Returns the syscall * number or -1 when it cannot be determined. */ static __always_inline int read_syscall(struct pt_regs *ctx) { __u64 pid_tgid; struct syscall_state *syscall; pid_tgid = bpf_get_current_pid_tgid(); syscall = bpf_map_lookup_elem(¤t_syscalls, &pid_tgid); if (syscall) return syscall->nr; #ifdef __TARGET_ARCH_x86 return BPF_CORE_READ(ctx, orig_ax); #elif defined(__TARGET_ARCH_arm64) return BPF_CORE_READ(ctx, syscallno); #elif defined(__TARGET_ARCH_powerpc) return BPF_CORE_READ(ctx, gpr[0]); #elif defined(__TARGET_ARCH_s390) return BPF_CORE_READ(ctx, gprs[2]); #else return -1; #endif } /* * fill_event_common - populate the static fields of a cap_event. * @ev: event structure to fill. * @ctx: pt_regs from the capability hook. * @cap: capability number being checked. * * Captures PID/TID, timestamp, UID/GID, command name, syscall number, and * user stack id for the current task. */ static __always_inline void fill_event_common(struct cap_event *ev, struct pt_regs *ctx, int cap) { __u64 pid_tgid; __u64 uid_gid; pid_tgid = bpf_get_current_pid_tgid(); ev->pid = pid_tgid >> 32; ev->tid = (__u32)pid_tgid; ev->capability = cap; ev->timestamp_ns = bpf_ktime_get_ns(); uid_gid = bpf_get_current_uid_gid(); ev->uid = uid_gid >> 32; ev->gid = (__u32)uid_gid; bpf_get_current_comm(&ev->comm, sizeof(ev->comm)); ev->syscall_nr = read_syscall(ctx); ev->stack_id = bpf_get_stackid(ctx, &stack_traces, BPF_F_USER_STACK); } /* * stash_event - store a partially filled event until the kretprobe fires. * @ev: event to stash. * * Keeps the event keyed by pid_tgid in cap_events_inflight so the return * probe can finalize result status before emitting to userspace. */ static __always_inline void stash_event(struct cap_event *ev) { __u64 pid_tgid; pid_tgid = bpf_get_current_pid_tgid(); bpf_map_update_elem(&cap_events_inflight, &pid_tgid, ev, BPF_ANY); } /* Record a denied check against the syscall invocation that contained it. */ static __always_inline void mark_syscall_denial(int cap) { struct syscall_state *syscall; __u64 pid_tgid; if (cap < 0 || cap >= 64) return; pid_tgid = bpf_get_current_pid_tgid(); syscall = bpf_map_lookup_elem(¤t_syscalls, &pid_tgid); if (syscall) syscall->denied_caps |= 1ULL << cap; } /* * submit_event - finalize and emit a stashed event to the ring buffer. * @ret: return code from the capability helper (0 = granted). * * Looks up the in-flight event, copies it to the ring buffer, sets the result * flag (1 = granted, 0 = denied), and removes the temporary entry. Returns 0 * whether or not an event was emitted. */ static __always_inline int submit_event(int ret) { __u64 pid_tgid; struct cap_event *stored; struct cap_event *out; int capability; pid_tgid = bpf_get_current_pid_tgid(); stored = bpf_map_lookup_elem(&cap_events_inflight, &pid_tgid); if (!stored) return 0; capability = stored->capability; out = bpf_ringbuf_reserve(&cap_events, sizeof(*out), 0); if (!out) goto cleanup; __builtin_memcpy(out, stored, sizeof(*out)); out->result = ret ? 0 : 1; if (ret) mark_syscall_denial(capability); bpf_ringbuf_submit(out, 0); cleanup: bpf_map_delete_elem(&cap_events_inflight, &pid_tgid); return 0; } /* * handle_capable - common logic for capability helper entry probes. * @ctx: pt_regs for the probed function. * @cap: capability number under evaluation. * @targ_ns: optional target namespace pointer (may be NULL). * * Filters by PID first; for traced tasks it records a stats increment, * populates a cap_event with contextual information, and stashes it so the * return probe can attach the result. Returns 0 to indicate the kprobe should * allow normal execution to continue. */ static __always_inline int handle_capable(struct pt_regs *ctx, int cap, struct user_namespace *targ_ns, unsigned int cap_opts) { struct cap_event ev = { 0 }; __u32 pid; pid = bpf_get_current_pid_tgid() >> 32; if (!should_record_pid(pid)) return 0; /* Track how many times this capability was inspected. */ record_stats(cap); /* Collect task identity, syscall, and stack trace information. */ fill_event_common(&ev, ctx, cap); ev.cap_opts = (__u32)cap_opts; if (targ_ns) { struct ns_common *ns; ns = (struct ns_common *)targ_ns; ev.targ_ns_inum = BPF_CORE_READ(ns, inum); } /* Save event so the kretprobe can add the success/failure result. */ stash_event(&ev); return 0; } /* * trace_cap_capable - entry probe for cap_capable(). * * Delegates to handle_capable(). Arguments mirror the kernel helper and the * capability number, namespace pointer, and opts value are recorded for the * in-flight event. Returns 0. */ SEC("kprobe/cap_capable") int BPF_KPROBE(trace_cap_capable, const struct cred *cred, struct user_namespace *targ_ns, int cap, unsigned int opts) { return handle_capable(ctx, cap, targ_ns, opts); } /* * trace_cap_capable_ret - return probe for cap_capable(). * @ret: kernel return value (0 = granted). * * Updates result statistics for the capability tied to this pid_tgid and * emits the finalized event to userspace. Always returns 0. */ SEC("kretprobe/cap_capable") int BPF_KRETPROBE(trace_cap_capable_ret, int ret) { __u64 pid_tgid; struct cap_event *stored; pid_tgid = bpf_get_current_pid_tgid(); stored = bpf_map_lookup_elem(&cap_events_inflight, &pid_tgid); if (stored) update_result_stats(stored->capability, ret); return submit_event(ret); } /* * trace_ns_capable - entry probe for ns_capable(). * * Uses handle_capable() to capture namespace-aware capability checks. Returns * 0. */ SEC("kprobe/ns_capable") int BPF_KPROBE(trace_ns_capable, struct user_namespace *ns, int cap) { return handle_capable(ctx, cap, ns, 0); } /* * trace_ns_capable_ret - return probe for ns_capable(). * @ret: kernel return value. * * Emits the stored event with the grant/deny result. Returns 0. */ SEC("kretprobe/ns_capable") int BPF_KRETPROBE(trace_ns_capable_ret, int ret) { return submit_event(ret); } /* * trace_ns_capable_noaudit - entry probe for ns_capable_noaudit(). * * Captures capability checks that bypass kernel audit logging but should be * observed by the auditor. Returns 0. */ SEC("kprobe/ns_capable_noaudit") int BPF_KPROBE(trace_ns_capable_noaudit, struct user_namespace *ns, int cap) { return handle_capable(ctx, cap, ns, 0); } /* * trace_ns_capable_noaudit_ret - return probe for ns_capable_noaudit(). * @ret: kernel return value. * * Finalizes and emits the stored event. Returns 0. */ SEC("kretprobe/ns_capable_noaudit") int BPF_KRETPROBE(trace_ns_capable_noaudit_ret, int ret) { return submit_event(ret); } /* * trace_capable - entry probe for capable(). * * Handles capability checks that do not involve namespaces. Returns 0. */ SEC("kprobe/capable") int BPF_KPROBE(trace_capable, int cap) { return handle_capable(ctx, cap, NULL, 0); } /* * trace_capable_ret - return probe for capable(). * @ret: kernel return value. * * Emits the stored capability event. Returns 0. */ SEC("kretprobe/capable") int BPF_KRETPROBE(trace_capable_ret, int ret) { return submit_event(ret); } /* * read_capset_payload - copy capability masks from capset user arguments. * @ctx: raw syscall entry context containing header and data pointers. * @syscall: per-thread syscall state to populate. * * Marks the payload valid only for a recognized ABI and complete user read. */ static __always_inline void read_capset_payload( struct trace_event_raw_sys_enter *ctx, struct syscall_state *syscall) { struct cap_user_header header; struct cap_user_data data[2] = { 0 }; const void *header_ptr = (const void *)ctx->args[0]; const void *data_ptr = (const void *)ctx->args[1]; if (!header_ptr || !data_ptr) return; if (bpf_probe_read_user(&header, sizeof(header), header_ptr) != 0) return; if (header.version == CAP_VERSION_1) { if (bpf_probe_read_user(&data[0], sizeof(data[0]), data_ptr) != 0) return; } else if (header.version == CAP_VERSION_2 || header.version == CAP_VERSION_3) { if (bpf_probe_read_user(data, sizeof(data), data_ptr) != 0) return; } else { return; } syscall->capset_effective = data[0].effective | ((__u64)data[1].effective << 32); syscall->capset_permitted = data[0].permitted | ((__u64)data[1].permitted << 32); syscall->capset_inheritable = data[0].inheritable | ((__u64)data[1].inheritable << 32); syscall->capset_valid = 1; } /* * trace_sys_enter - remember syscall numbers on entry. * @ctx: raw_syscalls/sys_enter tracepoint context. * * Stores the syscall number in a per-thread map for later lookup by the * capability probes. No-op for non-traced PIDs. Returns 0. */ SEC("tracepoint/raw_syscalls/sys_enter") int trace_sys_enter(struct trace_event_raw_sys_enter *ctx) { struct syscall_state syscall = { 0 }; __u64 pid_tgid; __u32 pid; pid_tgid = bpf_get_current_pid_tgid(); pid = pid_tgid >> 32; if (!should_trace_pid(pid)) return 0; syscall.nr = ctx->id; if (should_record_pid(pid) && ctx->id == capset_syscall_nr) read_capset_payload(ctx, &syscall); bpf_map_update_elem(¤t_syscalls, &pid_tgid, &syscall, BPF_ANY); return 0; } static __always_inline void emit_syscall_result( const struct syscall_state *syscall, __s64 ret) { struct cap_event *out; __u64 pid_tgid; if (syscall->denied_caps == 0) return; out = bpf_ringbuf_reserve(&cap_events, sizeof(*out), 0); if (!out) return; __builtin_memset(out, 0, sizeof(*out)); pid_tgid = bpf_get_current_pid_tgid(); out->timestamp_ns = bpf_ktime_get_ns(); out->pid = pid_tgid >> 32; out->tid = (__u32)pid_tgid; out->syscall_nr = syscall->nr; out->syscall_ret = ret; out->denied_caps = syscall->denied_caps; out->event_type = CAP_EVENT_SYSCALL_RESULT; bpf_ringbuf_submit(out, 0); } /* Emit masks installed by a successful capset call. */ static __always_inline void emit_capset_result( const struct syscall_state *syscall, __s64 ret) { struct cap_event *out; __u64 pid_tgid; if (!syscall->capset_valid || ret != 0) return; out = bpf_ringbuf_reserve(&cap_events, sizeof(*out), 0); if (!out) return; __builtin_memset(out, 0, sizeof(*out)); pid_tgid = bpf_get_current_pid_tgid(); out->timestamp_ns = bpf_ktime_get_ns(); out->pid = pid_tgid >> 32; out->tid = (__u32)pid_tgid; out->syscall_nr = syscall->nr; out->syscall_ret = ret; out->capset_effective = syscall->capset_effective; out->capset_permitted = syscall->capset_permitted; out->capset_inheritable = syscall->capset_inheritable; out->event_type = CAP_EVENT_CAPSET; bpf_ringbuf_submit(out, 0); } /* * trace_sys_exit - emit denied-check outcomes and clear syscall tracking. * @ctx: raw_syscalls/sys_exit tracepoint context. * * Emits the raw return value when the invocation contained denied capability * checks. A successful capset also emits its requested masks. Removes the * per-thread state before returning 0. */ SEC("tracepoint/raw_syscalls/sys_exit") int trace_sys_exit(struct trace_event_raw_sys_exit *ctx) { struct syscall_state *syscall; __u64 pid_tgid; __u32 pid; pid_tgid = bpf_get_current_pid_tgid(); pid = pid_tgid >> 32; if (!should_trace_pid(pid)) return 0; syscall = bpf_map_lookup_elem(¤t_syscalls, &pid_tgid); if (syscall) { emit_syscall_result(syscall, ctx->ret); emit_capset_result(syscall, ctx->ret); } bpf_map_delete_elem(¤t_syscalls, &pid_tgid); return 0; } /* * trace_sched_process_fork - follow new child processes. * @ctx: sched_process_fork tracepoint data. * * When a traced parent forks, automatically add the child PID to the filter * map so subsequent capability checks are captured. Returns 0. */ SEC("tracepoint/sched/sched_process_fork") int trace_sched_process_fork(struct trace_event_raw_sched_process_fork *ctx) { __u32 parent_pid; __u32 child_pid; __u8 *parent_val; parent_pid = ctx->parent_pid; child_pid = ctx->child_pid; parent_val = bpf_map_lookup_elem(&target_pids, &parent_pid); if (parent_val) { __u8 child_val = *parent_val; bpf_map_update_elem(&target_pids, &child_pid, &child_val, BPF_ANY); } return 0; } /* * trace_sched_process_exec - mark a traced PID as post-exec. * @ctx: sched_process_exec tracepoint data. * * Transitions the PID from pre-exec (phase 1) to post-exec (phase 2) once * the new binary image is loaded. */ SEC("tracepoint/sched/sched_process_exec") int trace_sched_process_exec(struct trace_event_raw_sched_process_exec *ctx) { __u32 pid; __u8 *val; __u8 new_val = 2; pid = bpf_get_current_pid_tgid() >> 32; val = bpf_map_lookup_elem(&target_pids, &pid); if (val) bpf_map_update_elem(&target_pids, &pid, &new_val, BPF_ANY); return 0; } /* * trace_sched_process_exit - prune exited processes from the target set. * @ctx: sched_process_exit tracepoint data. * * Removes the exiting PID from the target_pids map to prevent stale entries. * Returns 0. */ SEC("tracepoint/sched/sched_process_exit") int trace_sched_process_exit(struct trace_event_raw_sched_process_template *ctx) { __u32 pid; pid = ctx->pid; bpf_map_delete_elem(&target_pids, &pid); return 0; } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/cap_audit.c000066400000000000000000000440321524174023300231600ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* * cap-audit - Trace a target process to discover required capabilities. * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Authors: * Steve Grubb * Portions of this code were made with codex 5.2 */ #include "cap_audit.h" #include "cap_audit_path.h" #include #include #include #include #include #include #include #include #include #include #include /* * Overview: * cap-audit launches a target application, traces that process tree using * eBPF hooks, and reports which Linux capabilities were actually exercised. * The userspace side performs three major jobs: * * (1) prepare the runtime environment by checking our own capabilities and * raising rlimits; * (2) coordinate with the eBPF program by registering the target PID before * exec() and consuming capability check events from the ring buffer; and * (3) analyze collected data to present required, conditional, and denied * capabilities in human and machine-readable formats. * * When the tool observes a capset syscall from the initial PID, it splits * capability accounting into initialization and operational phases. The * initialization phase covers all capability checks from process start to * the first capset. The operational phase covers everything after. This * separation allows the tool to distinguish capabilities needed for one-time * setup (binding privileged ports, chroot, loading restricted configuration) * from capabilities needed for ongoing operation. Recommendations for * programmatic capability dropping use only the operational set, while * deployment recommendations (file capabilities, systemd, containers) use * the union since the process must start with sufficient capabilities for * initialization. Programs that never call capset produce an undifferentiated * report identical to previous versions. * * Core design problem: * when tracing as root, many capability checks come from kernel-internal * work under the same PID rather than from app logic. The tool uses a * two-layer noise filter pipeline, split between BPF and userspace, to * separate real requirements from incidental checks. * * Layer 1 - pre-exec noise (BPF phase gate): * after fork() but before execve() completes, the child is still this * auditor image. PATH lookup, directory traversal, and exec machinery can * trigger checks like DAC_READ_SEARCH/SYS_ADMIN/SETPCAP that are unrelated * to the target app. The BPF side tracks PID phases and suppresses all * capability events until sched_process_exec transitions the PID from * phase 1 (pre-exec) to phase 2 (post-exec). * * Layer 2 - userspace always-noise filter: * handle_cap_event() unconditionally drops two classes of capability * checks that never represent real application requirements. First, * execve-triggered SYS_ADMIN and SETPCAP checks are kernel-internal * credential-transition noise. Second, cap-audit identifies the known * advisory memory overcommit probe by combining syscall, capability, and * CAP_OPT_NOAUDIT: cap_vm_enough_memory checks CAP_SYS_ADMIN on * brk/mmap/mprotect/mremap and the operation succeeds regardless of the * result. CAP_OPT_NOAUDIT means "do not audit", not "advisory", so it * is only a confirming signal here. Other enforcement checks also use * CAP_OPT_NOAUDIT to avoid audit spam, and must still be reported. The * syscall + capability match identifies the specific advisory call site, * while CAP_OPT_NOAUDIT confirms that the event followed the advisory path * rather than a security-gating path on the same syscall. * * Layer 3 - final drain shutdown backstop: * after waitpid() reports that the initial PID has exited, cap-audit does a * short final ring-buffer drain before analyzing results. That drain should * stay conservative because queued late SYS_ADMIN/SETPCAP checks from the * exiting interpreter/runtime are more likely to be teardown chatter than a * meaningful application requirement. The shutdown backstop is intentionally * limited to the initial PID and only applies during that final drain. * * PID filtering remains central: parent registers the child immediately after * fork(), BPF follows forks/exits to keep the target set precise, and each * event carries capability, syscall context, namespace info, and result for * per-capability aggregation. * * Parent/child startup is synchronized with a pipe. The child blocks in * read(sync_pipe) until the parent has inserted its PID in target_pids and * written a go byte. That ordering prevents missed events and prevents events * from arriving before the PID is registered. */ struct audit_state state; int audit_machine = -1; static char **prepend_execstart_argv(const service_config_t *cfg, char **args, int arg_count) __attribute_malloc__ __attr_dealloc_free __attr_access ((__read_only__, 1)) __attr_access ((__read_only__, 2)) __wur; static int has_service_suffix(const char *path) __attr_access ((__read_only__, 1)) __attribute_pure__; static int read_user_ns_inum(__u32 *inum) { struct stat st; if (stat("/proc/self/ns/user", &st) != 0) { fprintf(stderr, "Error: failed to read user namespace: %s\n", strerror(errno)); return -1; } if (st.st_ino == 0 || st.st_ino > UINT_MAX) { fprintf(stderr, "Error: invalid user namespace inode\n"); return -1; } *inum = (__u32)st.st_ino; return 0; } static void sig_handler(int sig __attribute__((unused))) { state.stop = 1; } static int set_memlock_rlimit(void) { struct rlimit rlim_new = { .rlim_cur = RLIM_INFINITY, .rlim_max = RLIM_INFINITY, }; if (setrlimit(RLIMIT_MEMLOCK, &rlim_new) < 0) { fprintf(stderr, "Error: Failed to raise memlock rlimit: %s\n", errno == EPERM ? "insufficient privileges" : strerror(errno)); return -1; } return 0; } static int init_capng(void) { capng_clear(CAPNG_SELECT_BOTH); if (capng_get_caps_process() != 0) { fprintf(stderr, "Error: Failed to get process capabilities\n"); return -1; } return 0; } static char **prepend_execstart_argv(const service_config_t *cfg, char **args, int arg_count) { char **argv; int i; argv = calloc((size_t)arg_count + 2, sizeof(char *)); if (!argv) return NULL; argv[0] = cfg->exec_argv[0]; for (i = 0; i < arg_count; i++) argv[i + 1] = args[i]; argv[arg_count + 1] = NULL; return argv; } static int has_service_suffix(const char *path) { size_t len; const char suffix[] = ".service"; size_t suffix_len = sizeof(suffix) - 1; if (!path) return 0; len = strlen(path); return len >= suffix_len && strcmp(path + len - suffix_len, suffix) == 0; } static int check_auditor_caps(void) { if (!capng_have_capability(CAPNG_EFFECTIVE, CAP_BPF) && !capng_have_capability(CAPNG_EFFECTIVE, CAP_SYS_ADMIN)) { fprintf( stderr, "Error: Need CAP_BPF or CAP_SYS_ADMIN to run auditor\n"); return -1; } if (!capng_have_capability(CAPNG_EFFECTIVE, CAP_PERFMON) && !capng_have_capability(CAPNG_EFFECTIVE, CAP_SYS_ADMIN)) { fprintf(stderr, "Error: Need CAP_PERFMON or CAP_SYS_ADMIN for " "perf events\n"); return -1; } if (!capng_have_capability(CAPNG_EFFECTIVE, CAP_SYS_PTRACE)) fprintf(stderr, "Warning: CAP_SYS_PTRACE not available, stack " "traces may be limited\n"); return 0; } static int set_target_pid(pid_t pid) { int map_fd; __u8 val = 1; map_fd = bpf_map__fd(state.skel->maps.target_pids); if (map_fd < 0) { fprintf(stderr, "Error: Failed to get target_pids map fd\n"); return -1; } if (bpf_map_update_elem(map_fd, &pid, &val, BPF_ANY) != 0) { fprintf(stderr, "Error: Failed to register target PID %d: %s\n", pid, strerror(errno)); return -1; } if (state.verbose) printf("[*] Registered PID %d for tracing\n", pid); return 0; } static void usage(FILE *out, const char *prog) { fprintf(out, "Usage: %s [options] -- command [args...]\n", prog); fprintf(out, " %s --service FILE [-- command-or-args...]\n", prog); fprintf(out, "Options:\n"); fprintf(out, " -h, --help Show this help message\n"); fprintf(out, " -v, --verbose Verbose output\n"); fprintf(out, " -j, --json JSON output\n"); fprintf(out, " -y, --yaml YAML output\n"); fprintf(out, " -s, --service FILE Simulate a systemd service unit\n"); } #ifndef CAP_AUDIT_NO_MAIN int main(int argc, char **argv) { int err; int arg_idx; pid_t child; pid_t ret_pid; int wstatus; char *target_path = NULL; char **service_target_argv = NULL; int separator_seen = 0; if (argc < 2) { usage(stderr, argv[0]); return 1; } arg_idx = 1; while (arg_idx < argc && argv[arg_idx][0] == '-') { if (!strcmp(argv[arg_idx], "-h") || !strcmp(argv[arg_idx], "--help")) { usage(stdout, argv[0]); return 0; } else if (!strcmp(argv[arg_idx], "-v") || !strcmp(argv[arg_idx], "--verbose")) state.verbose = 1; else if (!strcmp(argv[arg_idx], "-j") || !strcmp(argv[arg_idx], "--json")) state.json_output = 1; else if (!strcmp(argv[arg_idx], "-y") || !strcmp(argv[arg_idx], "--yaml")) state.yaml_output = 1; else if (!strcmp(argv[arg_idx], "-s") || !strcmp(argv[arg_idx], "--service")) { char *service_file; arg_idx++; if (arg_idx >= argc) { fprintf(stderr, "Error: --service requires a file\n"); usage(stderr, argv[0]); return 1; } service_file = strdup(argv[arg_idx]); if (!service_file) return 1; free(state.service_file); state.service_file = service_file; } else if (!strcmp(argv[arg_idx], "--")) { separator_seen = 1; arg_idx++; break; } else { fprintf(stderr, "Error: Unknown option '%s'\n", argv[arg_idx]); usage(stderr, argv[0]); return 1; } arg_idx++; } if (state.service_file) { state.service_cfg = calloc(1, sizeof(*state.service_cfg)); if (!state.service_cfg) goto err_target_path; if (parse_service_file(state.service_file, state.service_cfg) != 0) goto err_target_path; if (!separator_seen && arg_idx < argc) { fprintf(stderr, "Error: --service command overrides require --\n"); usage(stderr, argv[0]); goto err_target_path; } if (separator_seen && arg_idx < argc) { if (argv[arg_idx][0] == '-') { if (!state.service_cfg->exec_argv) { fprintf(stderr, "Error: service has no ExecStart executable for argument override\n"); goto err_target_path; } service_target_argv = prepend_execstart_argv(state.service_cfg, &argv[arg_idx], argc - arg_idx); if (!service_target_argv) goto err_target_path; state.target_argv = service_target_argv; } else { state.target_argv = &argv[arg_idx]; } } else { if (!state.service_cfg->exec_argv) { fprintf(stderr, "Error: service has no ExecStart command\n"); goto err_target_path; } state.target_argv = state.service_cfg->exec_argv; } } else if (arg_idx >= argc) { fprintf(stderr, "Error: No command specified\n"); usage(stderr, argv[0]); return 1; } else { state.target_argv = &argv[arg_idx]; } /* * Fail before capability checks, BPF setup, or fork/exec tracing so * a bad target command is reported directly by cap-audit. */ target_path = resolve_target_command(state.target_argv[0]); if (!target_path) { fprintf(stderr, "Error: target is not executable: %s\n", state.target_argv[0]); if (!state.service_file && has_service_suffix(state.target_argv[0])) fprintf(stderr, "Hint: to audit a systemd unit, use --service FILE\n"); goto err_target_path; } if (read_user_ns_inum(&state.baseline_user_ns_inum) != 0) goto err_target_path; if (init_capng() != 0) goto err_target_path; if (check_auditor_caps() != 0) goto err_target_path; if (set_memlock_rlimit() != 0) goto err_target_path; state.app.exe = strdup(state.target_argv[0]); state.app.prog_type = UNSUPPORTED; if (audit_machine < 0) audit_machine = audit_detect_machine(); if (audit_machine < 0) { fprintf(stderr, "Error: unable to determine hardware architecture for syscall lookup. Exiting.\n"); free(state.app.exe); goto err_target_path; } state.app.execve_nr = audit_name_to_syscall("execve", audit_machine); state.app.mmap_nr = audit_name_to_syscall("mmap", audit_machine); state.app.brk_nr = audit_name_to_syscall("brk", audit_machine); state.app.mprotect_nr = audit_name_to_syscall("mprotect", audit_machine); state.app.mremap_nr = audit_name_to_syscall("mremap", audit_machine); state.app.capset_nr = audit_name_to_syscall("capset", audit_machine); state.skel = cap_audit_bpf__open(); if (!state.skel) { fprintf(stderr, "Error: Failed to open BPF program: %s\n", strerror(errno)); free(state.app.exe); goto err_target_path; } state.skel->rodata->capset_syscall_nr = state.app.capset_nr; err = cap_audit_bpf__load(state.skel); if (err) { fprintf(stderr, "Error: Failed to load BPF program: %s\n", strerror(-err)); cap_audit_bpf__destroy(state.skel); free(state.app.exe); goto err_target_path; } err = cap_audit_bpf__attach(state.skel); if (err) { fprintf(stderr, "Error: Failed to attach BPF programs: %s\n", strerror(-err)); cap_audit_bpf__destroy(state.skel); free(state.app.exe); goto err_target_path; } state.rb = ring_buffer__new(bpf_map__fd(state.skel->maps.cap_events), handle_cap_event, NULL, NULL); if (!state.rb) { fprintf(stderr, "Error: Failed to create ring buffer: %s\n", strerror(errno)); cap_audit_bpf__destroy(state.skel); free(state.app.exe); goto err_target_path; } printf("[*] Capability auditor started\n"); if (pipe(state.sync_pipe) != 0) { fprintf(stderr, "Error: pipe failed: %s\n", strerror(errno)); ring_buffer__free(state.rb); cap_audit_bpf__destroy(state.skel); free(state.app.exe); goto err_target_path; } child = fork(); if (child == 0) { char sync_byte; ssize_t bytes; close(state.sync_pipe[1]); bytes = read(state.sync_pipe[0], &sync_byte, 1); if (bytes != 1) { if (bytes < 0) perror("read"); else fprintf(stderr, "Error: failed to sync with parent\n"); close(state.sync_pipe[0]); exit(1); } close(state.sync_pipe[0]); if (state.service_cfg && apply_service_config(state.service_cfg) != 0) _exit(1); /* * The earlier path check is only diagnostic. Repeat lookup here * so execvp() preserves PATH fallback if a candidate changes. */ execvp(state.target_argv[0], state.target_argv); perror("execvp"); exit(1); } else if (child < 0) { fprintf(stderr, "Error: fork failed: %s\n", strerror(errno)); close(state.sync_pipe[0]); close(state.sync_pipe[1]); ring_buffer__free(state.rb); cap_audit_bpf__destroy(state.skel); free(state.app.exe); goto err_target_path; } state.app.pid = child; close(state.sync_pipe[0]); if (set_target_pid(child) != 0) { close(state.sync_pipe[1]); kill(child, SIGKILL); waitpid(child, NULL, 0); ring_buffer__free(state.rb); cap_audit_bpf__destroy(state.skel); free(state.app.exe); goto err_target_path; } if (write(state.sync_pipe[1], "1", 1) != 1) { fprintf(stderr, "Error: write failed: %s\n", strerror(errno)); close(state.sync_pipe[1]); kill(child, SIGKILL); waitpid(child, NULL, 0); ring_buffer__free(state.rb); cap_audit_bpf__destroy(state.skel); free(state.app.exe); goto err_target_path; } close(state.sync_pipe[1]); { char resolved[PATH_MAX]; if (resolve_target_exe(child, resolved, sizeof(resolved)) == 0) { char *resolved_dup = strdup(resolved); if (resolved_dup) { free(state.app.exe); state.app.exe = resolved_dup; } state.app.prog_type = classify_app(state.app.exe); } else { fprintf(stderr, "Warning: unable to resolve target path\n"); } } read_system_state(&state.app); inspect_target_file_caps(child); if (state.app.prog_type == PYTHON && state.verbose) printf("[*] Script interpreter: %s\n", state.app.exe); if (state.service_cfg && state.verbose) print_service_config(state.service_cfg); printf("[*] Tracing application: %s (PID %d)\n", state.app.exe, child); printf("[*] Press Ctrl-C to stop\n\n"); signal(SIGINT, sig_handler); signal(SIGTERM, sig_handler); while (!state.stop) { err = ring_buffer__poll(state.rb, 100); if (err < 0 && err != -EINTR) { fprintf(stderr, "Error polling ring buffer: %s\n", strerror(-err)); break; } ret_pid = waitpid(child, &wstatus, WNOHANG); if (ret_pid == child) { if (WIFEXITED(wstatus)) printf( "\n[*] Application exited with status %d\n", WEXITSTATUS(wstatus)); else if (WIFSIGNALED(wstatus)) printf( "\n[*] Application terminated by signal %d\n", WTERMSIG(wstatus)); break; } } printf("[*] Analyzing results...\n"); state.shutting_down = 1; usleep(100000); ring_buffer__poll(state.rb, 0); if (state.json_output) output_json(); else if (state.yaml_output) output_yaml(); else analyze_capabilities(); ring_buffer__free(state.rb); cap_audit_bpf__destroy(state.skel); free(state.app.exe); free(target_path); free(service_target_argv); if (state.service_cfg) { free_config(state.service_cfg); free(state.service_cfg); } free(state.service_file); for (int i = 0; i <= CAP_LAST_CAP; i++) { if (state.app.checks[i].reason) free(state.app.checks[i].reason); if (state.app.checks[i].op_reason) free(state.app.checks[i].op_reason); if (state.app.checks[i].denied_syscalls) free(state.app.checks[i].denied_syscalls); if (state.app.checks[i].outcomes) free(state.app.checks[i].outcomes); } return 0; err_target_path: free(target_path); free(service_target_argv); if (state.service_cfg) { free_config(state.service_cfg); free(state.service_cfg); } free(state.service_file); return 1; } #endif stevegrubb-libcap-ng-9ae4440/utils/cap-audit/cap_audit.h000066400000000000000000000170371524174023300231720ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* * cap-audit - Trace a target process to discover required capabilities. * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Authors: * Steve Grubb * Portions of this code were made with codex 5.2 */ #ifndef CAP_AUDIT_H #define CAP_AUDIT_H #include "config.h" #include #include #include #include #include #include #include #include "cap-ng.h" #include "classify_app.h" #include "cap_audit.skel.h" #include "gcc-attributes.h" #ifndef CAP_OPT_NOAUDIT #define CAP_OPT_NOAUDIT 0x2 #endif enum cap_event_type { CAP_EVENT_CHECK, CAP_EVENT_SYSCALL_RESULT, CAP_EVENT_CAPSET, }; struct cap_event { __u64 timestamp_ns; __u32 pid; __u32 tid; __u32 uid; __u32 gid; int capability; int result; int syscall_nr; char comm[16]; __u64 stack_id; __u32 targ_ns_inum; __u32 cap_opts; __s64 syscall_ret; __u64 denied_caps; __u64 capset_effective; __u64 capset_permitted; __u64 capset_inheritable; __u32 event_type; }; enum syscall_outcome_class { SYSCALL_OUTCOME_SUCCESS, SYSCALL_OUTCOME_PERMISSION, SYSCALL_OUTCOME_OTHER, SYSCALL_OUTCOME_INTERRUPTED, }; enum denial_assessment { DENIAL_ASSESSMENT_NONE, DENIAL_ASSESSMENT_PERMISSION, DENIAL_ASSESSMENT_MIXED_INTERRUPTED, DENIAL_ASSESSMENT_MIXED_OTHER, DENIAL_ASSESSMENT_INCONCLUSIVE, DENIAL_ASSESSMENT_SUCCEEDED, DENIAL_ASSESSMENT_OTHER, }; struct syscall_outcome { int syscall_nr; __s64 result; unsigned long count; }; struct cap_outcome_summary { unsigned long succeeded; unsigned long permission_failed; unsigned long other_failed; unsigned long interrupted; }; struct capset_usage { __u64 effective; __u64 permitted; __u64 inheritable; unsigned long successful_calls; }; struct cap_check { int capability; unsigned long count; unsigned long granted; unsigned long denied; int needed; char *reason; unsigned long op_count; unsigned long op_granted; unsigned long op_denied; int op_needed; char *op_reason; int *denied_syscalls; size_t denied_syscall_count; size_t denied_syscall_capacity; struct syscall_outcome *outcomes; size_t outcome_count; size_t outcome_capacity; }; struct app_caps { pid_t pid; char *exe; int execve_nr; int mmap_nr; int brk_nr; int mprotect_nr; int mremap_nr; int capset_nr; type_t prog_type; struct cap_check checks[CAP_LAST_CAP + 1]; int yama_ptrace_scope; int kptr_restrict; int dmesg_restrict; int modules_disabled; int perf_event_paranoid; int unprivileged_bpf_disabled; int bpf_jit_enable; int bpf_jit_harden; int bpf_jit_kallsyms; int mmap_min_addr; int protected_hardlinks; int protected_symlinks; int suid_dumpable; char kernel_version[64]; int file_caps; int file_setpcap; struct capset_usage capset; }; typedef struct { bool seen; bool caps[CAP_LAST_CAP + 1]; } cap_set_t; typedef struct { gid_t *gids; size_t count; bool is_set; } gid_list_t; typedef struct { char *user_raw; uid_t user_uid; gid_t user_primary_gid; bool user_is_set; bool user_resolved; gid_t group_gid; bool group_is_set; gid_list_t sup_groups; cap_set_t bounding; cap_set_t ambient; bool dynamic_user; bool dynamic_user_set; bool no_new_privs; bool no_new_privs_set; char *service_type; char *exec_start; char **exec_argv; size_t exec_argc; /* ExecStart=! leaves configured credential changes to the command. */ bool exec_start_no_setuid; } service_config_t; struct audit_state { struct cap_audit_bpf *skel; struct ring_buffer *rb; struct app_caps app; int verbose; int json_output; int yaml_output; int sync_pipe[2]; char **target_argv; int capset_observed; __u32 baseline_user_ns_inum; bool foreign_target_ns_observed; volatile sig_atomic_t stop; int shutting_down; char *service_file; service_config_t *service_cfg; }; extern struct audit_state state; extern int audit_machine; int handle_cap_event(void *ctx, void *data, size_t data_sz) __attr_access ((__read_only__, 2)); void analyze_capabilities(void); void output_json(void); void output_yaml(void); int include_cap_in_recommendations(int cap); const char *cap_name_safe(int cap) __returns_nonnull; const char *syscall_name_from_nr(int nr); void read_sysctl(const char *path, int *value) __attr_access ((__read_only__, 1)) __attr_access ((__write_only__, 2)); void read_system_state(struct app_caps *app) __attr_access ((__read_write__, 1)); int resolve_target_exe(pid_t pid, char *exepath, size_t exepath_len) __attr_access ((__write_only__, 2, 3)) __wur; int inspect_target_file_caps(pid_t pid); char *json_escape(const char *input) __attribute_malloc__ __attr_dealloc_free __attr_access ((__read_only__, 1)) __wur; void update_reason_to(char **target, int syscall_nr) __attr_access ((__read_write__, 1)); void update_reason(struct cap_check *check, int syscall_nr) __attr_access ((__read_write__, 1)); void update_reason_op(struct cap_check *check, int syscall_nr) __attr_access ((__read_write__, 1)); int cap_required_union(const struct cap_check *check) __attr_access ((__read_only__, 1)) __attribute_pure__; unsigned long cap_total_checks(const struct cap_check *check) __attr_access ((__read_only__, 1)) __attribute_pure__; unsigned long cap_total_granted(const struct cap_check *check) __attr_access ((__read_only__, 1)) __attribute_pure__; unsigned long cap_total_denied(const struct cap_check *check) __attr_access ((__read_only__, 1)) __attribute_pure__; const char *cap_union_reason(const struct cap_check *check) __attr_access ((__read_only__, 1)) __attribute_pure__; void record_successful_capset(const struct cap_event *event) __attr_access ((__read_only__, 1)); int cap_requested_by_capset(int cap) __attribute_pure__; int cap_is_capset_only(int cap) __attribute_pure__; int cap_is_compat_requirement(int cap) __attribute_pure__; int add_cap_syscall_outcome(struct cap_check *check, int syscall_nr, __s64 result) __attr_access ((__read_write__, 1)); enum syscall_outcome_class classify_syscall_outcome(__s64 result) __attribute_const__; const char *syscall_outcome_class_name(enum syscall_outcome_class class) __attribute_const__; void summarize_cap_outcomes(const struct cap_check *check, struct cap_outcome_summary *summary) __attr_access ((__read_only__, 1)) __attr_access ((__write_only__, 2)); enum denial_assessment assess_cap_denials(const struct cap_check *check) __attr_access ((__read_only__, 1)) __attribute_pure__; const char *denial_assessment_name(enum denial_assessment assessment) __attribute_const__; int syscall_result_errno(__s64 result) __attribute_const__; const char *syscall_result_name(__s64 result) __attribute_const__; type_t classify_app(const char *exe) __attr_access ((__read_only__, 1)) __wur; int parse_service_file(const char *path, service_config_t *cfg) __attr_access ((__read_only__, 1)) __attr_access ((__read_write__, 2)) __wur; int apply_service_config(const service_config_t *cfg) __attr_access ((__read_only__, 1)) __wur; void free_config(service_config_t *cfg) __attr_access ((__read_write__, 1)); void print_service_config(const service_config_t *cfg) __attr_access ((__read_only__, 1)); #endif stevegrubb-libcap-ng-9ae4440/utils/cap-audit/cap_audit_analyze.c000066400000000000000000001267621524174023300247160ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* * cap-audit - Trace a target process to discover required capabilities. * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Authors: * Steve Grubb * Portions of this code were made with codex 5.2 */ /* Human-readable analysis output: capability summaries, deployment * recommendations, and wrapped terminal formatting helpers. */ #include "cap_audit.h" #include #include #include #include #include #include #include #include #include int include_cap_in_recommendations(int cap) { if (cap == CAP_SETPCAP && state.app.file_caps && !state.app.file_setpcap) return 0; return 1; } static void cap_name_upper_buf(int cap, char *buf, size_t buf_len) { const char *name = cap_name_safe(cap); size_t i; if (buf_len == 0) return; for (i = 0; name[i] && i + 1 < buf_len; i++) buf[i] = toupper((unsigned char)name[i]); buf[i] = '\0'; } static int get_output_width(void) { struct winsize ws; const char *columns; long env_width; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return ws.ws_col; columns = getenv("COLUMNS"); if (columns) { env_width = strtol(columns, NULL, 10); if (env_width >= 40 && env_width <= 400) return (int)env_width; } return 80; } static void print_rule(char ch) { int i; int width = get_output_width(); if (width < 40) width = 40; for (i = 0; i < width; i++) putchar(ch); putchar('\n'); } static void print_wrapped_text(const char *indent, const char *text) { size_t indent_len; char *cont_indent; int width; int content_width; const char *p; int line_len = 0; int first_line = 1; if (!text) { printf("%s\n", indent); return; } indent_len = strlen(indent); cont_indent = malloc(indent_len + 1); if (!cont_indent) { printf("%s%s\n", indent, text); return; } memset(cont_indent, ' ', indent_len); cont_indent[indent_len] = '\0'; width = get_output_width(); if (width < 40) width = 40; content_width = width - (int)indent_len; if (content_width < 16) content_width = 16; printf("%s", indent); p = text; while (*p) { size_t word_len; int need_space = line_len > 0; while (*p == ' ') p++; if (*p == '\n') { putchar('\n'); printf("%s", first_line ? cont_indent : cont_indent); line_len = 0; first_line = 0; p++; continue; } if (*p == '\0') break; word_len = strcspn(p, " \n"); if (need_space && line_len + 1 + (int)word_len > content_width) { putchar('\n'); printf("%s", cont_indent); line_len = 0; need_space = 0; first_line = 0; } if (need_space) { putchar(' '); line_len++; } fwrite(p, 1, word_len, stdout); line_len += word_len; p += word_len; } putchar('\n'); free(cont_indent); } static void print_wrappedf(const char *indent, const char *fmt, ...) { va_list ap; char *buf; va_start(ap, fmt); if (vasprintf(&buf, fmt, ap) < 0) buf = NULL; va_end(ap); if (buf) { print_wrapped_text(indent, buf); free(buf); } else { print_wrapped_text(indent, "(formatting error)"); } } static int cap_in_programmatic_set(int cap) { if (!include_cap_in_recommendations(cap)) return 0; if (state.capset_observed) return state.app.checks[cap].op_granted > 0; return state.app.checks[cap].granted > 0; } static int capset_only_count(void) { int cap; int count = 0; for (cap = 0; cap <= CAP_LAST_CAP; cap++) { if (cap_is_capset_only(cap)) count++; } return count; } static void print_capset_requested_sets(int cap, const char *prefix) { __u64 mask = 1ULL << cap; int first = 1; printf("%s", prefix); if (state.app.capset.effective & mask) { printf("effective"); first = 0; } if (state.app.capset.permitted & mask) { printf("%spermitted", first ? "" : ", "); first = 0; } if (state.app.capset.inheritable & mask) printf("%sinheritable", first ? "" : ", "); printf("\n"); } static void print_capset_only_section(void) { int cap; int found = 0; printf("CAPSET-ONLY CAPABILITIES:\n"); print_rule('-'); for (cap = 0; cap <= CAP_LAST_CAP; cap++) { if (!cap_is_capset_only(cap)) continue; printf(" %s (#%d)\n", cap_name_safe(cap), cap); print_capset_requested_sets(cap, " Requested by successful capset in: "); printf(" Granted use observed: no\n"); print_wrapped_text(" Assessment: ", "The current binary requested this capability in a " "capset that completed successfully, but no granted " "capability check was observed. This is a compatibility " "constraint, not confirmed functional use."); print_wrapped_text(" Recommendation: ", "Retain it in deployment capability sets for the current " "binary. Manually review the application, exercise relevant " "code paths, and remove it from the application's capset " "setup before removing it from the deployment boundary."); printf("\n"); found = 1; } if (!found) printf(" None\n\n"); } static bool svc_is_root_service(const service_config_t *cfg) { return !cfg->user_is_set || cfg->user_uid == 0; } static gid_t svc_effective_gid(const service_config_t *cfg) { if (cfg->group_is_set) return cfg->group_gid; if (cfg->user_is_set) return cfg->user_primary_gid; return 0; } static const char *svc_group_name(gid_t gid, char *buf, size_t buf_len) { struct group *grp; grp = getgrgid(gid); if (grp) return grp->gr_name; snprintf(buf, buf_len, "%u", (unsigned int)gid); return buf; } static const char *svc_user_name(const service_config_t *cfg) { if (cfg->user_is_set && cfg->user_raw) return cfg->user_raw; return "root"; } static int svc_print_cap_list(const bool caps[CAP_LAST_CAP + 1]) { int cap; int first = 1; for (cap = 0; cap <= CAP_LAST_CAP; cap++) { if (!caps[cap]) continue; if (!first) printf(" "); printf("%s", cap_name_safe(cap)); first = 0; } return !first; } static void svc_print_wrapped_cap_list( const char *label, const bool caps[CAP_LAST_CAP + 1]) { const int continuation = 8; int width = get_output_width(); int current = strlen(label); int cap; int line_has_cap = 0; if (width < 40) width = 40; printf("%s", label); for (cap = 0; cap <= CAP_LAST_CAP; cap++) { const char *name; int name_len; if (!caps[cap]) continue; name = cap_name_safe(cap); name_len = strlen(name); if (current + (line_has_cap ? 1 : 0) + name_len > width && current > continuation) { printf("\n%*s", continuation, ""); current = continuation; line_has_cap = 0; } if (line_has_cap) { printf(" "); current++; } printf("%s", name); current += name_len; line_has_cap = 1; } printf("\n"); } static void svc_print_cap_set(const cap_set_t *set) { if (!set->seen) { printf("not configured (all capabilities inherited)"); return; } if (!svc_print_cap_list(set->caps)) printf("(none)"); } static void svc_print_ambient_set(const cap_set_t *set) { if (!set->seen) { printf("not configured (none)"); return; } if (!svc_print_cap_list(set->caps)) printf("(none)"); } struct svc_deployment_caps { bool ambient[CAP_LAST_CAP + 1]; bool bounding[CAP_LAST_CAP + 1]; }; struct svc_recommendations { struct svc_deployment_caps compatible; int configured_unobserved; }; static int svc_cap_configured_unobserved(const service_config_t *cfg, int cap) { struct cap_check *check = &state.app.checks[cap]; int in_bounding; int in_ambient; if (!include_cap_in_recommendations(cap) || cap_requested_by_capset(cap) || cap_total_checks(check) > 0) return 0; in_bounding = cfg->bounding.seen && cfg->bounding.caps[cap]; in_ambient = cfg->ambient.seen && cfg->ambient.caps[cap] && (!cfg->bounding.seen || in_bounding); return in_bounding || in_ambient; } static void svc_build_recommendations(const service_config_t *cfg, struct svc_recommendations *caps) { int cap; memset(caps, 0, sizeof(*caps)); for (cap = 0; cap <= CAP_LAST_CAP; cap++) { if (!include_cap_in_recommendations(cap)) continue; /* * A denied check may be advisory or precede a fallback, so it * does not prove that the capability is required. */ if (cap_is_compat_requirement(cap)) { caps->compatible.ambient[cap] = true; caps->compatible.bounding[cap] = true; } if (!svc_cap_configured_unobserved(cfg, cap)) continue; /* * No observation can mean the run missed a feature-specific path. * Preserve explicit unit intent in the generated configuration; * absence of evidence is not enough to recommend removal. */ if (cfg->bounding.seen && cfg->bounding.caps[cap]) caps->compatible.bounding[cap] = true; if (cfg->ambient.seen && cfg->ambient.caps[cap]) { caps->compatible.ambient[cap] = true; caps->compatible.bounding[cap] = true; } caps->configured_unobserved++; } } static void svc_print_reason(const struct cap_check *check) { const char *reason = cap_union_reason(check); if (reason) printf(" (%s)", reason); } static void svc_print_needed_caps(void) { int cap; int found = 0; printf(" Needed capabilities:"); for (cap = 0; cap <= CAP_LAST_CAP; cap++) { struct cap_check *check = &state.app.checks[cap]; if (!cap_required_union(check) || !include_cap_in_recommendations(cap)) continue; if (!found) printf("\n"); printf(" %s", cap_name_safe(cap)); svc_print_reason(check); printf("\n"); found = 1; } if (!found) printf(" none\n"); } static void svc_print_phase_caps(const char *label, int operational) { int cap; int first = 1; printf(" %s", label); for (cap = 0; cap <= CAP_LAST_CAP; cap++) { struct cap_check *check = &state.app.checks[cap]; unsigned long granted; granted = operational ? check->op_granted : check->granted; if (granted == 0 || !include_cap_in_recommendations(cap)) continue; if (!first) printf(" "); printf("%s", cap_name_safe(cap)); first = 0; } if (first) printf("none"); printf("\n"); } static void print_outcome_error(const struct syscall_outcome *outcome, const char *syscall_name) { enum syscall_outcome_class class; const char *result_name; int error; class = classify_syscall_outcome(outcome->result); if (class == SYSCALL_OUTCOME_SUCCESS) { printf("%s: %lu succeeded\n", syscall_name, outcome->count); return; } result_name = syscall_result_name(outcome->result); error = syscall_result_errno(outcome->result); printf("%s: %lu %s with ", syscall_name, outcome->count, class == SYSCALL_OUTCOME_INTERRUPTED ? "interrupted" : "failed"); if (result_name) printf("-%s", result_name); else printf("%lld", (long long)outcome->result); if (error > 0 && error < 256) printf(" (%s)", strerror(error)); printf("\n"); } static void print_denied_outcomes(const struct cap_check *check, const char *indent) { size_t i; int found = 0; printf("%sOutcomes of syscall invocations containing such a check:\n", indent); for (i = 0; i < check->outcome_count; i++) { const struct syscall_outcome *outcome = &check->outcomes[i]; const char *syscall_name; char unknown[32]; syscall_name = syscall_name_from_nr(outcome->syscall_nr); if (!syscall_name) { snprintf(unknown, sizeof(unknown), "unknown(#%d)", outcome->syscall_nr); syscall_name = unknown; } printf("%s ", indent); print_outcome_error(outcome, syscall_name); found = 1; } for (i = 0; i < check->denied_syscall_count; i++) { const char *syscall_name; char unknown[32]; size_t j; for (j = 0; j < check->outcome_count; j++) { if (check->denied_syscalls[i] == check->outcomes[j].syscall_nr) break; } if (j < check->outcome_count) continue; syscall_name = syscall_name_from_nr(check->denied_syscalls[i]); if (!syscall_name) { snprintf(unknown, sizeof(unknown), "unknown(#%d)", check->denied_syscalls[i]); syscall_name = unknown; } printf("%s %s: outcome unavailable\n", indent, syscall_name); found = 1; } if (!found) printf("%s unavailable\n", indent); } static const char *failure_word(unsigned long count) { return count == 1 ? "failure" : "failures"; } static const char *interruption_word(unsigned long count) { return count == 1 ? "interruption" : "interruptions"; } static void print_denial_assessment(const struct cap_check *check, const char *indent) { struct cap_outcome_summary summary; char evidence[320]; unsigned long failed; summarize_cap_outcomes(check, &summary); switch (assess_cap_denials(check)) { case DENIAL_ASSESSMENT_PERMISSION: failed = summary.permission_failed + summary.other_failed; if (summary.other_failed > 0 && summary.interrupted > 0) snprintf(evidence, sizeof(evidence), "Associated syscall invocations: %lu succeeded, " "%lu failed, and %lu interrupted. Failure " "categories: %lu permission-related; %lu other.", summary.succeeded, failed, summary.interrupted, summary.permission_failed, summary.other_failed); else if (summary.other_failed > 0) snprintf(evidence, sizeof(evidence), "Associated syscall invocations: %lu succeeded and " "%lu failed. Failure categories: %lu " "permission-related; %lu other.", summary.succeeded, failed, summary.permission_failed, summary.other_failed); else if (summary.interrupted > 0) snprintf(evidence, sizeof(evidence), "Associated syscall invocations: %lu succeeded, " "%lu failed, and %lu interrupted. All failures " "were permission-related.", summary.succeeded, summary.permission_failed, summary.interrupted); else snprintf(evidence, sizeof(evidence), "Associated syscall invocations: %lu succeeded and " "%lu failed. All failures were permission-related.", summary.succeeded, summary.permission_failed); print_wrappedf(indent, "%s A permission failure does not prove that the " "capability check caused it. Manually investigate the " "permission-related failures and add the capability only " "if required functionality reproducibly fails because it " "is absent.", evidence); break; case DENIAL_ASSESSMENT_MIXED_INTERRUPTED: if (summary.other_failed > 0) print_wrappedf(indent, "Associated syscall outcomes: %lu succeeded, %lu " "non-permission %s, and %lu %s. Cap-audit cannot " "determine whether they came from the same call site " "or used the same arguments. Manual investigation is " "required to confirm that required functionality " "completed or was safely retried.", summary.succeeded, summary.other_failed, failure_word(summary.other_failed), summary.interrupted, interruption_word(summary.interrupted)); else print_wrappedf(indent, "Associated syscall outcomes: %lu succeeded and %lu " "%s. Cap-audit cannot determine whether they came " "from the same call site or used the same arguments. " "Manual investigation is required to confirm that " "required functionality completed or was safely " "retried.", summary.succeeded, summary.interrupted, interruption_word(summary.interrupted)); break; case DENIAL_ASSESSMENT_MIXED_OTHER: print_wrappedf(indent, "Associated syscall invocations: %lu succeeded and %lu " "failed for non-permission reasons. The not-granted " "capability checks are not established as the cause of " "those failures. The capability is not automatically " "recommended.", summary.succeeded, summary.other_failed); break; case DENIAL_ASSESSMENT_INCONCLUSIVE: if (check->outcome_count == 0) print_wrapped_text(indent, "Associated syscall outcomes are unavailable. Exercise " "the affected functionality again before making a " "capability decision."); else print_wrappedf(indent, "Available associated outcomes: %lu succeeded, %lu " "other %s, and %lu %s. The evidence remains " "inconclusive; exercise the affected functionality " "again before making a capability decision.", summary.succeeded, summary.other_failed, failure_word(summary.other_failed), summary.interrupted, interruption_word(summary.interrupted)); break; case DENIAL_ASSESSMENT_SUCCEEDED: print_wrappedf(indent, "Associated syscall outcomes: %lu succeeded. The " "capability was not granted by those checks, but the " "associated syscall invocations completed. The capability " "is not automatically recommended.", summary.succeeded); break; case DENIAL_ASSESSMENT_OTHER: print_wrappedf(indent, "Associated syscall outcomes: %lu non-permission %s. The " "not-granted capability checks are not established as " "their cause.", summary.other_failed, failure_word(summary.other_failed)); break; case DENIAL_ASSESSMENT_NONE: break; } } static const char *denial_review_label(enum denial_assessment assessment) { switch (assessment) { case DENIAL_ASSESSMENT_PERMISSION: case DENIAL_ASSESSMENT_MIXED_INTERRUPTED: return "Manual investigation required"; case DENIAL_ASSESSMENT_MIXED_OTHER: return "Omitted; successes and non-permission failures observed"; case DENIAL_ASSESSMENT_INCONCLUSIVE: return "Additional evidence required"; case DENIAL_ASSESSMENT_SUCCEEDED: return "Omitted; associated syscalls succeeded"; case DENIAL_ASSESSMENT_OTHER: return "Omitted; capability check not established as cause"; case DENIAL_ASSESSMENT_NONE: return NULL; } return NULL; } static void svc_print_denied_review(const service_config_t *cfg) { int cap; int found = 0; int capset_overlap = 0; for (cap = 0; cap <= CAP_LAST_CAP; cap++) { struct cap_check *check = &state.app.checks[cap]; const char *label; if (cap_total_denied(check) == 0 || cap_total_granted(check) > 0 || !include_cap_in_recommendations(cap)) continue; if (!found) printf(" Capabilities with only not-granted checks:\n"); label = denial_review_label(assess_cap_denials(check)); printf(" %s: %s\n", cap_name_safe(cap), label ? label : "no assessment"); if (cfg->bounding.seen && !cfg->bounding.caps[cap]) print_wrapped_text(" Note: ", "Capability is absent from the configured " "CapabilityBoundingSet."); if (cap_is_capset_only(cap)) capset_overlap = 1; found = 1; } if (found) { print_wrapped_text(" ", "Detailed evidence appears in CAPABILITIES WITH ONLY " "NOT-GRANTED CHECKS above. A not-granted check does not " "add a capability automatically."); if (capset_overlap) print_wrapped_text(" ", "A listed capability is retained separately because a " "successful capset requested it."); } else { printf(" Capabilities with only not-granted checks: none\n"); } } static void svc_print_recommended_denials(void) { bool caps[CAP_LAST_CAP + 1] = { false }; int cap; int found = 0; for (cap = 0; cap <= CAP_LAST_CAP; cap++) { struct cap_check *check = &state.app.checks[cap]; if (cap_total_denied(check) == 0 || !cap_required_union(check) || !include_cap_in_recommendations(cap)) continue; caps[cap] = true; found = 1; } if (!found) return; printf(" Mixed capability-check results: "); svc_print_cap_list(caps); printf("\n"); print_wrapped_text(" ", "These capabilities had both granted and not-granted checks. " "They are included because granted use was observed; the " "not-granted checks do not change the generated configuration."); } static void svc_print_capset_only(const service_config_t *cfg) { bool caps[CAP_LAST_CAP + 1] = { false }; int cap; int found = 0; for (cap = 0; cap <= CAP_LAST_CAP; cap++) { if (!cap_is_capset_only(cap) || !include_cap_in_recommendations(cap)) continue; caps[cap] = true; found = 1; } if (!found) return; printf(" Capset-only compatibility capabilities: "); svc_print_cap_list(caps); printf("\n"); print_wrapped_text(" ", "The current-binary-compatible configuration retains these " "capabilities. Detailed application-cleanup guidance appears " "in CAPSET-ONLY CAPABILITIES above."); if (!cfg->bounding.seen) return; for (cap = 0; cap <= CAP_LAST_CAP; cap++) { if (!caps[cap] || cfg->bounding.caps[cap]) continue; print_wrappedf(" Warning: ", "%s is absent from the configured CapabilityBoundingSet. " "The observed capset may fail until the application stops " "requesting it or the capability is restored.", cap_name_safe(cap)); } } static void svc_print_configured_unobserved(const service_config_t *cfg) { bool ambient_and_bounding[CAP_LAST_CAP + 1] = { false }; bool ambient_only[CAP_LAST_CAP + 1] = { false }; bool bounding_only[CAP_LAST_CAP + 1] = { false }; int cap; int found = 0; int found_ambient_and_bounding = 0; int found_ambient_only = 0; int found_bounding_only = 0; for (cap = 0; cap <= CAP_LAST_CAP; cap++) { int in_ambient; int in_bounding; if (!svc_cap_configured_unobserved(cfg, cap)) continue; in_ambient = cfg->ambient.seen && cfg->ambient.caps[cap]; in_bounding = cfg->bounding.seen && cfg->bounding.caps[cap]; if (in_ambient && in_bounding) { ambient_and_bounding[cap] = true; found_ambient_and_bounding = 1; } else if (in_ambient) { ambient_only[cap] = true; found_ambient_only = 1; } else { bounding_only[cap] = true; found_bounding_only = 1; } found = 1; } if (!found) { printf(" Configured but not observed in this run: none\n"); return; } printf(" Configured but not observed in this run:\n"); if (found_ambient_and_bounding) svc_print_wrapped_cap_list( " AmbientCapabilities and CapabilityBoundingSet: ", ambient_and_bounding); if (found_ambient_only) svc_print_wrapped_cap_list( " AmbientCapabilities only: ", ambient_only); if (found_bounding_only) svc_print_wrapped_cap_list( " CapabilityBoundingSet only: ", bounding_only); print_wrapped_text(" Assessment: ", "No confirmed capability check or successful capset request was " "observed. Relevant functionality may not have been exercised."); print_wrapped_text(" Recommendation: ", "Retained in the current-service-compatible configuration. " "Treat these as removal candidates only after manual review and " "targeted testing."); } static void svc_print_generated_config(const service_config_t *cfg, const char *group, int is_root, const struct svc_deployment_caps *caps) { printf(" [Service]\n"); printf(" User=%s\n", svc_user_name(cfg)); printf(" Group=%s\n", group); if (is_root) printf(" # AmbientCapabilities omitted for root service\n"); else { printf(" AmbientCapabilities="); svc_print_cap_list(caps->ambient); printf("\n"); } printf(" CapabilityBoundingSet="); svc_print_cap_list(caps->bounding); printf("\n"); /* * Enabling NoNewPrivileges was not tested when the service was traced * without it, so preserve the setting that governed the audit. */ printf(" NoNewPrivileges=%s\n", cfg->no_new_privs ? "yes" : "no"); } static void print_service_recommendations(void) { const service_config_t *cfg = state.service_cfg; struct svc_recommendations recommended; char group_buf[32]; gid_t gid = svc_effective_gid(cfg); const char *group = svc_group_name(gid, group_buf, sizeof(group_buf)); int is_root = svc_is_root_service(cfg); svc_build_recommendations(cfg, &recommended); printf("SYSTEMD SERVICE RECOMMENDATIONS:\n"); print_rule('-'); printf(" Current service context:\n"); printf(" Unit file: %s\n", state.service_file); printf(" ExecStart: %s\n", cfg->exec_start ? cfg->exec_start : "(not configured)"); printf(" User: %s (uid=%u)\n", svc_user_name(cfg), cfg->user_is_set ? (unsigned int)cfg->user_uid : 0); printf(" Group: %s (gid=%u)\n", group, (unsigned int)gid); printf(" NoNewPrivileges: %s\n", cfg->no_new_privs ? "yes" : "no"); printf(" AmbientCapabilities: "); svc_print_ambient_set(&cfg->ambient); printf("\n"); printf(" CapabilityBoundingSet: "); svc_print_cap_set(&cfg->bounding); printf("\n\n"); printf(" Analysis:\n"); svc_print_needed_caps(); if (state.capset_observed) { print_wrapped_text(" ", "capset was observed. CapabilityBoundingSet " "should include the union of initialization " "and operational capabilities."); svc_print_phase_caps("Initialization capabilities: ", 0); svc_print_phase_caps("Operational capabilities: ", 1); } if (is_root) print_wrapped_text(" ", "AmbientCapabilities omitted because ambient " "capabilities are not meaningful for root " "services."); if (!cfg->no_new_privs) print_wrapped_text(" NoNewPrivileges: ", "The service was audited with this setting disabled. " "Enabling it was not validated, so the generated " "configuration retains NoNewPrivileges=no. Test the " "change separately, including functionality that starts " "child processes or helpers, before enabling it."); svc_print_denied_review(cfg); svc_print_recommended_denials(); svc_print_capset_only(cfg); svc_print_configured_unobserved(cfg); printf("\n"); if (recommended.configured_unobserved > 0) printf(" Recommended current-service-compatible [Service] " "configuration:\n"); else if (capset_only_count() > 0) printf(" Recommended current-binary-compatible [Service] " "configuration:\n"); else printf(" Recommended [Service] configuration:\n"); svc_print_generated_config(cfg, group, is_root, &recommended.compatible); printf("\n"); } static void print_updatev_wrapped(const char *prefix, const char *cap_prefix, const char *suffix) { int width = get_output_width(); size_t prefix_len = strlen(prefix); size_t suffix_len = strlen(suffix); int cur_len = prefix_len; int cont_indent = 8; int i; if (width < 40) width = 40; for (i = 0; prefix[i] == ' '; i++) ; if (i > 0) cont_indent = i + 4; printf("%s", prefix); for (i = 0; i <= CAP_LAST_CAP; i++) { char cap_name[64]; char item[96]; size_t item_len; if (!cap_in_programmatic_set(i)) continue; cap_name_upper_buf(i, cap_name, sizeof(cap_name)); snprintf(item, sizeof(item), "%s%s", cap_prefix, cap_name); item_len = strlen(item); if (cur_len > (int)prefix_len && cur_len + 2 + (int)item_len + (int)suffix_len > width) { printf(",\n%*s%s", cont_indent, "", item); cur_len = cont_indent + item_len; } else { printf(", %s", item); cur_len += 2 + item_len; } } if (cur_len + 2 + (int)suffix_len > width && cur_len > cont_indent) { printf(",\n%*s%s", cont_indent, "", suffix); } else { printf(", %s", suffix); } } static void print_denied_recommendation_review(void) { int cap; int found = 0; int capset_overlap = 0; for (cap = 0; cap <= CAP_LAST_CAP; cap++) { struct cap_check *check = &state.app.checks[cap]; enum denial_assessment assessment; const char *label = NULL; if (cap_total_denied(check) == 0 || cap_total_granted(check) > 0) continue; if (!found) printf(" Capabilities with only not-granted checks:\n"); assessment = assess_cap_denials(check); label = denial_review_label(assessment); if (!label) continue; printf(" %s: %s\n", cap_name_safe(cap), label); if (cap_is_capset_only(cap)) capset_overlap = 1; found = 1; } if (found) { print_wrapped_text(" ", "A not-granted check does not add a capability " "automatically."); if (capset_overlap) print_wrapped_text(" ", "A listed capability is retained separately because a " "successful capset requested it."); printf("\n"); } } static int cap_has_related_system_context(int cap) { /* * Global settings cannot be correlated with an individual check, so * they remain diagnostic context and never drive recommendations. * protected_symlinks is intentionally absent because that policy * explicitly ignores CAP_DAC_OVERRIDE. */ switch (cap) { case CAP_SYS_PTRACE: return state.app.yama_ptrace_scope > 0 || state.app.suid_dumpable == 2; case CAP_PERFMON: return state.app.perf_event_paranoid >= 2; case CAP_BPF: return state.app.unprivileged_bpf_disabled == 1; case CAP_SYSLOG: return state.app.kptr_restrict >= 1 || state.app.dmesg_restrict >= 1; case CAP_SYS_MODULE: return state.app.modules_disabled == 1; case CAP_SYS_RAWIO: return state.app.mmap_min_addr > 0; case CAP_FOWNER: return state.app.protected_hardlinks == 1; default: return 0; } } static void print_system_context_value(const char *name, int value, const char *effect) { print_wrappedf(" ", "%s = %d: %s", name, value, effect); } static int print_capability_system_context(void) { int cap; int count = 0; printf("CAPABILITY-RELATED SYSTEM CONTEXT:\n"); print_rule('-'); for (cap = 0; cap <= CAP_LAST_CAP; cap++) { char cap_name[64]; if (cap_total_checks(&state.app.checks[cap]) == 0 || !cap_has_related_system_context(cap)) continue; if (count == 0) print_wrapped_text(" ", "These settings govern operations that can use the " "listed capabilities. They cannot be correlated with a " "particular observed check and do not prove that a " "capability is required."); cap_name_upper_buf(cap, cap_name, sizeof(cap_name)); printf(" CAP_%s\n", cap_name); switch (cap) { case CAP_SYS_PTRACE: if (state.app.yama_ptrace_scope > 0) print_system_context_value( "kernel.yama.ptrace_scope", state.app.yama_ptrace_scope, "may restrict ptrace access."); if (state.app.suid_dumpable == 2) print_system_context_value( "fs.suid_dumpable", state.app.suid_dumpable, "affects core dumps and ptrace of privileged " "programs."); break; case CAP_PERFMON: print_system_context_value( "kernel.perf_event_paranoid", state.app.perf_event_paranoid, "may restrict performance monitoring."); break; case CAP_BPF: print_system_context_value( "kernel.unprivileged_bpf_disabled", state.app.unprivileged_bpf_disabled, "restricts unprivileged BPF use."); break; case CAP_SYSLOG: if (state.app.kptr_restrict >= 1) print_system_context_value( "kernel.kptr_restrict", state.app.kptr_restrict, "restricts exposure of kernel pointers."); if (state.app.dmesg_restrict >= 1) print_system_context_value( "kernel.dmesg_restrict", state.app.dmesg_restrict, "restricts access to the kernel log."); break; case CAP_SYS_MODULE: print_system_context_value( "kernel.modules_disabled", state.app.modules_disabled, "permanently disables module loading; CAP_SYS_MODULE " "cannot override it."); break; case CAP_SYS_RAWIO: print_system_context_value( "vm.mmap_min_addr", state.app.mmap_min_addr, "restricts mappings below this address."); break; case CAP_FOWNER: print_system_context_value( "fs.protected_hardlinks", state.app.protected_hardlinks, "restricts creation of hard links to files not owned " "by the caller."); break; default: break; } printf("\n"); count++; } if (count == 0) printf(" None\n\n"); return count; } void analyze_capabilities(void) { int has_required = 0; int has_denied = 0; int required_count = 0; int context_count; int denied_count = 0; int denied_manual = 0; int denied_mixed_other = 0; int denied_inconclusive = 0; int denied_succeeded = 0; int denied_other = 0; int capset_only = capset_only_count(); unsigned long total_checks = 0; int i; int first; printf("\n"); print_rule('='); print_wrappedf("", "CAPABILITY ANALYSIS FOR: %s (PID %d)", state.app.exe, state.app.pid); print_rule('='); printf("\n"); printf("SYSTEM CONTEXT:\n"); print_rule('-'); printf(" Kernel version: %s\n", state.app.kernel_version); printf(" kernel.yama.ptrace_scope: %d\n", state.app.yama_ptrace_scope); printf(" kernel.kptr_restrict: %d\n", state.app.kptr_restrict); printf(" kernel.dmesg_restrict: %d\n", state.app.dmesg_restrict); printf(" kernel.modules_disabled: %d\n", state.app.modules_disabled); printf(" kernel.perf_event_paranoid: %d\n", state.app.perf_event_paranoid); printf(" kernel.unprivileged_bpf_disabled: %d\n", state.app.unprivileged_bpf_disabled); printf(" net.core.bpf_jit_enable: %d\n", state.app.bpf_jit_enable); printf(" net.core.bpf_jit_harden: %d\n", state.app.bpf_jit_harden); printf(" net.core.bpf_jit_kallsyms: %d\n", state.app.bpf_jit_kallsyms); printf(" vm.mmap_min_addr: %d\n", state.app.mmap_min_addr); printf(" fs.protected_hardlinks: %d\n", state.app.protected_hardlinks); printf(" fs.protected_symlinks: %d\n", state.app.protected_symlinks); printf(" fs.suid_dumpable: %d\n", state.app.suid_dumpable); printf("\n"); printf("REQUIRED CAPABILITIES:\n"); print_rule('-'); if (!state.capset_observed) { for (i = 0; i <= CAP_LAST_CAP; i++) { struct cap_check *check; check = &state.app.checks[i]; if (check->granted > 0) { has_required = 1; printf(" %s (#%d)\n", cap_name_safe(i), i); printf(" Checks: %lu granted, %lu denied\n", check->granted, check->denied); if (check->reason) print_wrappedf(" Reason: ", "%s", check->reason); if (!include_cap_in_recommendations(i)) print_wrapped_text(" Note: ", "Internal to capability setup; excluded from recommendations."); printf("\n"); } } if (!has_required) print_wrapped_text(" ", "None - No confirmed granted capability use was " "observed.\n"); } else { print_wrapped_text("", "INITIALIZATION CAPABILITIES (before capability drop):"); print_rule('-'); for (i = 0; i <= CAP_LAST_CAP; i++) { struct cap_check *check = &state.app.checks[i]; if (check->granted > 0) { has_required = 1; printf(" %s (#%d)\n", cap_name_safe(i), i); printf(" Checks: %lu granted, %lu denied\n", check->granted, check->denied); if (check->reason) print_wrappedf(" Reason: ", "%s", check->reason); if (!include_cap_in_recommendations(i)) print_wrapped_text(" Note: ", "Internal to capability setup; excluded from recommendations."); printf("\n"); } } if (!has_required) printf(" None\n\n"); print_wrapped_text("", "OPERATIONAL CAPABILITIES (after capability drop):"); print_rule('-'); has_required = 0; for (i = 0; i <= CAP_LAST_CAP; i++) { struct cap_check *check = &state.app.checks[i]; if (check->op_granted > 0) { has_required = 1; printf(" %s (#%d)\n", cap_name_safe(i), i); printf(" Checks: %lu granted, %lu denied\n", check->op_granted, check->op_denied); if (check->op_reason) print_wrappedf(" Reason: ", "%s", check->op_reason); if (!include_cap_in_recommendations(i)) print_wrapped_text(" Note: ", "Internal to capability setup; excluded from recommendations."); printf("\n"); } } if (!has_required) printf(" None\n\n"); } print_capset_only_section(); context_count = print_capability_system_context(); printf("CAPABILITIES WITH ONLY NOT-GRANTED CHECKS:\n"); print_rule('-'); for (i = 0; i <= CAP_LAST_CAP; i++) { struct cap_check *check; check = &state.app.checks[i]; if (cap_total_denied(check) > 0 && cap_total_granted(check) == 0) { has_denied = 1; printf(" %s (#%d)\n", cap_name_safe(i), i); printf(" Capability checks returning not granted: %lu\n", cap_total_denied(check)); print_denied_outcomes(check, " "); print_denial_assessment(check, " Assessment: "); if (cap_is_capset_only(i)) print_wrapped_text(" Recommendation: ", "Not added based on the not-granted checks. " "Retained separately in compatible deployment " "configurations because a successful capset " "requested it."); else print_wrapped_text(" Recommendation: ", "Not added automatically."); printf("\n"); } } if (!has_denied) printf(" None\n\n"); for (i = 0; i <= CAP_LAST_CAP; i++) { total_checks += cap_total_checks(&state.app.checks[i]); if (cap_required_union(&state.app.checks[i])) required_count++; if (cap_total_denied(&state.app.checks[i]) > 0 && cap_total_granted(&state.app.checks[i]) == 0) { enum denial_assessment assessment; denied_count++; assessment = assess_cap_denials(&state.app.checks[i]); switch (assessment) { case DENIAL_ASSESSMENT_PERMISSION: case DENIAL_ASSESSMENT_MIXED_INTERRUPTED: denied_manual++; break; case DENIAL_ASSESSMENT_INCONCLUSIVE: denied_inconclusive++; break; case DENIAL_ASSESSMENT_MIXED_OTHER: denied_mixed_other++; break; case DENIAL_ASSESSMENT_SUCCEEDED: denied_succeeded++; break; case DENIAL_ASSESSMENT_OTHER: denied_other++; break; case DENIAL_ASSESSMENT_NONE: break; } } } printf("SUMMARY:\n"); print_rule('-'); printf(" Total capability checks: %lu\n", total_checks); printf(" Required capabilities: %d\n", required_count); printf(" Capabilities with related system context: %d\n", context_count); printf(" Successful capset calls: %lu\n", state.app.capset.successful_calls); printf(" Capset-only capabilities: %d\n", capset_only); printf(" Capabilities with only not-granted checks: %d\n", denied_count); if (denied_count > 0) { printf(" Assessment counts:\n"); if (denied_manual > 0) printf(" Manual investigation required: %d\n", denied_manual); if (denied_inconclusive > 0) printf(" Additional evidence required: %d\n", denied_inconclusive); if (denied_mixed_other > 0) printf(" Omitted after mixed success/non-permission " "failure: %d\n", denied_mixed_other); if (denied_succeeded > 0) printf(" Omitted after associated syscall success: %d\n", denied_succeeded); if (denied_other > 0) printf(" Capability check not established as cause: %d\n", denied_other); } printf("\n"); if (state.foreign_target_ns_observed) { printf("RECOMMENDATIONS:\n"); print_rule('-'); print_wrapped_text(" Warning: ", "Capability checks targeted a different user " "namespace. Automatic capability recommendations " "are suppressed because their safe scope cannot " "be determined."); printf("\n"); } else if (state.service_cfg) { print_service_recommendations(); } else if (required_count > 0 || capset_only > 0) { printf("RECOMMENDATIONS:\n"); print_rule('-'); print_denied_recommendation_review(); if (state.app.prog_type != UNSUPPORTED && required_count > 0) { printf(" Programmatic solution (%s):\n", state.app.prog_type == ELF ? "C with libcap-ng" : "Python with python3-libcap-ng"); if (state.capset_observed) print_wrapped_text(" Note: ", "The application drops capabilities after initialization. The programmatic snippet reflects the operational set only."); if (state.app.prog_type == ELF) { printf(" #include \n"); printf(" #include \n"); printf(" #include \n"); printf(" ...\n"); printf(" capng_clear(CAPNG_SELECT_BOTH);\n"); print_updatev_wrapped(" capng_updatev(CAPNG_ADD, " "CAPNG_EFFECTIVE|CAPNG_PERMITTED", "CAP_", "-1);\n"); printf(" if (capng_change_id(uid, gid, " "CAPNG_DROP_SUPP_GRP | " "CAPNG_CLEAR_BOUNDING)) {\n"); printf("\tperror(\"capng_change_id\");\n"); /* Failure can leave partial privileges. */ printf("\texit(EXIT_FAILURE);\n"); printf(" }\n\n"); } else if (state.app.prog_type == PYTHON) { printf(" import sys\n"); printf(" import _capng as capng\n"); printf(" ...\n"); printf(" capng.capng_clear(capng.CAPNG_SELECT_BOTH)\n"); print_updatev_wrapped(" capng.capng_updatev(" "capng.CAPNG_ADD, " "capng.CAPNG_EFFECTIVE|" "capng.CAPNG_PERMITTED", "capng.", "-1)\n"); printf(" e = capng.capng_change_id(uid, gid, " "capng.CAPNG_DROP_SUPP_GRP | " "capng.CAPNG_CLEAR_BOUNDING)\n"); printf(" if e < 0:\n"); printf("\tprint(f\"Error: {e}\")\n"); printf("\tsys.exit(1)\n\n"); } } if (capset_only > 0) print_wrapped_text(" Compatibility note: ", "Deployment snippets retain capset-only capabilities " "so the observed capset calls can still succeed. Remove " "them from application capability setup before narrowing " "these deployment sets."); printf(" For systemd service%s:\n", capset_only > 0 ? " (current-binary-compatible)" : ""); if (state.capset_observed) print_wrapped_text(" Note: ", "Ambient capabilities must include initialization requirements. The application drops to the operational set internally via capset."); printf(" [Service]\n"); printf(" User=\n"); printf(" Group=\n"); printf(" AmbientCapabilities="); first = 1; for (i = 0; i <= CAP_LAST_CAP; i++) { if (cap_is_compat_requirement(i) && include_cap_in_recommendations(i)) { if (!first) printf(" "); printf("%s", cap_name_safe(i)); first = 0; } } printf("\n"); printf(" CapabilityBoundingSet="); first = 1; for (i = 0; i <= CAP_LAST_CAP; i++) { if (cap_is_compat_requirement(i) && include_cap_in_recommendations(i)) { if (!first) printf(" "); printf("%s", cap_name_safe(i)); first = 0; } } printf("\n\n"); printf(" For file capabilities (via filecap):\n"); if (state.capset_observed) print_wrapped_text(" Note: ", "File capabilities must include initialization requirements. The application drops to the operational set internally via capset."); printf(" filecap /path/to/binary"); for (i = 0; i <= CAP_LAST_CAP; i++) { if (cap_is_compat_requirement(i) && include_cap_in_recommendations(i)) printf(" %s", cap_name_safe(i)); } printf("\n\n"); printf(" For Docker/Podman:\n"); if (state.capset_observed) print_wrapped_text(" Note: ", "Container capabilities must include initialization requirements. The application drops to the operational set internally via capset."); printf(" docker run --user $(id -u):$(id -g) \\\n"); printf(" --cap-drop=ALL \\\n"); for (i = 0; i <= CAP_LAST_CAP; i++) { if (cap_is_compat_requirement(i) && include_cap_in_recommendations(i)) printf(" --cap-add=%s \\\n", cap_name_safe(i)); } printf(" your-image:tag\n\n"); printf(" For Kubernetes:\n"); if (state.capset_observed) print_wrapped_text(" Note: ", "Container capabilities must include initialization requirements. The application drops to the operational set internally via capset."); printf(" securityContext:\n"); printf(" runAsUser: 1000\n"); printf(" runAsGroup: 1000\n"); printf(" capabilities:\n"); printf(" drop:\n"); printf(" - ALL\n"); printf(" add:\n"); for (i = 0; i <= CAP_LAST_CAP; i++) { if (cap_is_compat_requirement(i) && include_cap_in_recommendations(i)) printf(" - %s\n", cap_name_safe(i)); } printf("\n"); } else { printf("RECOMMENDATIONS:\n"); print_rule('-'); if (denied_count > 0) { print_wrapped_text(" ", "No capabilities have confirmed granted use, so none " "are recommended automatically."); print_denied_recommendation_review(); } else { print_wrapped_text(" ", "This application does not require any elevated " "capabilities!"); print_wrapped_text(" ", "Run as an unprivileged user with no special " "capabilities."); } printf("\n"); } print_wrapped_text("EXPERIMENTAL NOTICE: ", "cap-audit output is experimental, but very close."); } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/cap_audit_capset.c000066400000000000000000000026251524174023300245210ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* * cap-audit - Track capabilities requested by successful capset calls. * Copyright 2026 Red Hat Inc. * All Rights Reserved. */ #include "cap_audit.h" /* Merge one successful capset payload into the application-wide union. */ void record_successful_capset(const struct cap_event *event) { if (event->syscall_ret != 0) return; state.app.capset.effective |= event->capset_effective; state.app.capset.permitted |= event->capset_permitted; state.app.capset.inheritable |= event->capset_inheritable; state.app.capset.successful_calls++; } /* Return whether any successful capset requested the capability. */ int cap_requested_by_capset(int cap) { __u64 requested; if (cap < 0 || cap >= 64) return 0; requested = state.app.capset.effective | state.app.capset.permitted | state.app.capset.inheritable; return (requested & (1ULL << cap)) != 0; } /* Identify capset requests that have no confirmed granted use. */ int cap_is_capset_only(int cap) { if (cap < 0 || cap > CAP_LAST_CAP) return 0; return cap_requested_by_capset(cap) && !cap_required_union(&state.app.checks[cap]); } /* Build the safe current-binary union used by deployment recommendations. */ int cap_is_compat_requirement(int cap) { if (cap < 0 || cap > CAP_LAST_CAP) return 0; return cap_required_union(&state.app.checks[cap]) || cap_requested_by_capset(cap); } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/cap_audit_event.c000066400000000000000000000157721524174023300243720ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* * cap-audit - Trace a target process to discover required capabilities. * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Authors: * Steve Grubb * Portions of this code were made with codex 5.2 */ /* Ring buffer event processing: noise filtering, shutdown backstop, * and per-capability accounting for observed checks. */ #include "cap_audit.h" #include #include static int is_always_noise(const struct cap_event *e) { if (e->syscall_nr == state.app.execve_nr && (e->capability == CAP_SYS_ADMIN || e->capability == CAP_SETPCAP)) return 1; if ((e->cap_opts & CAP_OPT_NOAUDIT) && e->capability == CAP_SYS_ADMIN && (e->syscall_nr == state.app.brk_nr || e->syscall_nr == state.app.mmap_nr || e->syscall_nr == state.app.mprotect_nr || e->syscall_nr == state.app.mremap_nr)) return 1; return 0; } static int is_shutdown_noise(const struct cap_event *e) { if (!state.shutting_down) return 0; if (e->pid != (__u32)state.app.pid) return 0; if (e->capability == CAP_SYS_ADMIN || e->capability == CAP_SETPCAP) return 1; return 0; } static void add_denied_syscall(struct cap_check *check, int syscall_nr) { size_t i; int *tmp; size_t new_cap; for (i = 0; i < check->denied_syscall_count; i++) { if (check->denied_syscalls[i] == syscall_nr) return; } if (check->denied_syscall_count == check->denied_syscall_capacity) { new_cap = check->denied_syscall_capacity ? check->denied_syscall_capacity * 2 : 4; tmp = realloc(check->denied_syscalls, new_cap * sizeof(int)); if (!tmp) return; check->denied_syscalls = tmp; check->denied_syscall_capacity = new_cap; } check->denied_syscalls[check->denied_syscall_count++] = syscall_nr; } static int check_has_denied_syscall(const struct cap_check *check, int syscall_nr) { size_t i; for (i = 0; i < check->denied_syscall_count; i++) { if (check->denied_syscalls[i] == syscall_nr) return 1; } return 0; } static void handle_syscall_result(const struct cap_event *e) { int cap; for (cap = 0; cap <= CAP_LAST_CAP && cap < 64; cap++) { struct cap_check *check; if (!(e->denied_caps & (1ULL << cap))) continue; check = &state.app.checks[cap]; /* Ignore outcomes for capability events filtered by userspace. */ if (!check_has_denied_syscall(check, e->syscall_nr)) continue; if (state.shutting_down && e->pid == (__u32)state.app.pid && (cap == CAP_SYS_ADMIN || cap == CAP_SETPCAP)) continue; if (add_cap_syscall_outcome(check, e->syscall_nr, e->syscall_ret) != 0 && state.verbose) fprintf(stderr, "Warning: unable to record syscall outcome\n"); } if (state.verbose) printf("[SYSCALL] pid=%u syscall=%s return=%lld " "denied_caps=0x%llx\n", e->pid, syscall_name_from_nr(e->syscall_nr) ?: "unknown", (long long)e->syscall_ret, (unsigned long long)e->denied_caps); } /* Mark the first capset from the initial process as the phase boundary. */ static void observe_initial_capset(const struct cap_event *e) { if (state.capset_observed || e->syscall_nr != state.app.capset_nr || e->pid != (__u32)state.app.pid) return; state.capset_observed = 1; if (state.verbose) printf("[CAP] Capability drop detected (capset from " "initial PID); switching to operational phase\n"); } /* * Child requests also constrain the inherited deployment boundary, but only * the initial process's capset separates its initialization and operation. */ static void handle_capset_result(const struct cap_event *e) { if (e->syscall_ret != 0) return; record_successful_capset(e); observe_initial_capset(e); if (state.verbose) printf("[CAPSET] pid=%u effective=0x%llx permitted=0x%llx " "inheritable=0x%llx\n", e->pid, (unsigned long long)e->capset_effective, (unsigned long long)e->capset_permitted, (unsigned long long)e->capset_inheritable); } int handle_cap_event(void *ctx __attribute__((unused)), void *data, size_t data_sz __attribute__((unused))) { const struct cap_event *e = data; int op_phase = state.capset_observed; if (e->event_type == CAP_EVENT_SYSCALL_RESULT) { handle_syscall_result(e); return 0; } if (e->event_type == CAP_EVENT_CAPSET) { handle_capset_result(e); return 0; } if (e->event_type != CAP_EVENT_CHECK) return 0; if (is_always_noise(e)) { if (state.verbose) { if (e->syscall_nr == state.app.execve_nr) printf("[CAP] Filtered exec noise: " "cap=%s syscall=%s\n", cap_name_safe(e->capability), syscall_name_from_nr(e->syscall_nr) ?: "unknown"); else printf("[CAP] Filtered advisory check: " "cap=%s syscall=%s (CAP_OPT_NOAUDIT)\n", cap_name_safe(e->capability), syscall_name_from_nr(e->syscall_nr) ?: "unknown"); } return 0; } if (is_shutdown_noise(e)) { if (state.verbose) printf("[CAP] Filtered shutdown noise: " "cap=%s syscall=%s\n", cap_name_safe(e->capability), syscall_name_from_nr(e->syscall_nr) ?: "unknown"); return 0; } /* * A target namespace does not identify the namespace that scoped the * checked credentials. Do not turn an ambiguous observation into a * potentially broader capability grant. */ if (e->targ_ns_inum != 0 && state.baseline_user_ns_inum != 0 && e->targ_ns_inum != state.baseline_user_ns_inum) state.foreign_target_ns_observed = true; if (state.verbose) { printf("[CAP] pid=%d cap=%s result=%s syscall=%s " "comm=%s\n", e->pid, cap_name_safe(e->capability), e->result ? "GRANTED" : "DENIED", syscall_name_from_nr(e->syscall_nr) ?: "unknown", e->comm); } observe_initial_capset(e); if (e->capability >= 0 && e->capability <= CAP_LAST_CAP) { struct cap_check *check; check = &state.app.checks[e->capability]; check->capability = e->capability; if (op_phase) { check->op_count++; if (e->result > 0) check->op_granted++; else if (e->result == 0) { check->op_denied++; add_denied_syscall(check, e->syscall_nr); } if (e->result > 0 && check->op_needed != 1) { check->op_needed = 1; update_reason_op(check, e->syscall_nr); } } else { check->count++; if (e->result > 0) check->granted++; else if (e->result == 0) { check->denied++; add_denied_syscall(check, e->syscall_nr); } if (e->result > 0 && check->needed != 1) { check->needed = 1; update_reason(check, e->syscall_nr); } } } return 0; } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/cap_audit_json.c000066400000000000000000000230231524174023300242060ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* * cap-audit - Trace a target process to discover required capabilities. * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Authors: * Steve Grubb * Portions of this code were made with codex 5.2 */ /* JSON output serialization: machine-readable capability summaries for * automated tooling and regression comparison. */ #include "cap_audit.h" #include #include static void print_json_denied_syscalls(const struct cap_check *check) { size_t i; printf(" \"denied_syscalls\": [\n"); for (i = 0; i < check->denied_syscall_count; i++) { const char *name; char *name_json; name = syscall_name_from_nr(check->denied_syscalls[i]); name_json = json_escape(name ? name : "unknown"); if (i > 0) printf(",\n"); printf(" {\"number\": %d, \"name\": \"%s\"}", check->denied_syscalls[i], name_json ? name_json : ""); free(name_json); } printf("\n ],\n"); } static void print_json_outcomes(const struct cap_check *check) { size_t i; printf(" \"assessment\": \"%s\",\n", denial_assessment_name(assess_cap_denials(check))); printf(" \"syscall_outcomes\": [\n"); for (i = 0; i < check->outcome_count; i++) { const struct syscall_outcome *outcome = &check->outcomes[i]; enum syscall_outcome_class class; const char *result_name; const char *syscall_name; char *syscall_json; int error; class = classify_syscall_outcome(outcome->result); result_name = syscall_result_name(outcome->result); error = syscall_result_errno(outcome->result); syscall_name = syscall_name_from_nr(outcome->syscall_nr); syscall_json = json_escape(syscall_name ? syscall_name : "unknown"); if (i > 0) printf(",\n"); printf(" {\n"); printf(" \"syscall_number\": %d,\n", outcome->syscall_nr); printf(" \"syscall\": \"%s\",\n", syscall_json ? syscall_json : ""); printf(" \"outcome_class\": \"%s\",\n", syscall_outcome_class_name(class)); printf(" \"raw_return\": %lld,\n", (long long)outcome->result); if (result_name) printf(" \"return_name\": \"%s\",\n", result_name); else printf(" \"return_name\": null,\n"); if (error > 0) printf(" \"errno\": %d,\n", error); else printf(" \"errno\": null,\n"); printf(" \"count\": %lu\n", outcome->count); printf(" }"); free(syscall_json); } printf("\n ]\n"); } static void print_json_capset_only(void) { int cap; int first_cap = 1; printf(" \"capset_only_capabilities\": [\n"); for (cap = 0; cap <= CAP_LAST_CAP; cap++) { char *name_json; __u64 mask; int first_set = 1; if (!cap_is_capset_only(cap)) continue; name_json = json_escape(capng_capability_to_name(cap)); if (!first_cap) printf(",\n"); printf(" {\n"); printf(" \"number\": %d,\n", cap); printf(" \"name\": \"%s\",\n", name_json ? name_json : ""); printf(" \"requested_sets\": ["); mask = 1ULL << cap; if (state.app.capset.effective & mask) { printf("\"effective\""); first_set = 0; } if (state.app.capset.permitted & mask) { printf("%s\"permitted\"", first_set ? "" : ", "); first_set = 0; } if (state.app.capset.inheritable & mask) printf("%s\"inheritable\"", first_set ? "" : ", "); printf("]\n"); printf(" }"); first_cap = 0; free(name_json); } printf("\n ]"); } void output_json(void) { int i; int first_cap; int first_denied; char *exe_json; char *kernel_json; exe_json = json_escape(state.app.exe); kernel_json = json_escape(state.app.kernel_version); printf("{\n"); printf(" \"application\": {\n"); printf(" \"pid\": %d,\n", state.app.pid); printf(" \"comm\": \"%s\"\n", exe_json ? exe_json : ""); printf(" },\n"); printf(" \"system_context\": {\n"); printf(" \"kernel_version\": \"%s\",\n", kernel_json ? kernel_json : ""); printf(" \"yama_ptrace_scope\": %d,\n", state.app.yama_ptrace_scope); printf(" \"kptr_restrict\": %d,\n", state.app.kptr_restrict); printf(" \"dmesg_restrict\": %d,\n", state.app.dmesg_restrict); printf(" \"modules_disabled\": %d,\n", state.app.modules_disabled); printf(" \"perf_event_paranoid\": %d,\n", state.app.perf_event_paranoid); printf(" \"unprivileged_bpf_disabled\": %d,\n", state.app.unprivileged_bpf_disabled); printf(" \"bpf_jit_enable\": %d,\n", state.app.bpf_jit_enable); printf(" \"bpf_jit_harden\": %d,\n", state.app.bpf_jit_harden); printf(" \"bpf_jit_kallsyms\": %d,\n", state.app.bpf_jit_kallsyms); printf(" \"vm_mmap_min_addr\": %d,\n", state.app.mmap_min_addr); printf(" \"fs_protected_hardlinks\": %d,\n", state.app.protected_hardlinks); printf(" \"fs_protected_symlinks\": %d,\n", state.app.protected_symlinks); printf(" \"fs_suid_dumpable\": %d\n", state.app.suid_dumpable); printf(" },\n"); free(exe_json); free(kernel_json); printf(" \"capability_drop_observed\": %s,\n", state.capset_observed ? "true" : "false"); printf(" \"successful_capset_calls\": %lu,\n", state.app.capset.successful_calls); printf(" \"required_capabilities\": [\n"); first_cap = 1; for (i = 0; i <= CAP_LAST_CAP; i++) { struct cap_check *check = &state.app.checks[i]; char *name_json; char *reason_json; if (cap_required_union(check)) { name_json = json_escape(capng_capability_to_name(i)); reason_json = cap_union_reason(check) ? json_escape(cap_union_reason(check)) : NULL; if (!first_cap) printf(",\n"); printf(" {\n"); printf(" \"number\": %d,\n", i); printf(" \"name\": \"%s\",\n", name_json ? name_json : ""); printf(" \"checks\": {\n"); printf(" \"total\": %lu,\n", cap_total_checks(check)); printf(" \"granted\": %lu,\n", cap_total_granted(check)); printf(" \"denied\": %lu\n", cap_total_denied(check)); printf(" }"); if (cap_union_reason(check)) printf(",\n \"reason\": \"%s\"\n", reason_json ? reason_json : ""); else printf("\n"); printf(" }"); first_cap = 0; free(name_json); free(reason_json); } } printf("\n ],\n"); print_json_capset_only(); if (state.capset_observed) { printf(",\n \"initialization_capabilities\": [\n"); first_cap = 1; for (i = 0; i <= CAP_LAST_CAP; i++) { struct cap_check *check = &state.app.checks[i]; char *name_json; char *reason_json; if (check->granted == 0) continue; name_json = json_escape(capng_capability_to_name(i)); reason_json = check->reason ? json_escape(check->reason) : NULL; if (!first_cap) printf(",\n"); printf(" {\n"); printf(" \"number\": %d,\n", i); printf(" \"name\": \"%s\",\n", name_json ? name_json : ""); printf(" \"checks\": {\n"); printf(" \"total\": %lu,\n", check->count); printf(" \"granted\": %lu,\n", check->granted); printf(" \"denied\": %lu\n", check->denied); printf(" }"); if (check->reason) printf(",\n \"reason\": \"%s\"\n", reason_json ? reason_json : ""); else printf("\n"); printf(" }"); first_cap = 0; free(name_json); free(reason_json); } printf("\n ],\n"); printf(" \"operational_capabilities\": [\n"); first_cap = 1; for (i = 0; i <= CAP_LAST_CAP; i++) { struct cap_check *check = &state.app.checks[i]; char *name_json; char *reason_json; if (check->op_granted == 0) continue; name_json = json_escape(capng_capability_to_name(i)); reason_json = check->op_reason ? json_escape(check->op_reason) : NULL; if (!first_cap) printf(",\n"); printf(" {\n"); printf(" \"number\": %d,\n", i); printf(" \"name\": \"%s\",\n", name_json ? name_json : ""); printf(" \"checks\": {\n"); printf(" \"total\": %lu,\n", check->op_count); printf(" \"granted\": %lu,\n", check->op_granted); printf(" \"denied\": %lu\n", check->op_denied); printf(" }"); if (check->op_reason) printf(",\n \"reason\": \"%s\"\n", reason_json ? reason_json : ""); else printf("\n"); printf(" }"); first_cap = 0; free(name_json); free(reason_json); } printf("\n ]"); } printf(",\n"); printf(" \"denied_capabilities\": [\n"); first_denied = 1; for (i = 0; i <= CAP_LAST_CAP; i++) { struct cap_check *check = &state.app.checks[i]; char *name_json; if (cap_total_denied(check) > 0 && cap_total_granted(check) == 0) { name_json = json_escape(capng_capability_to_name(i)); if (!first_denied) printf(",\n"); printf(" {\n"); printf(" \"number\": %d,\n", i); printf(" \"name\": \"%s\",\n", name_json ? name_json : ""); printf(" \"not_granted_checks\": %lu,\n", cap_total_denied(check)); print_json_denied_syscalls(check); print_json_outcomes(check); printf(" }"); first_denied = 0; free(name_json); } } printf("\n ]\n"); printf("}\n"); } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/cap_audit_outcome.c000066400000000000000000000132321524174023300247110ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* * cap-audit - Aggregate syscall outcomes for denied capability checks. * Copyright 2026 Red Hat Inc. * All Rights Reserved. */ #include "cap_audit.h" #include #include #include /* Internal restart results can be visible at the raw syscall tracepoint. */ #define KERNEL_ERESTARTSYS 512 #define KERNEL_ERESTARTNOINTR 513 #define KERNEL_ERESTARTNOHAND 514 #define KERNEL_ERESTART_RESTARTBLOCK 516 static int is_kernel_restart(int error) { return error == KERNEL_ERESTARTSYS || error == KERNEL_ERESTARTNOINTR || error == KERNEL_ERESTARTNOHAND || error == KERNEL_ERESTART_RESTARTBLOCK; } enum syscall_outcome_class classify_syscall_outcome(__s64 result) { int error; if (result >= 0) return SYSCALL_OUTCOME_SUCCESS; if (result < -4095) return SYSCALL_OUTCOME_OTHER; error = (int)-result; if (error == EPERM || error == EACCES) return SYSCALL_OUTCOME_PERMISSION; if (error == EINTR || is_kernel_restart(error)) return SYSCALL_OUTCOME_INTERRUPTED; return SYSCALL_OUTCOME_OTHER; } const char *syscall_outcome_class_name(enum syscall_outcome_class class) { switch (class) { case SYSCALL_OUTCOME_SUCCESS: return "success"; case SYSCALL_OUTCOME_PERMISSION: return "permission_failure"; case SYSCALL_OUTCOME_OTHER: return "other_failure"; case SYSCALL_OUTCOME_INTERRUPTED: return "interrupted"; } return "other_failure"; } int add_cap_syscall_outcome(struct cap_check *check, int syscall_nr, __s64 result) { struct syscall_outcome *tmp; size_t new_capacity; size_t i; /* Successful return values are equivalent for capability analysis. */ if (result >= 0) result = 0; for (i = 0; i < check->outcome_count; i++) { struct syscall_outcome *outcome = &check->outcomes[i]; if (outcome->syscall_nr == syscall_nr && outcome->result == result) { outcome->count++; return 0; } } if (check->outcome_count == check->outcome_capacity) { new_capacity = check->outcome_capacity ? check->outcome_capacity * 2 : 4; tmp = realloc(check->outcomes, new_capacity * sizeof(*check->outcomes)); if (!tmp) return -1; check->outcomes = tmp; check->outcome_capacity = new_capacity; } check->outcomes[check->outcome_count].syscall_nr = syscall_nr; check->outcomes[check->outcome_count].result = result; check->outcomes[check->outcome_count].count = 1; check->outcome_count++; return 0; } void summarize_cap_outcomes(const struct cap_check *check, struct cap_outcome_summary *summary) { size_t i; memset(summary, 0, sizeof(*summary)); for (i = 0; i < check->outcome_count; i++) { const struct syscall_outcome *outcome = &check->outcomes[i]; switch (classify_syscall_outcome(outcome->result)) { case SYSCALL_OUTCOME_SUCCESS: summary->succeeded += outcome->count; break; case SYSCALL_OUTCOME_PERMISSION: summary->permission_failed += outcome->count; break; case SYSCALL_OUTCOME_OTHER: summary->other_failed += outcome->count; break; case SYSCALL_OUTCOME_INTERRUPTED: summary->interrupted += outcome->count; break; } } } static int has_unmatched_denied_syscall(const struct cap_check *check) { size_t i; size_t j; for (i = 0; i < check->denied_syscall_count; i++) { int found = 0; for (j = 0; j < check->outcome_count; j++) { if (check->denied_syscalls[i] == check->outcomes[j].syscall_nr) { found = 1; break; } } if (!found) return 1; } return 0; } enum denial_assessment assess_cap_denials(const struct cap_check *check) { struct cap_outcome_summary summary; if (cap_total_denied(check) == 0) return DENIAL_ASSESSMENT_NONE; summarize_cap_outcomes(check, &summary); if (summary.permission_failed > 0) return DENIAL_ASSESSMENT_PERMISSION; if (summary.succeeded > 0 && summary.interrupted > 0) return DENIAL_ASSESSMENT_MIXED_INTERRUPTED; if (has_unmatched_denied_syscall(check)) return DENIAL_ASSESSMENT_INCONCLUSIVE; /* A success must not hide correlated failures for other reasons. */ if (summary.succeeded > 0 && summary.other_failed > 0) return DENIAL_ASSESSMENT_MIXED_OTHER; if (summary.succeeded > 0) return DENIAL_ASSESSMENT_SUCCEEDED; if (summary.interrupted > 0 || check->outcome_count == 0) return DENIAL_ASSESSMENT_INCONCLUSIVE; return DENIAL_ASSESSMENT_OTHER; } const char *denial_assessment_name(enum denial_assessment assessment) { switch (assessment) { case DENIAL_ASSESSMENT_PERMISSION: return "permission_failure"; case DENIAL_ASSESSMENT_MIXED_INTERRUPTED: return "mixed_success_interruption"; case DENIAL_ASSESSMENT_MIXED_OTHER: return "mixed_success_other_failure"; case DENIAL_ASSESSMENT_INCONCLUSIVE: return "inconclusive"; case DENIAL_ASSESSMENT_SUCCEEDED: return "succeeded_despite_denial"; case DENIAL_ASSESSMENT_OTHER: return "denial_not_established"; case DENIAL_ASSESSMENT_NONE: return "none"; } return "none"; } int syscall_result_errno(__s64 result) { int error; if (result >= 0 || result < -4095) return 0; error = (int)-result; if (is_kernel_restart(error)) return 0; return error; } const char *syscall_result_name(__s64 result) { int error; if (result >= 0 || result < -4095) return NULL; error = (int)-result; switch (error) { case EPERM: return "EPERM"; case EACCES: return "EACCES"; case ENOENT: return "ENOENT"; case EINTR: return "EINTR"; case EBADF: return "EBADF"; #ifdef EBADFD case EBADFD: return "EBADFD"; #endif case KERNEL_ERESTARTSYS: return "ERESTARTSYS"; case KERNEL_ERESTARTNOINTR: return "ERESTARTNOINTR"; case KERNEL_ERESTARTNOHAND: return "ERESTARTNOHAND"; case KERNEL_ERESTART_RESTARTBLOCK: return "ERESTART_RESTARTBLOCK"; default: return NULL; } } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/cap_audit_path.c000066400000000000000000000034741524174023300242010ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* * cap-audit - Target command path resolution. * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. */ #include "cap_audit_path.h" #include #include #include #include #include static char *make_path_candidate(const char *dir, size_t dir_len, const char *target, size_t target_len) { char candidate[PATH_MAX]; if (!dir_len) { /* Empty PATH components refer to the current directory. */ if (target_len + 3 > sizeof(candidate)) return NULL; candidate[0] = '.'; candidate[1] = '/'; memcpy(candidate + 2, target, target_len + 1); return strdup(candidate); } if (dir_len + target_len + 2 > sizeof(candidate)) return NULL; memcpy(candidate, dir, dir_len); candidate[dir_len] = '/'; memcpy(candidate + dir_len + 1, target, target_len + 1); return strdup(candidate); } static int is_executable_file(const char *path) { struct stat st; /* This preflight is advisory; execvp() makes the final decision. */ return stat(path, &st) == 0 && S_ISREG(st.st_mode) && access(path, X_OK) == 0; } char *resolve_target_command(const char *target) { const char *path; const char *dir; size_t target_len; if (!target || !target[0]) return NULL; if (strchr(target, '/')) return is_executable_file(target) ? strdup(target) : NULL; target_len = strlen(target); path = getenv("PATH"); if (!path || !path[0]) return NULL; for (dir = path; dir;) { char *candidate; const char *next = strchr(dir, ':'); size_t dir_len = next ? (size_t)(next - dir) : strlen(dir); candidate = make_path_candidate(dir, dir_len, target, target_len); if (candidate) { if (is_executable_file(candidate)) return candidate; free(candidate); } dir = next ? next + 1 : NULL; } return NULL; } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/cap_audit_path.h000066400000000000000000000004611524174023300241770ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (c) 2026 Red Hat Inc. #ifndef CAP_AUDIT_PATH_H #define CAP_AUDIT_PATH_H #include "gcc-attributes.h" char *resolve_target_command(const char *target) __attribute_malloc__ __attr_dealloc_free __attr_access ((__read_only__, 1)) __wur; #endif stevegrubb-libcap-ng-9ae4440/utils/cap-audit/cap_audit_service.c000066400000000000000000000534741524174023300247120ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* * cap-audit - systemd service simulation support. * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. */ #include "cap_audit.h" #include #include #include #include #include #include #include #include #include #include #include struct argv_builder { char **argv; size_t argc; size_t capacity; }; struct word_buf { char *data; size_t len; size_t capacity; int started; }; enum numeric_id_result { NUMERIC_ID_INVALID = -1, NUMERIC_ID_NAME, NUMERIC_ID_VALID, }; #define LEGACY_INVALID_ID 65535UL /* capng_change_id takes int IDs and reserves -1 to mean "no change". */ #define MAX_CHANGE_ID ((unsigned long)INT_MAX) /* Supplementary groups use gid_t directly, whose all-ones value is invalid. */ #define MAX_SUPPLEMENTARY_GID ((unsigned long)((gid_t)-2)) static char *trim(char *str) { char *end; while (isspace((unsigned char)*str)) str++; if (*str == '\0') return str; end = str + strlen(str) - 1; while (end > str && isspace((unsigned char)*end)) *end-- = '\0'; return str; } static void clear_cap_set(cap_set_t *set) { int cap; set->seen = true; for (cap = 0; cap <= CAP_LAST_CAP; cap++) set->caps[cap] = false; } static void fill_cap_set(cap_set_t *set) { int cap; set->seen = true; for (cap = 0; cap <= CAP_LAST_CAP; cap++) set->caps[cap] = true; } static int cap_set_any(const cap_set_t *set) { int cap; for (cap = 0; cap <= CAP_LAST_CAP; cap++) { if (set->caps[cap]) return 1; } return 0; } static void free_exec_argv(service_config_t *cfg) { size_t i; if (!cfg->exec_argv) return; for (i = 0; i < cfg->exec_argc; i++) free(cfg->exec_argv[i]); free(cfg->exec_argv); cfg->exec_argv = NULL; cfg->exec_argc = 0; } void free_config(service_config_t *cfg) { if (!cfg) return; free(cfg->user_raw); cfg->user_raw = NULL; free(cfg->sup_groups.gids); cfg->sup_groups.gids = NULL; cfg->sup_groups.count = 0; cfg->sup_groups.is_set = false; free(cfg->service_type); cfg->service_type = NULL; free(cfg->exec_start); cfg->exec_start = NULL; free_exec_argv(cfg); } static int replace_string(char **dst, const char *src) { char *copy = NULL; if (src) { copy = strdup(src); if (!copy) return -1; } free(*dst); *dst = copy; return 0; } static int id_in_range(unsigned long id, unsigned long max) { return id <= max && id != LEGACY_INVALID_ID; } static enum numeric_id_result parse_numeric_id(const char *value, unsigned long max, unsigned long *id) { char *end = NULL; unsigned long parsed; if (!value || value[0] == '\0' || value[0] == '+' || value[0] == '-') return NUMERIC_ID_INVALID; if (!isdigit((unsigned char)value[0])) return NUMERIC_ID_NAME; errno = 0; parsed = strtoul(value, &end, 10); if (!end || end == value || *end != '\0') return NUMERIC_ID_NAME; if (errno || !id_in_range(parsed, max)) return NUMERIC_ID_INVALID; *id = parsed; return NUMERIC_ID_VALID; } static int resolve_group_token(const char *value, gid_t *gid, unsigned long max) { struct group *grp; unsigned long parsed; enum numeric_id_result result; result = parse_numeric_id(value, max, &parsed); if (result == NUMERIC_ID_VALID) { *gid = (gid_t)parsed; return 0; } if (result == NUMERIC_ID_INVALID) return -1; grp = getgrnam(value); if (!grp || !id_in_range((unsigned long)grp->gr_gid, max)) return -1; *gid = grp->gr_gid; return 0; } static int resolve_user_raw(service_config_t *cfg) { struct passwd *pw; unsigned long parsed; enum numeric_id_result result; if (!cfg->user_is_set) return 0; result = parse_numeric_id(cfg->user_raw, MAX_CHANGE_ID, &parsed); if (result == NUMERIC_ID_INVALID) return -EINVAL; if (result == NUMERIC_ID_VALID) { cfg->user_uid = (uid_t)parsed; pw = getpwuid(cfg->user_uid); if (pw) { if (!id_in_range((unsigned long)pw->pw_gid, MAX_CHANGE_ID)) return -ERANGE; cfg->user_primary_gid = pw->pw_gid; } else { cfg->user_primary_gid = (gid_t)parsed; } cfg->user_resolved = true; return 0; } pw = getpwnam(cfg->user_raw); if (!pw) return -ENOENT; if (!id_in_range((unsigned long)pw->pw_uid, MAX_CHANGE_ID) || !id_in_range((unsigned long)pw->pw_gid, MAX_CHANGE_ID)) return -ERANGE; cfg->user_uid = pw->pw_uid; cfg->user_primary_gid = pw->pw_gid; cfg->user_resolved = true; return 0; } static int fallback_to_nobody(service_config_t *cfg) { struct passwd *pw; pw = getpwnam("nobody"); if (pw && pw->pw_uid != 0 && pw->pw_gid != 0 && id_in_range((unsigned long)pw->pw_uid, MAX_CHANGE_ID) && id_in_range((unsigned long)pw->pw_gid, MAX_CHANGE_ID)) { cfg->user_uid = pw->pw_uid; cfg->user_primary_gid = pw->pw_gid; } else { cfg->user_uid = 65534; cfg->user_primary_gid = 65534; } if (replace_string(&cfg->user_raw, "nobody") != 0) return -1; /* Downstream code treats this as the effective simulated User=. */ cfg->user_is_set = true; cfg->user_resolved = true; return 0; } static int finalize_user(service_config_t *cfg) { int rc; if (!cfg->user_is_set) { if (cfg->dynamic_user_set && cfg->dynamic_user) return fallback_to_nobody(cfg); return 0; } rc = resolve_user_raw(cfg); if (rc == 0) return 0; /* Only a missing dynamic name is eligible for the nobody fallback. */ if (rc == -ENOENT && cfg->dynamic_user_set && cfg->dynamic_user) return fallback_to_nobody(cfg); fprintf(stderr, "Error: invalid or unresolved User=%s\n", cfg->user_raw); return -1; } static int parse_bool_value(const char *value, bool *result) { if (!strcasecmp(value, "yes") || !strcasecmp(value, "true") || !strcasecmp(value, "on") || !strcmp(value, "1")) { *result = true; return 0; } if (!strcasecmp(value, "no") || !strcasecmp(value, "false") || !strcasecmp(value, "off") || !strcmp(value, "0")) { *result = false; return 0; } return -1; } static int word_append(struct word_buf *word, char ch) { char *tmp; size_t new_capacity; if (word->len + 2 <= word->capacity) { word->data[word->len++] = ch; word->data[word->len] = '\0'; word->started = 1; return 0; } new_capacity = word->capacity ? word->capacity * 2 : 32; while (word->len + 2 > new_capacity) new_capacity *= 2; tmp = realloc(word->data, new_capacity); if (!tmp) return -1; word->data = tmp; word->capacity = new_capacity; word->data[word->len++] = ch; word->data[word->len] = '\0'; word->started = 1; return 0; } static int argv_append(struct argv_builder *builder, char *word) { char **tmp; size_t new_capacity; if (builder->argc + 2 > builder->capacity) { new_capacity = builder->capacity ? builder->capacity * 2 : 8; while (builder->argc + 2 > new_capacity) new_capacity *= 2; tmp = realloc(builder->argv, new_capacity * sizeof(char *)); if (!tmp) return -1; builder->argv = tmp; builder->capacity = new_capacity; } builder->argv[builder->argc++] = word; builder->argv[builder->argc] = NULL; return 0; } static int flush_word(struct argv_builder *builder, struct word_buf *word) { char *copy; if (!word->started) return 0; copy = strdup(word->data ? word->data : ""); if (!copy) return -1; if (argv_append(builder, copy) != 0) { free(copy); return -1; } word->len = 0; word->started = 0; if (word->data) word->data[0] = '\0'; return 0; } static void free_builder(struct argv_builder *builder) { size_t i; for (i = 0; i < builder->argc; i++) free(builder->argv[i]); free(builder->argv); builder->argv = NULL; builder->argc = 0; builder->capacity = 0; } static int reject_command_substitution(const char *pos) { return pos[0] == '`' || (pos[0] == '$' && pos[1] == '('); } static int tokenize_exec_start(const char *value, char ***argv, size_t *argc) { struct argv_builder builder = { 0 }; struct word_buf word = { 0 }; int quote = 0; const char *pos; int rc = -1; for (pos = value; *pos; pos++) { if (quote == 0 && isspace((unsigned char)*pos)) { if (flush_word(&builder, &word) != 0) goto out; continue; } if (quote != '\'' && reject_command_substitution(pos)) { fprintf(stderr, "Error: ExecStart command substitution is unsupported\n"); goto out; } if (quote == 0) { if (*pos == '\'' || *pos == '"') { quote = *pos; word.started = 1; } else if (*pos == '\\') { if (pos[1]) pos++; if (word_append(&word, *pos) != 0) goto out; } else if (word_append(&word, *pos) != 0) { goto out; } } else if (quote == *pos) { quote = 0; } else if (quote == '"' && *pos == '\\') { if (pos[1]) pos++; if (word_append(&word, *pos) != 0) goto out; } else if (word_append(&word, *pos) != 0) { goto out; } } if (quote != 0) { fprintf(stderr, "Error: unterminated quote in ExecStart\n"); goto out; } if (flush_word(&builder, &word) != 0) goto out; if (builder.argc == 0) { fprintf(stderr, "Error: ExecStart has no executable\n"); goto out; } *argv = builder.argv; *argc = builder.argc; builder.argv = NULL; builder.argc = 0; rc = 0; out: free(word.data); free_builder(&builder); return rc; } static int strip_exec_prefixes(service_config_t *cfg, int *no_env_expand) { const char *arg0; char *copy; size_t offset = 0; int no_setuid = 0; if (!cfg->exec_argv || !cfg->exec_argv[0]) return 0; arg0 = cfg->exec_argv[0]; while (arg0[offset] == '-' || arg0[offset] == '@' || arg0[offset] == ':' || arg0[offset] == '+' || arg0[offset] == '!') { if (arg0[offset] == ':') *no_env_expand = 1; if (arg0[offset] == '+') { fprintf(stderr, "Error: ExecStart '+' prefix is unsupported\n"); return -1; } if (arg0[offset] == '!') { if (no_setuid) { fprintf(stderr, "Error: ExecStart '!!' prefix is unsupported\n"); return -1; } no_setuid = 1; } offset++; } if (offset == 0) return 0; if (arg0[offset] == '\0') { fprintf(stderr, "Error: ExecStart has no executable\n"); return -1; } copy = strdup(arg0 + offset); if (!copy) return -1; free(cfg->exec_argv[0]); cfg->exec_argv[0] = copy; cfg->exec_start_no_setuid = no_setuid; return 0; } static int is_exec_environment_word(const char *arg) { const unsigned char *pos = (const unsigned char *)arg; if (*pos++ != '$' || (!isalpha(*pos) && *pos != '_')) return 0; while (isalnum(*pos) || *pos == '_') pos++; return *pos == '\0'; } static void omit_exec_environment_words(service_config_t *cfg) { size_t src; size_t dst = 1; /* * The service environment is unavailable here. An unset standalone * $NAME expands to zero arguments under systemd, so do not pass the * unresolved placeholder literally to the audited application. */ for (src = 1; src < cfg->exec_argc; src++) { if (is_exec_environment_word(cfg->exec_argv[src])) { free(cfg->exec_argv[src]); continue; } cfg->exec_argv[dst++] = cfg->exec_argv[src]; } cfg->exec_argv[dst] = NULL; cfg->exec_argc = dst; } static int set_exec_start(service_config_t *cfg, const char *value) { char **argv = NULL; size_t argc = 0; int no_env_expand = 0; free_exec_argv(cfg); cfg->exec_start_no_setuid = false; if (replace_string(&cfg->exec_start, value) != 0) return -1; if (value[0] == '\0') return 0; if (tokenize_exec_start(value, &argv, &argc) != 0) return -1; cfg->exec_argv = argv; cfg->exec_argc = argc; if (strip_exec_prefixes(cfg, &no_env_expand) != 0) return -1; if (!no_env_expand) omit_exec_environment_words(cfg); return 0; } static int cap_from_name(const char *name) { if (!strncasecmp(name, "CAP_", 4)) name += 4; return capng_name_to_capability(name); } static int parse_cap_tokens(cap_set_t *set, char *value, int invert) { char *saveptr = NULL; char *token; for (token = strtok_r(value, " \t", &saveptr); token; token = strtok_r(NULL, " \t", &saveptr)) { int cap = cap_from_name(token); if (cap < 0 || cap > CAP_LAST_CAP) { fprintf(stderr, "Error: unsupported capability name: %s\n", token); return -1; } set->caps[cap] = invert ? false : true; } return 0; } static int parse_cap_list(cap_set_t *set, const char *value, int bounding) { char *copy; char *tokens; int invert = 0; int rc; copy = strdup(value); if (!copy) return -1; tokens = trim(copy); if (tokens[0] == '\0') { clear_cap_set(set); free(copy); return 0; } if (bounding && tokens[0] == '~') { invert = 1; tokens = trim(tokens + 1); if (!set->seen) fill_cap_set(set); else set->seen = true; } else { set->seen = true; } rc = parse_cap_tokens(set, tokens, invert); free(copy); return rc; } static int append_gid(gid_list_t *list, gid_t gid) { gid_t *tmp; tmp = realloc(list->gids, (list->count + 1) * sizeof(gid_t)); if (!tmp) return -1; list->gids = tmp; list->gids[list->count++] = gid; list->is_set = true; return 0; } static int parse_supplementary_groups(gid_list_t *list, const char *value) { char *copy; char *tokens; char *saveptr = NULL; char *token; copy = strdup(value); if (!copy) return -1; tokens = trim(copy); if (tokens[0] == '\0') { free(list->gids); list->gids = NULL; list->count = 0; list->is_set = true; free(copy); return 0; } for (token = strtok_r(tokens, " \t", &saveptr); token; token = strtok_r(NULL, " \t", &saveptr)) { gid_t gid; if (resolve_group_token(token, &gid, MAX_SUPPLEMENTARY_GID) != 0) { fprintf(stderr, "Error: unable to resolve SupplementaryGroups=%s\n", token); free(copy); return -1; } if (append_gid(list, gid) != 0) { free(copy); return -1; } } free(copy); return 0; } static int set_user(service_config_t *cfg, const char *value) { cfg->user_is_set = value[0] != '\0'; cfg->user_resolved = false; cfg->user_uid = 0; cfg->user_primary_gid = 0; if (!cfg->user_is_set) { free(cfg->user_raw); cfg->user_raw = NULL; return 0; } return replace_string(&cfg->user_raw, value); } static int set_group(service_config_t *cfg, const char *value) { if (value[0] == '\0') { cfg->group_is_set = false; cfg->group_gid = 0; return 0; } if (resolve_group_token(value, &cfg->group_gid, MAX_CHANGE_ID) != 0) { fprintf(stderr, "Error: unable to resolve Group=%s\n", value); return -1; } cfg->group_is_set = true; return 0; } static int set_bool_directive(bool *value, bool *is_set, const char *raw) { if (parse_bool_value(raw, value) != 0) return -1; *is_set = true; return 0; } static int handle_service_directive(service_config_t *cfg, const char *key, const char *value) { if (!strcmp(key, "User")) return set_user(cfg, value); if (!strcmp(key, "Group")) return set_group(cfg, value); if (!strcmp(key, "SupplementaryGroups")) return parse_supplementary_groups(&cfg->sup_groups, value); if (!strcmp(key, "AmbientCapabilities")) return parse_cap_list(&cfg->ambient, value, 0); if (!strcmp(key, "CapabilityBoundingSet") || !strcmp(key, "BoundingSet")) return parse_cap_list(&cfg->bounding, value, 1); if (!strcmp(key, "NoNewPrivileges")) { if (set_bool_directive(&cfg->no_new_privs, &cfg->no_new_privs_set, value) != 0) { fprintf(stderr, "Error: invalid NoNewPrivileges=%s\n", value); return -1; } return 0; } if (!strcmp(key, "DynamicUser")) { if (set_bool_directive(&cfg->dynamic_user, &cfg->dynamic_user_set, value) != 0) { fprintf(stderr, "Error: invalid DynamicUser=%s\n", value); return -1; } return 0; } if (!strcmp(key, "ExecStart")) return set_exec_start(cfg, value); if (!strcmp(key, "Type")) return replace_string(&cfg->service_type, value); return 0; } int parse_service_file(const char *path, service_config_t *cfg) { FILE *file; char *line = NULL; size_t line_len = 0; ssize_t read_len; int in_service = 0; int rc = -1; if (!path || !cfg) return -1; memset(cfg, 0, sizeof(*cfg)); file = fopen(path, "r"); if (!file) { fprintf(stderr, "Error: failed to open service file %s: %s\n", path, strerror(errno)); return -1; } while ((read_len = getline(&line, &line_len, file)) != -1) { char *cur; char *eq; if (read_len > 0 && line[read_len - 1] == '\n') line[read_len - 1] = '\0'; cur = trim(line); if (cur[0] == '\0' || cur[0] == '#' || cur[0] == ';') continue; if (cur[0] == '[') { char *end = strchr(cur, ']'); if (!end) { in_service = 0; continue; } *end = '\0'; in_service = strcmp(cur + 1, "Service") == 0; continue; } if (!in_service) continue; eq = strchr(cur, '='); if (!eq) continue; *eq = '\0'; if (handle_service_directive(cfg, trim(cur), trim(eq + 1)) != 0) goto out; } if (ferror(file)) { fprintf(stderr, "Error: failed reading service file %s\n", path); goto out; } if (finalize_user(cfg) != 0) goto out; rc = 0; out: free(line); fclose(file); if (rc != 0) free_config(cfg); return rc; } static void print_cap_set_line(const char *label, const cap_set_t *set) { int cap; int first = 1; printf(" %s", label); if (!set->seen) { printf("not configured\n"); return; } for (cap = 0; cap <= CAP_LAST_CAP; cap++) { if (!set->caps[cap]) continue; if (!first) printf(" "); printf("%s", cap_name_safe(cap)); first = 0; } if (first) printf("(empty)"); printf("\n"); } static const char *gid_name(gid_t gid) { struct group *grp = getgrgid(gid); if (grp) return grp->gr_name; return NULL; } void print_service_config(const service_config_t *cfg) { gid_t gid = 0; const char *group_name; char group_buf[32]; if (!cfg) return; if (cfg->group_is_set) gid = cfg->group_gid; else if (cfg->user_is_set) gid = cfg->user_primary_gid; group_name = gid_name(gid); if (!group_name) { snprintf(group_buf, sizeof(group_buf), "%u", (unsigned int)gid); group_name = group_buf; } printf("[*] Parsed systemd service configuration\n"); if (state.service_file) printf(" Unit file: %s\n", state.service_file); printf(" Type: %s\n", cfg->service_type ? cfg->service_type : "simple"); if (cfg->service_type && !strcmp(cfg->service_type, "forking")) printf("[*] Warning: Type=forking detected. The traced " "process may daemonize; capability events after fork " "may be missed. Consider using a foreground flag if " "available.\n"); printf(" ExecStart: %s\n", cfg->exec_start ? cfg->exec_start : "(not configured)"); printf(" User: %s (uid=%u)\n", cfg->user_is_set ? cfg->user_raw : "root", cfg->user_is_set ? (unsigned int)cfg->user_uid : 0); printf(" Group: %s (gid=%u)\n", group_name, (unsigned int)gid); printf(" NoNewPrivileges: %s\n", cfg->no_new_privs ? "yes" : "no"); print_cap_set_line("AmbientCapabilities: ", &cfg->ambient); print_cap_set_line("CapabilityBoundingSet: ", &cfg->bounding); } static int stage_service_caps(const service_config_t *cfg, int non_root) { capng_type_t type = CAPNG_INHERITABLE | CAPNG_AMBIENT; int cap; if (!non_root && !cfg->exec_start_no_setuid) return 0; if (non_root) { capng_clear(CAPNG_SELECT_CAPS | CAPNG_SELECT_AMBIENT); type |= CAPNG_EFFECTIVE | CAPNG_PERMITTED; } else { /* With ExecStart=!, systemd retains credentials and capabilities. */ capng_clear(CAPNG_SELECT_AMBIENT); } for (cap = 0; cap <= CAP_LAST_CAP; cap++) { if (!cfg->ambient.caps[cap]) continue; if (capng_update(CAPNG_ADD, type, cap) != 0) return -1; } return 0; } static int stage_bounding_set(const service_config_t *cfg) { int cap; if (!cfg->bounding.seen) return 0; capng_clear(CAPNG_SELECT_BOUNDS); for (cap = 0; cap <= CAP_LAST_CAP; cap++) { if (!cfg->bounding.caps[cap]) continue; if (capng_update(CAPNG_ADD, CAPNG_BOUNDING_SET, cap) != 0) return -1; } return 0; } int apply_service_config(const service_config_t *cfg) { capng_flags_t flags = CAPNG_NO_FLAG; int target_uid = -1; int target_gid = -1; int non_root = 0; int rc; if (!cfg) return 0; if ((cfg->user_is_set && !cfg->user_resolved) || (cfg->dynamic_user && !cfg->user_resolved)) { fprintf(stderr, "Error: service user is not resolved\n"); return -1; } if (!cfg->exec_start_no_setuid && cfg->user_is_set) { gid_t gid = cfg->group_is_set ? cfg->group_gid : cfg->user_primary_gid; if (!id_in_range((unsigned long)cfg->user_uid, MAX_CHANGE_ID) || !id_in_range((unsigned long)gid, MAX_CHANGE_ID)) { fprintf(stderr, "Error: service credentials are out of range\n"); return -1; } target_uid = (int)cfg->user_uid; target_gid = (int)gid; non_root = cfg->user_uid != 0; flags = (capng_flags_t)(flags | CAPNG_INIT_SUPP_GRP); } else if (!cfg->exec_start_no_setuid && cfg->group_is_set) { if (!id_in_range((unsigned long)cfg->group_gid, MAX_CHANGE_ID)) { fprintf(stderr, "Error: service group is out of range\n"); return -1; } target_gid = (int)cfg->group_gid; } if (capng_get_caps_process() != 0) { fprintf(stderr, "Error: failed to read child process capabilities\n"); return -1; } if (stage_bounding_set(cfg) != 0) { fprintf(stderr, "Error: failed to stage bounding set\n"); return -1; } if (cfg->bounding.seen) flags = (capng_flags_t)(flags | CAPNG_APPLY_BOUNDING); if (stage_service_caps(cfg, non_root) != 0) { fprintf(stderr, "Error: failed to stage ambient capabilities\n"); return -1; } if (!cfg->ambient.seen || !cap_set_any(&cfg->ambient)) flags = (capng_flags_t)(flags | CAPNG_CLEAR_AMBIENT); if (!cfg->exec_start_no_setuid && cfg->sup_groups.is_set && cfg->sup_groups.count > 0) { if (capng_stage_additional_groups(cfg->sup_groups.gids, cfg->sup_groups.count) != 0) { fprintf(stderr, "Error: failed to stage supplementary groups\n"); return -1; } flags = (capng_flags_t)(flags | CAPNG_APPLY_STAGED_GROUPS); } rc = capng_change_id(target_uid, target_gid, flags); if (rc != 0) { fprintf(stderr, "Error: failed to apply service credentials: %d\n", rc); return -1; } if (cfg->no_new_privs && prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) { fprintf(stderr, "Error: failed to set NoNewPrivileges: %s\n", strerror(errno)); return -1; } return 0; } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/cap_audit_util.c000066400000000000000000000167101524174023300242170ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* * cap-audit - Trace a target process to discover required capabilities. * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Authors: * Steve Grubb * Portions of this code were made with codex 5.2 */ /* Shared utility helpers: target inspection, syscall/cap name lookup, * output escaping, and common capability aggregation functions. */ #include "cap_audit.h" #include #include #include #include #include #include #include #include #include int resolve_target_exe(pid_t pid, char *exepath, size_t exepath_len) { char linkpath[64]; char selfpath[PATH_MAX]; ssize_t len; ssize_t self_len; int tries = 50; if (snprintf(linkpath, sizeof(linkpath), "/proc/%d/exe", pid) < 0) return -1; self_len = readlink("/proc/self/exe", selfpath, sizeof(selfpath) - 1); if (self_len >= 0) selfpath[self_len] = '\0'; while (tries--) { len = readlink(linkpath, exepath, exepath_len - 1); if (len < 0) { fprintf(stderr, "Warning: readlink(%s) failed: %s\n", linkpath, strerror(errno)); return -1; } exepath[len] = '\0'; if (self_len < 0 || strcmp(exepath, selfpath) != 0) break; if (tries == 0) fprintf(stderr, "Warning: %s still points to auditor binary (%s)\n", linkpath, exepath); usleep(10000); } return 0; } int inspect_target_file_caps(pid_t pid) { char exepath[PATH_MAX]; int fd; struct stat st; capng_results_t caps; state.app.file_caps = 0; state.app.file_setpcap = 0; if (resolve_target_exe(pid, exepath, sizeof(exepath)) < 0) return -1; fd = open(exepath, O_RDONLY | O_CLOEXEC); if (fd < 0) { fprintf(stderr, "Warning: open(%s) failed: %s\n", exepath, strerror(errno)); return -1; } if (fstat(fd, &st) < 0) { fprintf(stderr, "Warning: fstat(%s) failed: %s\n", exepath, strerror(errno)); close(fd); return -1; } if (!S_ISREG(st.st_mode)) { fprintf(stderr, "Warning: %s is not a regular file\n", exepath); close(fd); return -1; } capng_clear(CAPNG_SELECT_BOTH); if (capng_get_caps_fd(fd)) { if (errno != ENODATA) fprintf(stderr, "Warning: capng_get_caps_fd(%s) failed: %s\n", exepath, strerror(errno)); close(fd); if (capng_get_caps_process()) fprintf(stderr, "Warning: failed to restore process capabilities\n"); return -1; } close(fd); caps = capng_have_capabilities(CAPNG_SELECT_CAPS); if (caps == CAPNG_NONE) caps = capng_have_permitted_capabilities(); if (caps > CAPNG_NONE) state.app.file_caps = 1; if (capng_have_capability(CAPNG_PERMITTED, CAP_SETPCAP) || capng_have_capability(CAPNG_INHERITABLE, CAP_SETPCAP)) state.app.file_setpcap = 1; if (state.verbose) printf("[*] File caps source: %s (has_caps=%d setpcap=%d)\n", exepath, state.app.file_caps, state.app.file_setpcap); if (capng_get_caps_process()) fprintf(stderr, "Warning: failed to restore process capabilities\n"); return 0; } const char *cap_name_safe(int cap) { const char *name = capng_capability_to_name(cap); return name ? name : "unknown"; } void read_sysctl(const char *path, int *value) { FILE *f; f = fopen(path, "r"); if (f) { if (fscanf(f, "%d", value) != 1) *value = -1; fclose(f); } else { *value = -1; } } void read_system_state(struct app_caps *app) { FILE *f; read_sysctl("/proc/sys/kernel/yama/ptrace_scope", &app->yama_ptrace_scope); read_sysctl("/proc/sys/kernel/kptr_restrict", &app->kptr_restrict); read_sysctl("/proc/sys/kernel/dmesg_restrict", &app->dmesg_restrict); read_sysctl("/proc/sys/kernel/modules_disabled", &app->modules_disabled); read_sysctl("/proc/sys/kernel/perf_event_paranoid", &app->perf_event_paranoid); read_sysctl("/proc/sys/kernel/unprivileged_bpf_disabled", &app->unprivileged_bpf_disabled); read_sysctl("/proc/sys/net/core/bpf_jit_enable", &app->bpf_jit_enable); read_sysctl("/proc/sys/net/core/bpf_jit_harden", &app->bpf_jit_harden); read_sysctl("/proc/sys/net/core/bpf_jit_kallsyms", &app->bpf_jit_kallsyms); read_sysctl("/proc/sys/vm/mmap_min_addr", &app->mmap_min_addr); read_sysctl("/proc/sys/fs/protected_hardlinks", &app->protected_hardlinks); read_sysctl("/proc/sys/fs/protected_symlinks", &app->protected_symlinks); read_sysctl("/proc/sys/fs/suid_dumpable", &app->suid_dumpable); f = fopen("/proc/sys/kernel/osrelease", "r"); if (f) { if (!fgets(app->kernel_version, sizeof(app->kernel_version), f)) app->kernel_version[0] = 0; app->kernel_version[strcspn(app->kernel_version, "\n")] = 0; fclose(f); } } const char *syscall_name_from_nr(int nr) { if (audit_machine < 0) return NULL; return audit_syscall_to_name(nr, audit_machine); } char *json_escape(const char *input) { size_t i; size_t needed = 0; char *out; char *pos; if (!input) return strdup(""); for (i = 0; input[i]; i++) { unsigned char c = input[i]; switch (c) { case '\"': case '\\': case '\b': case '\f': case '\n': case '\r': case '\t': needed += 2; break; default: if (c < 0x20) needed += 6; else needed++; break; } } out = malloc(needed + 1); if (!out) return NULL; pos = out; for (i = 0; input[i]; i++) { unsigned char c = input[i]; switch (c) { case '\"': *pos++ = '\\'; *pos++ = '\"'; break; case '\\': *pos++ = '\\'; *pos++ = '\\'; break; case '\b': *pos++ = '\\'; *pos++ = 'b'; break; case '\f': *pos++ = '\\'; *pos++ = 'f'; break; case '\n': *pos++ = '\\'; *pos++ = 'n'; break; case '\r': *pos++ = '\\'; *pos++ = 'r'; break; case '\t': *pos++ = '\\'; *pos++ = 't'; break; default: if (c < 0x20) { snprintf(pos, 7, "\\u%04x", c); pos += 6; } else { *pos++ = c; } break; } } *pos = '\0'; return out; } void update_reason_to(char **target, int syscall_nr) { const char *syscall_name; if (*target) free(*target); if (syscall_nr < 0) { if (asprintf(target, "Used during capability check (syscall unknown)") < 0) *target = NULL; return; } syscall_name = syscall_name_from_nr(syscall_nr); if (asprintf(target, "Used by %s", syscall_name ? syscall_name : "unknown") < 0) *target = NULL; } void update_reason(struct cap_check *check, int syscall_nr) { update_reason_to(&check->reason, syscall_nr); } void update_reason_op(struct cap_check *check, int syscall_nr) { update_reason_to(&check->op_reason, syscall_nr); } int cap_required_union(const struct cap_check *check) { return check->granted > 0 || check->op_granted > 0; } unsigned long cap_total_checks(const struct cap_check *check) { return check->count + check->op_count; } unsigned long cap_total_granted(const struct cap_check *check) { return check->granted + check->op_granted; } unsigned long cap_total_denied(const struct cap_check *check) { return check->denied + check->op_denied; } const char *cap_union_reason(const struct cap_check *check) { if (check->reason) return check->reason; return check->op_reason; } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/cap_audit_yaml.c000066400000000000000000000161541524174023300242060ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* * cap-audit - Trace a target process to discover required capabilities. * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Authors: * Steve Grubb * Portions of this code were made with codex 5.2 */ /* YAML output serialization: alternate machine-readable output mirroring * the JSON structure for downstream consumers. */ #include "cap_audit.h" #include #include static void print_yaml_denied_syscalls(const struct cap_check *check) { size_t i; if (check->denied_syscall_count == 0) { printf(" denied_syscalls: []\n"); return; } printf(" denied_syscalls:\n"); for (i = 0; i < check->denied_syscall_count; i++) { const char *name; name = syscall_name_from_nr(check->denied_syscalls[i]); printf(" - number: %d\n", check->denied_syscalls[i]); printf(" name: %s\n", name ? name : "unknown"); } } static void print_yaml_outcomes(const struct cap_check *check) { size_t i; printf(" assessment: %s\n", denial_assessment_name(assess_cap_denials(check))); if (check->outcome_count == 0) { printf(" syscall_outcomes: []\n"); return; } printf(" syscall_outcomes:\n"); for (i = 0; i < check->outcome_count; i++) { const struct syscall_outcome *outcome = &check->outcomes[i]; enum syscall_outcome_class class; const char *result_name; const char *syscall_name; int error; class = classify_syscall_outcome(outcome->result); result_name = syscall_result_name(outcome->result); error = syscall_result_errno(outcome->result); syscall_name = syscall_name_from_nr(outcome->syscall_nr); printf(" - syscall_number: %d\n", outcome->syscall_nr); printf(" syscall: %s\n", syscall_name ? syscall_name : "unknown"); printf(" outcome_class: %s\n", syscall_outcome_class_name(class)); printf(" raw_return: %lld\n", (long long)outcome->result); printf(" return_name: %s\n", result_name ? result_name : "null"); if (error > 0) printf(" errno: %d\n", error); else printf(" errno: null\n"); printf(" count: %lu\n", outcome->count); } } static void print_yaml_capset_only(void) { int cap; int found = 0; for (cap = 0; cap <= CAP_LAST_CAP; cap++) { __u64 mask; if (!cap_is_capset_only(cap)) continue; if (!found) printf("capset_only_capabilities:\n"); printf(" - number: %d\n", cap); printf(" name: %s\n", cap_name_safe(cap)); printf(" requested_sets:\n"); mask = 1ULL << cap; if (state.app.capset.effective & mask) printf(" - effective\n"); if (state.app.capset.permitted & mask) printf(" - permitted\n"); if (state.app.capset.inheritable & mask) printf(" - inheritable\n"); found = 1; } if (!found) printf("capset_only_capabilities: []\n"); } void output_yaml(void) { int i; char *exe_yaml = json_escape(state.app.exe); char *kernel_yaml = json_escape(state.app.kernel_version); printf("application:\n"); printf(" pid: %d\n", state.app.pid); printf(" comm: \"%s\"\n", exe_yaml ? exe_yaml : ""); printf("system_context:\n"); printf(" kernel_version: \"%s\"\n", kernel_yaml ? kernel_yaml : ""); printf(" yama_ptrace_scope: %d\n", state.app.yama_ptrace_scope); printf(" kptr_restrict: %d\n", state.app.kptr_restrict); printf(" dmesg_restrict: %d\n", state.app.dmesg_restrict); printf(" modules_disabled: %d\n", state.app.modules_disabled); printf(" perf_event_paranoid: %d\n", state.app.perf_event_paranoid); printf(" unprivileged_bpf_disabled: %d\n", state.app.unprivileged_bpf_disabled); printf(" bpf_jit_enable: %d\n", state.app.bpf_jit_enable); printf(" bpf_jit_harden: %d\n", state.app.bpf_jit_harden); printf(" bpf_jit_kallsyms: %d\n", state.app.bpf_jit_kallsyms); printf(" vm_mmap_min_addr: %d\n", state.app.mmap_min_addr); printf(" fs_protected_hardlinks: %d\n", state.app.protected_hardlinks); printf(" fs_protected_symlinks: %d\n", state.app.protected_symlinks); printf(" fs_suid_dumpable: %d\n", state.app.suid_dumpable); free(exe_yaml); free(kernel_yaml); printf("capability_drop_observed: %s\n", state.capset_observed ? "true" : "false"); printf("successful_capset_calls: %lu\n", state.app.capset.successful_calls); printf("required_capabilities:\n"); for (i = 0; i <= CAP_LAST_CAP; i++) { struct cap_check *check = &state.app.checks[i]; if (cap_required_union(check)) { printf(" - number: %d\n", i); printf(" name: %s\n", cap_name_safe(i)); printf(" checks:\n"); printf(" total: %lu\n", cap_total_checks(check)); printf(" granted: %lu\n", cap_total_granted(check)); printf(" denied: %lu\n", cap_total_denied(check)); if (cap_union_reason(check)) { char *reason_yaml = json_escape(cap_union_reason(check)); printf(" reason: \"%s\"\n", reason_yaml ? reason_yaml : ""); free(reason_yaml); } } } print_yaml_capset_only(); if (state.capset_observed) { printf("initialization_capabilities:\n"); for (i = 0; i <= CAP_LAST_CAP; i++) { struct cap_check *check = &state.app.checks[i]; if (check->granted == 0) continue; printf(" - number: %d\n", i); printf(" name: %s\n", cap_name_safe(i)); printf(" checks:\n"); printf(" total: %lu\n", check->count); printf(" granted: %lu\n", check->granted); printf(" denied: %lu\n", check->denied); if (check->reason) { char *reason_yaml = json_escape(check->reason); printf(" reason: \"%s\"\n", reason_yaml ? reason_yaml : ""); free(reason_yaml); } } printf("operational_capabilities:\n"); for (i = 0; i <= CAP_LAST_CAP; i++) { struct cap_check *check = &state.app.checks[i]; if (check->op_granted == 0) continue; printf(" - number: %d\n", i); printf(" name: %s\n", cap_name_safe(i)); printf(" checks:\n"); printf(" total: %lu\n", check->op_count); printf(" granted: %lu\n", check->op_granted); printf(" denied: %lu\n", check->op_denied); if (check->op_reason) { char *reason_yaml = json_escape(check->op_reason); printf(" reason: \"%s\"\n", reason_yaml ? reason_yaml : ""); free(reason_yaml); } } } printf("denied_capabilities:\n"); for (i = 0; i <= CAP_LAST_CAP; i++) { struct cap_check *check = &state.app.checks[i]; if (cap_total_denied(check) > 0 && cap_total_granted(check) == 0) { printf(" - number: %d\n", i); printf(" name: %s\n", cap_name_safe(i)); printf(" not_granted_checks: %lu\n", cap_total_denied(check)); print_yaml_denied_syscalls(check); print_yaml_outcomes(check); } } } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/classify_app.c000066400000000000000000000025001524174023300236760ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (c) 2026 Red Hat Inc. /* * classify_app.c - File type detection helper for cap-audit * * This helper was split out of cap_audit_util.c so the short-read corner * cases can be exercised by make check. The important rule is that file * classification may only inspect bytes that were actually returned by * read(); tiny files must not be classified from uninitialized stack data. */ #include "config.h" #include #include #include #include #include #include #include "classify_app.h" #define ELFMAGIC "\177ELF" type_t classify_app(const char *exe) { int fd; ssize_t rc; char buf[257]; fd = open(exe, O_RDONLY); if (fd < 0) { fprintf(stderr, "Cannot open %s - %s\n", exe, strerror(errno)); exit(1); } rc = read(fd, buf, 256); close(fd); if (rc > 0) { buf[rc] = 0; /* * Only inspect signatures that fully fit in the bytes we read. * Short reads happen on tiny files and should not consult * uninitialized stack data past rc. */ if (rc >= 2 && buf[0] == '#' && buf[1] == '!') { char *ptr = strchr(buf, '\n'); if (ptr) *ptr = 0; if (strstr(buf, "python")) return PYTHON; } else if (rc >= 4 && strncmp(buf, ELFMAGIC, 4) == 0) return ELF; } return UNSUPPORTED; } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/classify_app.h000066400000000000000000000004751524174023300237140ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (c) 2026 Red Hat Inc. #ifndef CAP_AUDIT_CLASSIFY_APP_H #define CAP_AUDIT_CLASSIFY_APP_H #include "gcc-attributes.h" typedef enum { UNSUPPORTED, ELF, PYTHON } type_t; type_t classify_app(const char *exe) __attr_access ((__read_only__, 1)) __wur; #endif stevegrubb-libcap-ng-9ae4440/utils/cap-audit/test/000077500000000000000000000000001524174023300220375ustar00rootroot00000000000000stevegrubb-libcap-ng-9ae4440/utils/cap-audit/test/Makefile.am000066400000000000000000000035761524174023300241060ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0-or-later # Makefile.am -- cap-audit tests # # Copyright 2026 Red Hat Inc. # All Rights Reserved. AM_CPPFLAGS = -I${top_srcdir} -I${top_srcdir}/src \ -I${top_srcdir}/utils \ -I${top_srcdir}/utils/cap-audit \ -I${top_builddir}/utils/cap-audit ${LIBBPF_CFLAGS} check_PROGRAMS = cap_audit_util_test cap_audit_path_test TESTS = $(check_PROGRAMS) cap_audit_util_test_SOURCES = cap_audit_util_test.c \ ${top_srcdir}/utils/cap-audit/classify_app.c cap_audit_path_test_SOURCES = cap_audit_path_test.c \ ${top_srcdir}/utils/cap-audit/cap_audit_path.c if BUILD_CAP_AUDIT check_PROGRAMS += cap_audit_namespace_test cap_audit_outcome_test \ cap_audit_recommendation_test cap_audit_service_test cap_audit_namespace_test_SOURCES = cap_audit_namespace_test.c \ ${top_srcdir}/utils/cap-audit/cap_audit_event.c \ ${top_srcdir}/utils/cap-audit/cap_audit_analyze.c \ ${top_srcdir}/utils/cap-audit/cap_audit_capset.c \ ${top_srcdir}/utils/cap-audit/cap_audit_outcome.c cap_audit_outcome_test_SOURCES = cap_audit_outcome_test.c \ ${top_srcdir}/utils/cap-audit/cap_audit_event.c \ ${top_srcdir}/utils/cap-audit/cap_audit_analyze.c \ ${top_srcdir}/utils/cap-audit/cap_audit_capset.c \ ${top_srcdir}/utils/cap-audit/cap_audit_json.c \ ${top_srcdir}/utils/cap-audit/cap_audit_outcome.c \ ${top_srcdir}/utils/cap-audit/cap_audit_yaml.c cap_audit_outcome_test_LDADD = ${top_builddir}/src/libcap-ng.la cap_audit_recommendation_test_SOURCES = cap_audit_recommendation_test.c \ ${top_srcdir}/utils/cap-audit/cap_audit_analyze.c \ ${top_srcdir}/utils/cap-audit/cap_audit_capset.c \ ${top_srcdir}/utils/cap-audit/cap_audit_outcome.c cap_audit_service_test_SOURCES = cap_audit_service_test.c \ ${top_srcdir}/utils/cap-audit/cap_audit_service.c cap_audit_service_test_LDADD = ${top_builddir}/src/libcap-ng.la \ ${LIBBPF_LIBS} cap_audit_service_test_LDFLAGS = -Wl,--wrap=capng_change_id endif stevegrubb-libcap-ng-9ae4440/utils/cap-audit/test/cap_audit_namespace_test.c000066400000000000000000000101121524174023300272020ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* cap_audit_namespace_test.c -- capability recommendation scope tests * Copyright 2026 Red Hat Inc. * All Rights Reserved. */ #include "config.h" #include #include #include #include #include "cap_audit.h" struct audit_state state; int audit_machine; static void fail(const char *msg) { fprintf(stderr, "%s\n", msg); exit(EXIT_FAILURE); } const char *cap_name_safe(int cap) { return cap == CAP_SYS_ADMIN ? "sys_admin" : "unknown"; } const char *syscall_name_from_nr(int nr) { (void)nr; return "mount"; } void update_reason(struct cap_check *check, int syscall_nr) { (void)check; (void)syscall_nr; } void update_reason_op(struct cap_check *check, int syscall_nr) { (void)check; (void)syscall_nr; } int cap_required_union(const struct cap_check *check) { return check->needed || check->op_needed; } unsigned long cap_total_checks(const struct cap_check *check) { return check->count + check->op_count; } unsigned long cap_total_granted(const struct cap_check *check) { return check->granted + check->op_granted; } unsigned long cap_total_denied(const struct cap_check *check) { return check->denied + check->op_denied; } const char *cap_union_reason(const struct cap_check *check) { return check->op_reason ? check->op_reason : check->reason; } static char *capture_analysis(void) { FILE *capture; char *output; long len; int saved_stdout; capture = tmpfile(); if (!capture) fail("Failed to create output file"); saved_stdout = dup(STDOUT_FILENO); if (saved_stdout < 0) fail("Failed to save stdout"); if (fflush(stdout) || dup2(fileno(capture), STDOUT_FILENO) < 0) fail("Failed to capture stdout"); analyze_capabilities(); if (fflush(stdout) || dup2(saved_stdout, STDOUT_FILENO) < 0) fail("Failed to restore stdout"); close(saved_stdout); if (fseek(capture, 0, SEEK_END) != 0) fail("Failed to seek captured output"); len = ftell(capture); if (len < 0 || fseek(capture, 0, SEEK_SET) != 0) fail("Failed to measure captured output"); output = malloc((size_t)len + 1); if (!output) fail("Failed to allocate captured output"); if (fread(output, 1, (size_t)len, capture) != (size_t)len) fail("Failed to read captured output"); output[len] = '\0'; fclose(capture); return output; } static char *analyze_event(__u32 target_ns, int result) { service_config_t service = { .user_raw = "validation-user", .user_uid = 1000, .user_primary_gid = 1000, .user_is_set = true, .user_resolved = true, .exec_start = "/usr/bin/validation-target", }; struct cap_event event = { .pid = 1234, .capability = CAP_SYS_ADMIN, .result = result, .syscall_nr = 165, .targ_ns_inum = target_ns, }; memset(&state, 0, sizeof(state)); state.app.exe = "/usr/bin/validation-target"; state.app.pid = 1234; state.app.prog_type = ELF; strcpy(state.app.kernel_version, "validation"); state.service_file = "/tmp/validation.service"; state.service_cfg = &service; state.baseline_user_ns_inum = 100; if (handle_cap_event(NULL, &event, sizeof(event)) != 0) fail("Event handler rejected test event"); if ((target_ns != 0 && target_ns != state.baseline_user_ns_inum) != state.foreign_target_ns_observed) fail("Event handler misclassified target namespace"); return capture_analysis(); } static void expect_suppressed(char *output) { if (!strstr(output, "Warning:")) fail("Namespace warning was not emitted"); if (strstr(output, "AmbientCapabilities=") || strstr(output, "CapabilityBoundingSet=")) fail("Foreign namespace produced service capability directives"); if (!strstr(output, "sys_admin")) fail("Foreign namespace observation was hidden from diagnostics"); free(output); } int main(void) { char *output; output = analyze_event(100, 1); if (!strstr(output, "AmbientCapabilities=sys_admin") || !strstr(output, "CapabilityBoundingSet=sys_admin")) fail("Baseline namespace recommendation was suppressed"); free(output); expect_suppressed(analyze_event(200, 1)); expect_suppressed(analyze_event(200, 0)); puts("cap-audit namespace recommendation tests passed"); return 0; } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/test/cap_audit_outcome_test.c000066400000000000000000000341411524174023300267310ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* cap_audit_outcome_test.c -- syscall outcome correlation tests * Copyright 2026 Red Hat Inc. * All Rights Reserved. */ #include "config.h" #include #include #include #include #include #include "cap_audit.h" #define TEST_MOUNT_NR 1001 #define TEST_IOCTL_NR 1002 #define TEST_PROCESS_VM_NR 1003 #define TEST_OPENAT_NR 1004 #define TEST_BIND_NR 1005 #define TEST_LINK_NR 1006 #define TEST_KILL_NR 1007 #define TEST_ACCESS_NR 1008 #define TEST_ERESTARTSYS 512 struct audit_state state; int audit_machine; static void fail(const char *msg) { fprintf(stderr, "%s\n", msg); exit(EXIT_FAILURE); } const char *cap_name_safe(int cap) { switch (cap) { case CAP_CHOWN: return "chown"; case CAP_DAC_OVERRIDE: return "dac_override"; case CAP_DAC_READ_SEARCH: return "dac_read_search"; case CAP_FOWNER: return "fowner"; case CAP_KILL: return "kill"; case CAP_NET_BIND_SERVICE: return "net_bind_service"; case CAP_SYS_PTRACE: return "sys_ptrace"; case CAP_SYS_ADMIN: return "sys_admin"; case CAP_SYS_RESOURCE: return "sys_resource"; default: return "unknown"; } } const char *syscall_name_from_nr(int nr) { switch (nr) { case TEST_MOUNT_NR: return "mount"; case TEST_IOCTL_NR: return "ioctl"; case TEST_PROCESS_VM_NR: return "process_vm_readv"; case TEST_OPENAT_NR: return "openat"; case TEST_BIND_NR: return "bind"; case TEST_LINK_NR: return "link"; case TEST_KILL_NR: return "kill"; case TEST_ACCESS_NR: return "access"; default: return NULL; } } char *json_escape(const char *input) { return input ? strdup(input) : strdup(""); } void update_reason(struct cap_check *check, int syscall_nr) { const char *name = syscall_name_from_nr(syscall_nr); if (asprintf(&check->reason, "Used by %s", name ? name : "unknown") < 0) check->reason = NULL; } void update_reason_op(struct cap_check *check, int syscall_nr) { (void)check; (void)syscall_nr; } int cap_required_union(const struct cap_check *check) { return check->granted > 0 || check->op_granted > 0; } unsigned long cap_total_checks(const struct cap_check *check) { return check->count + check->op_count; } unsigned long cap_total_granted(const struct cap_check *check) { return check->granted + check->op_granted; } unsigned long cap_total_denied(const struct cap_check *check) { return check->denied + check->op_denied; } const char *cap_union_reason(const struct cap_check *check) { if (check->reason) return check->reason; return check->op_reason; } static void emit_check(int cap, int syscall_nr, int result) { struct cap_event event = { .pid = 1234, .tid = 1234, .capability = cap, .result = result, .syscall_nr = syscall_nr, .event_type = CAP_EVENT_CHECK, }; if (handle_cap_event(NULL, &event, sizeof(event)) != 0) fail("Capability event was rejected"); } static void emit_outcome(int cap, int syscall_nr, __s64 result) { struct cap_event event = { .pid = 1234, .tid = 1234, .syscall_nr = syscall_nr, .syscall_ret = result, .denied_caps = 1ULL << cap, .event_type = CAP_EVENT_SYSCALL_RESULT, }; if (handle_cap_event(NULL, &event, sizeof(event)) != 0) fail("Syscall outcome event was rejected"); } static void emit_capset(__s64 result, __u64 effective, __u64 permitted, __u64 inheritable) { struct cap_event event = { .pid = 1235, .tid = 1235, .syscall_ret = result, .capset_effective = effective, .capset_permitted = permitted, .capset_inheritable = inheritable, .event_type = CAP_EVENT_CAPSET, }; if (handle_cap_event(NULL, &event, sizeof(event)) != 0) fail("capset event was rejected"); } static void setup_events(void) { int i; memset(&state, 0, sizeof(state)); state.app.exe = "/usr/bin/outcome-target"; state.app.pid = 1234; state.app.prog_type = ELF; strcpy(state.app.kernel_version, "validation"); emit_check(CAP_NET_BIND_SERVICE, TEST_BIND_NR, 1); emit_check(CAP_SYS_ADMIN, TEST_MOUNT_NR, 0); emit_outcome(CAP_SYS_ADMIN, TEST_MOUNT_NR, -EPERM); emit_check(CAP_SYS_ADMIN, TEST_MOUNT_NR, 0); emit_outcome(CAP_SYS_ADMIN, TEST_MOUNT_NR, 0); emit_check(CAP_SYS_ADMIN, TEST_MOUNT_NR, 0); emit_outcome(CAP_SYS_ADMIN, TEST_MOUNT_NR, -ENOENT); emit_check(CAP_DAC_OVERRIDE, TEST_IOCTL_NR, 0); #ifdef EBADFD emit_outcome(CAP_DAC_OVERRIDE, TEST_IOCTL_NR, -EBADFD); #else emit_outcome(CAP_DAC_OVERRIDE, TEST_IOCTL_NR, -EBADF); #endif for (i = 0; i < 3; i++) { emit_check(CAP_FOWNER, TEST_LINK_NR, 0); emit_outcome(CAP_FOWNER, TEST_LINK_NR, 0); } for (i = 0; i < 2; i++) { emit_check(CAP_DAC_READ_SEARCH, TEST_ACCESS_NR, 0); emit_outcome(CAP_DAC_READ_SEARCH, TEST_ACCESS_NR, 0); } emit_check(CAP_DAC_READ_SEARCH, TEST_ACCESS_NR, 0); emit_outcome(CAP_DAC_READ_SEARCH, TEST_ACCESS_NR, -ENOENT); emit_check(CAP_KILL, TEST_KILL_NR, 0); emit_outcome(CAP_KILL, TEST_KILL_NR, -EINTR); emit_check(CAP_KILL, TEST_KILL_NR, 0); emit_outcome(CAP_KILL, TEST_KILL_NR, -TEST_ERESTARTSYS); for (i = 0; i < 20; i++) { emit_check(CAP_SYS_PTRACE, TEST_PROCESS_VM_NR, 0); emit_outcome(CAP_SYS_PTRACE, TEST_PROCESS_VM_NR, 0); } emit_check(CAP_SYS_PTRACE, TEST_PROCESS_VM_NR, 0); emit_outcome(CAP_SYS_PTRACE, TEST_PROCESS_VM_NR, -EINTR); /* A success correlated to another capability must not count here. */ emit_check(CAP_NET_BIND_SERVICE, TEST_OPENAT_NR, 0); emit_outcome(CAP_NET_BIND_SERVICE, TEST_OPENAT_NR, 0); emit_check(CAP_CHOWN, TEST_OPENAT_NR, 0); /* Failed capset payloads are diagnostic noise, not compatibility input. */ emit_capset(-EPERM, 1ULL << CAP_SYS_CHROOT, 1ULL << CAP_SYS_CHROOT, 0); emit_capset(0, (1ULL << CAP_NET_BIND_SERVICE) | (1ULL << CAP_SYS_RESOURCE), (1ULL << CAP_NET_BIND_SERVICE) | (1ULL << CAP_SYS_RESOURCE), 0); } typedef void (*output_fn)(void); static char *capture_output(output_fn output) { FILE *capture; char *text; long len; int saved_stdout; capture = tmpfile(); if (!capture) fail("Failed to create output file"); saved_stdout = dup(STDOUT_FILENO); if (saved_stdout < 0) fail("Failed to save stdout"); if (fflush(stdout) || dup2(fileno(capture), STDOUT_FILENO) < 0) fail("Failed to capture stdout"); output(); if (fflush(stdout) || dup2(saved_stdout, STDOUT_FILENO) < 0) fail("Failed to restore stdout"); close(saved_stdout); if (fseek(capture, 0, SEEK_END) != 0) fail("Failed to seek captured output"); len = ftell(capture); if (len < 0 || fseek(capture, 0, SEEK_SET) != 0) fail("Failed to measure captured output"); text = malloc((size_t)len + 1); if (!text) fail("Failed to allocate captured output"); if (fread(text, 1, (size_t)len, capture) != (size_t)len) fail("Failed to read captured output"); text[len] = '\0'; fclose(capture); return text; } static void expect_text(const char *output, const char *expected) { if (!strstr(output, expected)) fail(expected); } static size_t count_text(const char *output, const char *expected) { size_t count = 0; size_t len = strlen(expected); while ((output = strstr(output, expected)) != NULL) { count++; output += len; } return count; } static void test_human_output(void) { service_config_t service = { .user_raw = "validation-user", .user_uid = 1000, .user_primary_gid = 1000, .user_is_set = true, .user_resolved = true, .exec_start = "/usr/bin/outcome-target", .bounding = { .seen = true, }, }; char *output; output = capture_output(analyze_capabilities); expect_text(output, "mount: 1 failed with -EPERM"); expect_text(output, "mount: 1 failed with -ENOENT"); #ifdef EBADFD expect_text(output, "ioctl: 1 failed with -EBADFD"); #else expect_text(output, "ioctl: 1 failed with -EBADF"); #endif expect_text(output, "process_vm_readv: 20 succeeded"); expect_text(output, "process_vm_readv: 1 interrupted with -EINTR"); expect_text(output, "link: 3 succeeded"); expect_text(output, "access: 2 succeeded"); expect_text(output, "access: 1 failed with -ENOENT"); expect_text(output, "kill: 1 interrupted with -EINTR"); expect_text(output, "kill: 1 interrupted with -ERESTARTSYS"); expect_text(output, "CAPSET-ONLY CAPABILITIES:"); expect_text(output, "sys_resource (#24)"); expect_text(output, "Requested by successful capset in: effective, permitted"); expect_text(output, "compatibility constraint"); expect_text(output, "confirmed"); expect_text(output, "functional use"); expect_text(output, "Successful capset calls: 1"); expect_text(output, "Capset-only capabilities: 1"); expect_text(output, "CAPABILITIES WITH ONLY NOT-GRANTED CHECKS:"); expect_text(output, "Capability checks returning not granted: 1"); expect_text(output, "Outcomes of syscall invocations containing such a check:"); expect_text(output, "Associated syscall invocations:"); expect_text(output, "Failure categories:"); expect_text(output, "sys_admin: Manual investigation required"); expect_text(output, "sys_ptrace: Manual investigation required"); expect_text(output, "chown: Additional evidence required"); expect_text(output, "openat: outcome unavailable"); expect_text(output, "kill: Additional evidence required"); expect_text(output, "fowner: Omitted; associated syscalls succeeded"); expect_text(output, "dac_read_search: Omitted; successes and non-permission " "failures observed"); expect_text(output, "Associated syscall invocations: 2 succeeded and 1 failed"); expect_text(output, "non-permission reasons"); expect_text(output, "not-granted capability checks are not established"); expect_text(output, "cause of those failures"); expect_text(output, "Manual investigation required: 2"); expect_text(output, "Additional evidence required: 2"); expect_text(output, "Omitted after associated syscall success: 1"); expect_text(output, "Omitted after mixed success/non-permission failure: 1"); expect_text(output, "Capability check not established as cause: 1"); expect_text(output, "CapabilityBoundingSet=net_bind_service sys_resource"); free(output); service.bounding.caps[CAP_NET_BIND_SERVICE] = true; state.service_file = "/tmp/outcome.service"; state.service_cfg = &service; output = capture_output(analyze_capabilities); expect_text(output, "sys_admin: Manual investigation required"); expect_text(output, "Capability is absent from the configured"); expect_text(output, "Mixed capability-check results: net_bind_service"); expect_text(output, "Capset-only compatibility capabilities: sys_resource"); expect_text(output, "sys_resource is absent from the configured"); expect_text(output, "Detailed evidence appears in CAPABILITIES WITH ONLY"); if (count_text(output, "mount: 1 failed with -EPERM") != 1 || count_text(output, "mount: 1 failed with -ENOENT") != 1) fail("Service recommendations duplicated syscall outcomes"); expect_text(output, "CapabilityBoundingSet=net_bind_service sys_resource"); free(output); state.service_cfg = NULL; } static void test_structured_output(void) { char *output; output = capture_output(output_json); expect_text(output, "\"assessment\": \"permission_failure\""); expect_text(output, "\"assessment\": \"mixed_success_interruption\""); expect_text(output, "\"assessment\": \"mixed_success_other_failure\""); expect_text(output, "\"assessment\": \"denial_not_established\""); expect_text(output, "\"assessment\": \"inconclusive\""); expect_text(output, "\"assessment\": \"succeeded_despite_denial\""); expect_text(output, "\"return_name\": \"EPERM\""); expect_text(output, "\"return_name\": \"ENOENT\""); expect_text(output, "\"not_granted_checks\": 1"); expect_text(output, "\"successful_capset_calls\": 1"); expect_text(output, "\"capset_only_capabilities\": ["); expect_text(output, "\"name\": \"sys_resource\""); expect_text(output, "\"requested_sets\": [\"effective\", \"permitted\"]"); expect_text(output, "\"denied_syscalls\": ["); expect_text(output, "\"return_name\": \"ERESTARTSYS\",\n" " \"errno\": null"); #ifdef EBADFD expect_text(output, "\"return_name\": \"EBADFD\""); #endif expect_text(output, "\"count\": 20"); free(output); output = capture_output(output_yaml); expect_text(output, "assessment: permission_failure"); expect_text(output, "assessment: mixed_success_interruption"); expect_text(output, "assessment: mixed_success_other_failure"); expect_text(output, "assessment: denial_not_established"); expect_text(output, "assessment: inconclusive"); expect_text(output, "assessment: succeeded_despite_denial"); expect_text(output, "return_name: EPERM"); expect_text(output, "return_name: ENOENT"); expect_text(output, "not_granted_checks: 1"); expect_text(output, "successful_capset_calls: 1"); expect_text(output, "capset_only_capabilities:"); expect_text(output, "name: sys_resource"); expect_text(output, "denied_syscalls:"); expect_text(output, "return_name: ERESTARTSYS\n" " errno: null"); expect_text(output, "count: 20"); free(output); } int main(void) { if (classify_syscall_outcome(0) != SYSCALL_OUTCOME_SUCCESS || classify_syscall_outcome(-EPERM) != SYSCALL_OUTCOME_PERMISSION || classify_syscall_outcome(-EACCES) != SYSCALL_OUTCOME_PERMISSION || classify_syscall_outcome(-EINTR) != SYSCALL_OUTCOME_INTERRUPTED || classify_syscall_outcome(-EBADF) != SYSCALL_OUTCOME_OTHER || classify_syscall_outcome(-TEST_ERESTARTSYS) != SYSCALL_OUTCOME_INTERRUPTED || syscall_result_errno(-EPERM) != EPERM || syscall_result_errno(-TEST_ERESTARTSYS) != 0 || !syscall_result_name(-ENOENT) || strcmp(syscall_result_name(-ENOENT), "ENOENT") != 0 || !syscall_result_name(-TEST_ERESTARTSYS) || strcmp(syscall_result_name(-TEST_ERESTARTSYS), "ERESTARTSYS") != 0) fail("Raw syscall outcomes were misclassified"); setup_events(); if (state.app.checks[CAP_CHOWN].outcome_count != 0 || state.app.checks[CAP_NET_BIND_SERVICE].outcome_count != 1) fail("Syscall outcome was correlated to the wrong capability"); if (state.app.capset.successful_calls != 1 || !cap_is_capset_only(CAP_SYS_RESOURCE) || cap_is_capset_only(CAP_NET_BIND_SERVICE) || cap_requested_by_capset(CAP_SYS_CHROOT)) fail("capset payloads were not filtered or classified correctly"); test_human_output(); test_structured_output(); puts("cap-audit syscall outcome tests passed"); return 0; } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/test/cap_audit_path_test.c000066400000000000000000000052631524174023300262150ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* cap_audit_path_test.c -- target PATH resolution regression tests * Copyright 2026 Red Hat Inc. * All Rights Reserved. */ #include #include #include #include #include #include #include #include "cap_audit_path.h" static void fail(const char *msg) { fprintf(stderr, "%s\n", msg); exit(EXIT_FAILURE); } static void make_executable(const char *path) { int fd; const char script[] = "#!/bin/sh\nexit 0\n"; ssize_t rc; fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0700); if (fd < 0) fail("Failed to create executable target"); rc = write(fd, script, sizeof(script) - 1); close(fd); if (rc != (ssize_t)(sizeof(script) - 1)) fail("Failed to write executable target"); } int main(void) { char root[] = "/tmp/libcap-ng-cap-audit-path-XXXXXX"; char early[PATH_MAX]; char late[PATH_MAX]; char early_target[PATH_MAX]; char late_target[PATH_MAX]; char link_target[PATH_MAX]; char path[PATH_MAX * 2 + 2]; char *resolved; if (!mkdtemp(root)) fail("Failed to create temporary directory"); if (snprintf(early, sizeof(early), "%s/early", root) < 0 || snprintf(late, sizeof(late), "%s/late", root) < 0 || snprintf(early_target, sizeof(early_target), "%s/probe", early) < 0 || snprintf(late_target, sizeof(late_target), "%s/probe", late) < 0 || snprintf(link_target, sizeof(link_target), "%s/link", root) < 0) fail("Failed to format target paths"); if (mkdir(early, 0700) || mkdir(late, 0700) || mkdir(early_target, 0700)) fail("Failed to create PATH directories"); make_executable(late_target); if (snprintf(path, sizeof(path), "%s:%s", early, late) < 0 || setenv("PATH", path, 1)) fail("Failed to set test PATH"); resolved = resolve_target_command("probe"); if (!resolved || strcmp(resolved, late_target)) fail("Resolver did not skip a directory PATH candidate"); free(resolved); resolved = resolve_target_command(early_target); if (resolved) fail("Resolver accepted a directory target"); resolved = resolve_target_command(late_target); if (!resolved || strcmp(resolved, late_target)) fail("Resolver rejected a regular executable"); free(resolved); if (symlink(late_target, link_target)) fail("Failed to create executable symlink"); resolved = resolve_target_command(link_target); if (!resolved || strcmp(resolved, link_target)) fail("Resolver rejected a symlink to an executable"); free(resolved); if (unlink(link_target) || unlink(late_target) || rmdir(early_target) || rmdir(early) || rmdir(late) || rmdir(root)) fail("Failed to remove temporary paths"); puts("cap-audit target PATH tests passed"); return 0; } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/test/cap_audit_recommendation_test.c000066400000000000000000000262351524174023300302670ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* cap_audit_recommendation_test.c -- service recommendation tests * Copyright 2026 Red Hat Inc. * All Rights Reserved. */ #include "config.h" #include #include #include #include #include "cap_audit.h" struct audit_state state; int audit_machine; static void fail(const char *msg) { fprintf(stderr, "%s\n", msg); exit(EXIT_FAILURE); } const char *cap_name_safe(int cap) { switch (cap) { case CAP_CHOWN: return "chown"; case CAP_DAC_OVERRIDE: return "dac_override"; case CAP_FOWNER: return "fowner"; case CAP_NET_BIND_SERVICE: return "net_bind_service"; case CAP_SYS_CHROOT: return "sys_chroot"; case CAP_SYS_PTRACE: return "sys_ptrace"; case CAP_SYS_ADMIN: return "sys_admin"; case CAP_SYS_RESOURCE: return "sys_resource"; default: return "unknown"; } } const char *syscall_name_from_nr(int nr) { (void)nr; return "openat"; } int cap_required_union(const struct cap_check *check) { return check->granted > 0 || check->op_granted > 0; } unsigned long cap_total_checks(const struct cap_check *check) { return check->count + check->op_count; } unsigned long cap_total_granted(const struct cap_check *check) { return check->granted + check->op_granted; } unsigned long cap_total_denied(const struct cap_check *check) { return check->denied + check->op_denied; } const char *cap_union_reason(const struct cap_check *check) { if (check->reason) return check->reason; return check->op_reason; } static char *capture_analysis(void) { FILE *capture; char *output; long len; int saved_stdout; capture = tmpfile(); if (!capture) fail("Failed to create output file"); saved_stdout = dup(STDOUT_FILENO); if (saved_stdout < 0) fail("Failed to save stdout"); if (fflush(stdout) || dup2(fileno(capture), STDOUT_FILENO) < 0) fail("Failed to capture stdout"); analyze_capabilities(); if (fflush(stdout) || dup2(saved_stdout, STDOUT_FILENO) < 0) fail("Failed to restore stdout"); close(saved_stdout); if (fseek(capture, 0, SEEK_END) != 0) fail("Failed to seek captured output"); len = ftell(capture); if (len < 0 || fseek(capture, 0, SEEK_SET) != 0) fail("Failed to measure captured output"); output = malloc((size_t)len + 1); if (!output) fail("Failed to allocate captured output"); if (fread(output, 1, (size_t)len, capture) != (size_t)len) fail("Failed to read captured output"); output[len] = '\0'; fclose(capture); return output; } static void expect_line(const char *output, const char *line) { const char *found = strstr(output, line); size_t len = strlen(line); if (!found || found[len] != '\n') fail(line); } static int contains_tokens(const char *output, const char *const *tokens, size_t count) { size_t i; for (i = 0; i < count; i++) { output = strstr(output, tokens[i]); if (!output) return 0; output += strlen(tokens[i]); } return 1; } static size_t count_text(const char *output, const char *expected) { size_t count = 0; size_t len = strlen(expected); while ((output = strstr(output, expected)) != NULL) { count++; output += len; } return count; } static void setup_analysis(service_config_t *service) { static int denied_syscall = 257; struct cap_check *check; memset(&state, 0, sizeof(state)); memset(service, 0, sizeof(*service)); state.app.exe = "/usr/bin/recommendation-target"; state.app.pid = 1234; state.app.prog_type = ELF; strcpy(state.app.kernel_version, "validation"); state.service_file = "/tmp/recommendation.service"; state.service_cfg = service; state.capset_observed = 1; state.app.capset.effective = 1ULL << CAP_SYS_RESOURCE; state.app.capset.permitted = 1ULL << CAP_SYS_RESOURCE; state.app.capset.successful_calls = 1; service->user_raw = "validation-user"; service->user_uid = 1000; service->user_primary_gid = 1000; service->user_is_set = true; service->user_resolved = true; service->exec_start = "/usr/bin/recommendation-target"; service->ambient.seen = true; service->ambient.caps[CAP_SYS_ADMIN] = true; service->bounding.seen = true; service->bounding.caps[CAP_SYS_PTRACE] = true; service->bounding.caps[CAP_SYS_ADMIN] = true; service->bounding.caps[CAP_SYS_RESOURCE] = true; check = &state.app.checks[CAP_CHOWN]; check->count = 1; check->denied = 1; check->denied_syscalls = &denied_syscall; check->denied_syscall_count = 1; check = &state.app.checks[CAP_DAC_OVERRIDE]; check->count = 1; check->granted = 1; check = &state.app.checks[CAP_FOWNER]; check->count = 2; check->granted = 1; check->denied = 1; check = &state.app.checks[CAP_NET_BIND_SERVICE]; check->count = 1; check->granted = 1; check = &state.app.checks[CAP_SYS_CHROOT]; check->op_count = 1; check->op_granted = 1; } static void setup_empty_analysis(service_config_t *service) { memset(&state, 0, sizeof(state)); memset(service, 0, sizeof(*service)); state.app.exe = "/usr/bin/empty-target"; state.app.pid = 5678; strcpy(state.app.kernel_version, "validation"); state.app.yama_ptrace_scope = 1; state.app.protected_symlinks = 1; state.app.suid_dumpable = 2; state.service_file = "/tmp/empty.service"; state.service_cfg = service; service->exec_start = "/usr/bin/empty-target"; service->bounding.seen = true; state.app.checks[CAP_DAC_OVERRIDE].count = 1; state.app.checks[CAP_DAC_OVERRIDE].denied = 1; state.app.checks[CAP_SYS_PTRACE].count = 1; state.app.checks[CAP_SYS_PTRACE].denied = 1; } int main(void) { static const char *const denied_guidance[] = { "Detailed evidence appears in", "CAPABILITIES WITH ONLY NOT-GRANTED CHECKS", "not-granted check", "does not add", "capability", "automatically", }; static const char *const capset_guidance[] = { "CAPSET-ONLY CAPABILITIES", "sys_resource", "application's", "capset", "setup", "before", "removing", "deployment", "boundary", }; service_config_t service; char *output; setup_analysis(&service); output = capture_analysis(); expect_line(output, " AmbientCapabilities: sys_admin"); if (!contains_tokens(output, (const char *const[]) { "Recommended current-service-compatible [Service]", "AmbientCapabilities=dac_override fowner " "net_bind_service sys_chroot sys_admin sys_resource", "CapabilityBoundingSet=dac_override fowner " "net_bind_service sys_chroot sys_ptrace sys_admin " "sys_resource", }, 3)) fail("Compatible configuration did not retain unit capabilities"); if (strstr(output, "observed-workload-minimized") || count_text(output, " Recommended ") != 1) fail("A second configuration removed unobserved capabilities"); expect_line(output, " Initialization capabilities: dac_override fowner " "net_bind_service"); expect_line(output, " Operational capabilities: sys_chroot"); if (!strstr(output, "chown: Additional evidence required") || !strstr(output, "Capability is absent from the configured")) fail("Denied-only capability was hidden from diagnostics"); if (!contains_tokens(output, denied_guidance, sizeof(denied_guidance) / sizeof(denied_guidance[0]))) fail("Denied-only capability lacked investigation guidance"); if (count_text(output, " Configured but not observed in this run:") != 1) fail("Unused bounding capability was not reported as removable"); expect_line(output, " AmbientCapabilities and CapabilityBoundingSet: " "sys_admin"); expect_line(output, " CapabilityBoundingSet only: sys_ptrace"); if (!contains_tokens(output, (const char *const[]) { "Relevant functionality may not have been", "Retained in the", "current-service-compatible configuration", "manual review", "targeted testing", }, 5)) fail("Unobserved configured capability lacked safe guidance"); if (!strstr(output, "CAPSET-ONLY CAPABILITIES:") || !strstr(output, "sys_resource (#24)") || !strstr(output, "Capset-only compatibility capabilities: ") || !strstr(output, "current-binary-compatible configuration retains these")) fail("Capset-only capability lacked compatibility diagnostics"); if (!strstr(output, " Current service context:\n") || !strstr(output, " AmbientCapabilities: sys_admin\n") || !strstr(output, " CapabilityBoundingSet: sys_ptrace sys_admin " "sys_resource\n")) fail("Current service context was not labeled"); if (!contains_tokens(output, capset_guidance, sizeof(capset_guidance) / sizeof(capset_guidance[0]))) fail("Capset-only capability lacked coordinated removal guidance"); if (strstr(output, "A listed capability is retained separately because a")) fail("Unrelated capset caveat was shown for denied capabilities"); expect_line(output, " NoNewPrivileges=no"); if (!contains_tokens(output, (const char *const[]) { "The service was audited", "Enabling it was not validated", "configuration retains", "NoNewPrivileges=no", "functionality that starts child", "processes or helpers", }, 6)) fail("Disabled NoNewPrivileges lacked compatibility guidance"); free(output); service.no_new_privs = true; output = capture_analysis(); expect_line(output, " NoNewPrivileges=yes"); if (strstr(output, "The service was audited with this setting disabled")) fail("Enabled NoNewPrivileges received disabled-setting guidance"); service.no_new_privs = false; free(output); state.app.checks[CAP_SYS_RESOURCE].count = 1; state.app.checks[CAP_SYS_RESOURCE].denied = 1; output = capture_analysis(); if (!strstr(output, "A listed capability is retained separately because a")) fail("Capset caveat was omitted for an overlapping capability"); free(output); setup_empty_analysis(&service); output = capture_analysis(); if (!strstr(output, "None - No confirmed granted capability use was observed.")) fail("No-granted-use summary was not evidence based"); expect_line(output, " CapabilityBoundingSet: (none)"); expect_line(output, " AmbientCapabilities: not configured (none)"); expect_line(output, " CapabilityBoundingSet="); if (strstr(output, " CapabilityBoundingSet=(none)")) fail("Empty recommendation used invalid systemd syntax"); if (!strstr(output, "CAPABILITY-RELATED SYSTEM CONTEXT:") || !strstr(output, "kernel.yama.ptrace_scope = 1") || !strstr(output, "fs.suid_dumpable = 2") || !strstr(output, "Capabilities with related system context: 1")) fail("Related system context was not grouped by capability"); if (count_text(output, " CAP_SYS_PTRACE\n") != 1) fail("CAP_SYS_PTRACE system context was duplicated"); if (strstr(output, " CAP_DAC_OVERRIDE\n") || strstr(output, "capability needed") || strstr(output, "CONDITIONAL CAPABILITIES")) fail("System context was presented as capability evidence"); free(output); setup_analysis(&service); state.service_file = NULL; state.service_cfg = NULL; state.capset_observed = 0; memset(&state.app.capset, 0, sizeof(state.app.capset)); output = capture_analysis(); if (!contains_tokens(output, (const char *const[]) { "capng_updatev(CAPNG_ADD", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_NET_BIND_SERVICE", "-1);", }, 5) || strstr(output, ", DAC_OVERRIDE")) fail("C recommendation did not use CAP_ constants"); free(output); puts("cap-audit service recommendation tests passed"); return 0; } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/test/cap_audit_service_test.c000066400000000000000000000213271524174023300267200ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* cap_audit_service_test.c -- service credential regression tests * Copyright 2026 Red Hat Inc. * All Rights Reserved. */ #include "config.h" #include #include #include #include #include #include #include #include #include "cap_audit.h" struct audit_state state; int audit_machine; static int change_id_calls; static int changed_uid; static int changed_gid; static capng_flags_t changed_flags; static void fail(const char *msg) { fprintf(stderr, "%s\n", msg); exit(EXIT_FAILURE); } const char *cap_name_safe(int cap) { (void)cap; return "unknown"; } int __wrap_capng_change_id(int uid, int gid, capng_flags_t flags) { change_id_calls++; changed_uid = uid; changed_gid = gid; changed_flags = flags; return 0; } static char *write_service(const char *dir, const char *name, const char *contents) { char *path; size_t len = strlen(contents); ssize_t written; int fd; if (asprintf(&path, "%s/%s", dir, name) < 0) fail("Failed to allocate service path"); fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); if (fd < 0) fail("Failed to open temporary service"); written = write(fd, contents, len); close(fd); if (written < 0 || (size_t)written != len) fail("Failed to write temporary service"); return path; } static int parse_unit(const char *dir, const char *name, const char *contents, service_config_t *cfg) { char *path; int rc; path = write_service(dir, name, contents); rc = parse_service_file(path, cfg); unlink(path); free(path); return rc; } static void expect_parse_failure(const char *dir, const char *name, const char *contents) { service_config_t cfg; if (parse_unit(dir, name, contents, &cfg) == 0) { free_config(&cfg); fail(name); } } static void test_dynamic_user(const char *dir) { service_config_t cfg; const char unit[] = "[Service]\n" "DynamicUser=yes\n" "ExecStart=/usr/bin/true\n"; if (parse_unit(dir, "dynamic.service", unit, &cfg) != 0) fail("DynamicUser service should parse"); if (!cfg.user_resolved || !cfg.user_raw || strcmp(cfg.user_raw, "nobody") != 0) fail("DynamicUser should resolve to nobody for simulation"); if (cfg.user_uid == 0 || cfg.user_uid > INT_MAX || cfg.user_primary_gid == 0 || cfg.user_primary_gid > INT_MAX) fail("DynamicUser simulation must use representable non-root IDs"); change_id_calls = 0; if (apply_service_config(&cfg) != 0) fail("DynamicUser credentials should apply"); if (change_id_calls != 1 || changed_uid != (int)cfg.user_uid || changed_gid != (int)cfg.user_primary_gid) fail("DynamicUser credentials did not reach capng_change_id"); if (!(changed_flags & CAPNG_INIT_SUPP_GRP)) fail("DynamicUser should initialize supplementary groups"); free_config(&cfg); } static void test_unresolved_dynamic_user(const char *dir) { service_config_t cfg; const char unit[] = "[Service]\n" "DynamicUser=yes\n" "User=cap-audit-no-such-user\n" "ExecStart=/usr/bin/true\n"; if (parse_unit(dir, "dynamic-name.service", unit, &cfg) != 0) fail("Unresolved DynamicUser name should use the simulation user"); if (!cfg.user_resolved || !cfg.user_raw || strcmp(cfg.user_raw, "nobody") != 0) fail("Unresolved DynamicUser name should map to nobody"); free_config(&cfg); } static void test_valid_credentials(const char *dir) { service_config_t cfg; const char unit[] = "[Service]\n" "User=0\n" "Group=0\n" "SupplementaryGroups=1 2\n" "ExecStart=/usr/bin/true\n"; if (parse_unit(dir, "valid.service", unit, &cfg) != 0) fail("Valid numeric credentials should parse"); if (!cfg.user_resolved || cfg.user_uid != 0 || !cfg.group_is_set || cfg.group_gid != 0 || cfg.sup_groups.count != 2) fail("Valid numeric credentials were not preserved"); free_config(&cfg); } static void test_invalid_credentials(const char *dir) { static const struct { const char *name; const char *unit; } tests[] = { { "negative-user.service", "[Service]\nUser=-1\nExecStart=/usr/bin/true\n" }, { "positive-user.service", "[Service]\nUser=+1\nExecStart=/usr/bin/true\n" }, { "sentinel-user.service", "[Service]\nUser=4294967295\nExecStart=/usr/bin/true\n" }, { "wrapped-user.service", "[Service]\nUser=4294967296\nExecStart=/usr/bin/true\n" }, { "int-user.service", "[Service]\nUser=2147483648\nExecStart=/usr/bin/true\n" }, { "legacy-user.service", "[Service]\nUser=65535\nExecStart=/usr/bin/true\n" }, { "negative-group.service", "[Service]\nGroup=-1\nExecStart=/usr/bin/true\n" }, { "wrapped-group.service", "[Service]\nGroup=4294967296\nExecStart=/usr/bin/true\n" }, { "negative-supplementary.service", "[Service]\nSupplementaryGroups=-1\nExecStart=/usr/bin/true\n" }, { "wrapped-supplementary.service", "[Service]\nSupplementaryGroups=4294967296\nExecStart=/usr/bin/true\n" }, { "dynamic-negative-user.service", "[Service]\nDynamicUser=yes\nUser=-1\n" "ExecStart=/usr/bin/true\n" }, }; size_t i; for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) expect_parse_failure(dir, tests[i].name, tests[i].unit); } static void test_sink_validation(void) { service_config_t cfg = { .user_uid = (uid_t)UINT32_MAX, .user_primary_gid = (gid_t)UINT32_MAX, .user_is_set = true, .user_resolved = true, }; change_id_calls = 0; if (apply_service_config(&cfg) == 0) fail("Unrepresentable credentials should fail before application"); if (change_id_calls != 0) fail("Invalid credentials reached capng_change_id"); } static void test_exec_start_privilege_prefixes(const char *dir) { service_config_t cfg; const char bang_unit[] = "[Service]\n" "User=1234\n" "Group=1234\n" "SupplementaryGroups=1 2\n" "CapabilityBoundingSet=CAP_CHOWN\n" "NoNewPrivileges=yes\n" "ExecStart=-!/usr/bin/true\n"; const char plus_unit[] = "[Service]\n" "ExecStart=-+/usr/bin/true\n"; const char combined_plus_unit[] = "[Service]\n" "ExecStart=!-+/usr/bin/true\n"; const char double_bang_unit[] = "[Service]\n" "ExecStart=!!/usr/bin/true\n"; if (parse_unit(dir, "bang.service", bang_unit, &cfg) != 0) fail("ExecStart=! service should parse"); if (!cfg.exec_start_no_setuid) fail("ExecStart=! credential semantics were not retained"); if (!cfg.exec_argv || strcmp(cfg.exec_argv[0], "/usr/bin/true")) fail("ExecStart=! prefixes were not removed from executable"); change_id_calls = 0; changed_uid = 0; changed_gid = 0; changed_flags = CAPNG_NO_FLAG; if (apply_service_config(&cfg) != 0) fail("ExecStart=! configuration should apply"); if (change_id_calls != 1 || changed_uid != -1 || changed_gid != -1) fail("ExecStart=! must leave credential changes to the command"); if (changed_flags & (CAPNG_INIT_SUPP_GRP | CAPNG_APPLY_STAGED_GROUPS)) fail("ExecStart=! must not apply supplementary groups"); if (!(changed_flags & CAPNG_APPLY_BOUNDING)) fail("ExecStart=! must still apply the capability bounding set"); if (prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) != 1) fail("ExecStart=! must still apply NoNewPrivileges"); free_config(&cfg); expect_parse_failure(dir, "plus.service", plus_unit); expect_parse_failure(dir, "combined-plus.service", combined_plus_unit); expect_parse_failure(dir, "double-bang.service", double_bang_unit); } static void test_exec_start_environment_words(const char *dir) { service_config_t cfg; const char unit[] = "[Service]\n" "ExecStart=/usr/bin/firewalld --nofork --nopid " "$FIREWALLD_ARGS --label=$KEEP\n"; const char literal_unit[] = "[Service]\n" "ExecStart=:/usr/bin/true $LITERAL_ARG\n"; if (parse_unit(dir, "environment.service", unit, &cfg) != 0) fail("ExecStart environment argument should parse"); if (cfg.exec_argc != 4 || strcmp(cfg.exec_argv[0], "/usr/bin/firewalld") || strcmp(cfg.exec_argv[1], "--nofork") || strcmp(cfg.exec_argv[2], "--nopid") || strcmp(cfg.exec_argv[3], "--label=$KEEP")) fail("Standalone ExecStart environment argument was not omitted"); free_config(&cfg); if (parse_unit(dir, "literal-environment.service", literal_unit, &cfg) != 0) fail("ExecStart=: environment argument should parse"); if (cfg.exec_argc != 2 || strcmp(cfg.exec_argv[0], "/usr/bin/true") || strcmp(cfg.exec_argv[1], "$LITERAL_ARG")) fail("ExecStart=: must disable environment argument omission"); free_config(&cfg); } int main(void) { char dir[] = "/tmp/libcap-ng-service-XXXXXX"; if (mkdtemp(dir) == NULL) fail("Failed to create temporary directory"); test_dynamic_user(dir); test_unresolved_dynamic_user(dir); test_valid_credentials(dir); test_invalid_credentials(dir); test_sink_validation(); test_exec_start_privilege_prefixes(dir); test_exec_start_environment_words(dir); rmdir(dir); puts("cap-audit service credential tests passed"); return 0; } stevegrubb-libcap-ng-9ae4440/utils/cap-audit/test/cap_audit_util_test.c000066400000000000000000000037511524174023300262360ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* cap_audit_util_test.c -- cap-audit helper regression tests * Copyright 2026 Red Hat Inc. * All Rights Reserved. * * These checks pin down classify_app() behavior for very small files. * The historical failure mode was reading a short file and then comparing * shebang or ELF signatures past the bytes returned by read(). */ #include "config.h" #include #include #include #include #include #include #include "classify_app.h" static void fail(const char *msg) { fprintf(stderr, "%s\n", msg); exit(EXIT_FAILURE); } static void write_file(const char *path, const char *data, size_t len) { int fd; ssize_t rc; fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0700); if (fd < 0) fail("Failed to open temporary file"); rc = write(fd, data, len); close(fd); if (rc < 0 || (size_t)rc != len) fail("Failed to write temporary file"); } int main(void) { char dir[] = "/tmp/libcap-ng-cap-audit-XXXXXX"; char path[PATH_MAX]; if (mkdtemp(dir) == NULL) fail("Failed to create temporary directory"); snprintf(path, sizeof(path), "%s/one-byte", dir); write_file(path, "#", 1); if (classify_app(path) != UNSUPPORTED) fail("One-byte file should be unsupported"); snprintf(path, sizeof(path), "%s/short-shebang", dir); write_file(path, "#!", 2); if (classify_app(path) != UNSUPPORTED) fail("Short shebang should be unsupported"); snprintf(path, sizeof(path), "%s/short-elf", dir); write_file(path, "\177EL", 3); if (classify_app(path) != UNSUPPORTED) fail("Short ELF magic should be unsupported"); snprintf(path, sizeof(path), "%s/python", dir); write_file(path, "#!/usr/bin/python\n", 18); if (classify_app(path) != PYTHON) fail("Python shebang should be detected"); snprintf(path, sizeof(path), "%s/elf", dir); write_file(path, "\177ELF", 4); if (classify_app(path) != ELF) fail("ELF magic should be detected"); puts("cap-audit classify_app tests passed"); return 0; } stevegrubb-libcap-ng-9ae4440/utils/captest.8000066400000000000000000000041671524174023300207550ustar00rootroot00000000000000.\" SPDX-License-Identifier: GPL-2.0-or-later .\" Copyright (c) 2009,2020,2026 Red Hat Inc. .TH CAPTEST: "8" "March 2026" "Red Hat" "System Administration Utilities" .SH NAME captest \- a program to demonstrate capabilities .SH SYNOPSIS .B captest [ \-\-ambient \-\-drop-all | \-\-drop-caps | \-\-id ] [ \-\-init-grp ] [ \-\-lock ] [ \-\-text ] .SH DESCRIPTION \fBcaptest\fP is a program that demonstrates and prints out the current process capabilities. Each option prints the same report. It will output current capabilities. then it will try to access /etc/shadow directly to show if that can be done. Then it creates a child process that attempts to read /etc/shadow and outputs the results of that. Then it outputs the capabilities that a child process would have. You can also apply file system capabilities to this program to study how they work. For example, filecap /usr/bin/captest chown. Then run captest as a normal user. Another interesting test is to make captest suid root so that you can see what the interaction is between root's credentials and capabilities. For example, chmod 4755 /usr/bin/captest. When run as a normal user, the program will see if privilege escalation is possible. But do not leave this app setuid root after you are done testing so that an attacker cannot take advantage of it. .SH OPTIONS .TP .B \-\-ambient This attempts to add CAP_CHOWN ambient capability. .TP .B \-\-drop-all This drops all capabilities including ambient and clears the bounding set. .TP .B \-\-drop-caps This drops just traditional capabilities. .TP .B \-\-id This changes to uid and gid 99, drops supplemental groups, and clears the bounding set. .TP .B \-\-init-grp This changes to uid and gid 99 and then adds any supplemental groups that comes with that account. You would have to add them prior to testing because by default there are no supplemental groups on account 99. .TP .B \-\-text This option outputs the effective capabilities in text rather than numerically. .TP .B \-\-lock This prevents the ability for child processes to regain privileges if the uid is 0. .SH "SEE ALSO" .BR filecap (8), .BR capabilities (7) .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/utils/captest.c000066400000000000000000000173731524174023300210330ustar00rootroot00000000000000/* * captest.c - A program that demonstrates and outputs capabilities * Copyright (c) 2009, 2013, 2020, 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 * */ #include "config.h" #include #include #include #include #include #include #include #include #ifdef HAVE_LINUX_SECUREBITS_H #include #endif /* children can't get caps back */ #ifndef SECURE_NOROOT #define SECURE_NOROOT 0 #endif #ifndef SECURE_NOROOT_LOCKED #define SECURE_NOROOT_LOCKED 1 /* make bit-0 immutable */ #endif /* Setuid apps run by uid 0 don't get caps back */ #ifndef SECURE_NO_SETUID_FIXUP #define SECURE_NO_SETUID_FIXUP 2 #endif #ifndef SECURE_NO_SETUID_FIXUP_LOCKED #define SECURE_NO_SETUID_FIXUP_LOCKED 3 /* make bit-2 immutable */ #endif static int text = 0, no_child = 0, lock = 0, ambient = 0; static void report(void) { int rc, escalated = 0, need_comma = 0; uid_t uid, euid, suid; gid_t gid, egid, sgid; // Refresh what we have for capabilities if (capng_get_caps_process()) { printf("Error getting capabilities\n"); exit(1); } // Check user credentials getresuid(&uid, &euid, &suid); getresgid(&gid, &egid, &sgid); if (no_child) { if ((uid != euid && uid != 0) || capng_have_capability(CAPNG_EFFECTIVE, CAP_SETUID)) { printf("Attempting to regain root..."); setuid(0); getresuid(&uid, &euid, &suid); if (uid == 0) { printf("SUCCESS - PRIVILEGE ESCALATION POSSIBLE\n"); setgid(0); getresgid(&gid, &egid, &sgid); escalated = 1; } else printf("FAILED\n"); } printf("Child "); } printf("User credentials uid:%u euid:%u suid:%u\n", uid, euid, suid); if (no_child) printf("Child "); printf("Group credentials gid:%u egid:%u sgid:%u\n", gid, egid, sgid); if (uid != euid || gid != egid) printf("Note: app has mismatching credentials!!\n"); // Check capabilities if (text) { if (capng_have_capabilities(CAPNG_SELECT_CAPS) == CAPNG_NONE) { if (no_child) printf("Child capabilities: none\n"); else printf("Current capabilities: none\n"); } else { if (no_child) printf("Child "); printf("Effective: "); capng_print_caps_text(CAPNG_PRINT_STDOUT, CAPNG_EFFECTIVE); printf("\n"); if (no_child) printf("Child "); printf("Permitted: "); capng_print_caps_text(CAPNG_PRINT_STDOUT, CAPNG_PERMITTED); printf("\n"); if (no_child) printf("Child "); printf("Inheritable: "); capng_print_caps_text(CAPNG_PRINT_STDOUT, CAPNG_INHERITABLE); printf("\n"); if (no_child) printf("Child "); printf("Bounding Set: "); capng_print_caps_text(CAPNG_PRINT_STDOUT, CAPNG_BOUNDING_SET); printf("\n"); if (no_child) printf("Child "); printf("Ambient: "); capng_print_caps_text(CAPNG_PRINT_STDOUT, CAPNG_AMBIENT); printf("\n"); } } else { if (capng_have_capabilities(CAPNG_SELECT_CAPS) == CAPNG_NONE) { if (no_child) printf("Child capabilities: none\n"); else printf("Current capabilities: none\n"); } else { if (no_child) printf("Child capabilities:\n"); capng_print_caps_numeric(CAPNG_PRINT_STDOUT, CAPNG_SELECT_ALL); } } // Now check securebits flags #ifdef PR_SET_SECUREBITS if (no_child) printf("Child "); printf("securebits flags: "); rc = prctl(PR_GET_SECUREBITS, 1 << SECURE_NOROOT); if (rc & (1 << SECURE_NOROOT)) { printf("NOROOT"); need_comma = 1; } rc = prctl(PR_GET_SECUREBITS, 1 << SECURE_NOROOT_LOCKED); if (rc & (1 << SECURE_NOROOT_LOCKED)) { if (need_comma) printf(", "); printf("NOROOT_LOCKED"); need_comma = 1; } rc = prctl(PR_GET_SECUREBITS, 1 << SECURE_NO_SETUID_FIXUP); if (rc & (1 << SECURE_NO_SETUID_FIXUP)) { if (need_comma) printf(", "); printf("NO_SETUID_FIXUP"); need_comma = 1; } rc = prctl(PR_GET_SECUREBITS, 1 << SECURE_NO_SETUID_FIXUP_LOCKED); if (rc & (1 << SECURE_NO_SETUID_FIXUP_LOCKED)) { if (need_comma) printf(", "); printf("NO_SETUID_FIXUP_LOCKED"); need_comma = 1; } if (need_comma == 0) printf("none"); printf("\n"); #endif // Now do child process checks if (no_child == 0 || escalated) { printf("Attempting direct access to shadow..."); if (access("/etc/shadow", R_OK) == 0) printf("SUCCESS\n"); else printf("FAILED (%s)\n", strerror(errno)); } if (no_child == 0) { printf("Attempting to access shadow by child process..."); rc = system("cat /etc/shadow > /dev/null 2>&1"); if (rc == 0) printf("SUCCESS\n"); else printf("FAILED\n"); if (text) system("/usr/bin/captest --no-child --text"); else system("/usr/bin/captest --no-child"); } } static void usage(void) { printf("usage: captest [ --ambient | --drop-all | --drop-caps | --id | --init-grp ] [ --lock ] [ --text ]\n"); } int main(int argc, char *argv[]) { int which = 0, i; for (i = 1; i < argc; i++) { if (strcmp(argv[i], "--text") == 0) text = 1; else if (strcmp(argv[i], "--no-child") == 0) no_child = 1; else if (strcmp(argv[i], "--lock") == 0) lock = 1; else if (strcmp(argv[i], "--drop-all") == 0) which = 1; else if (strcmp(argv[i], "--drop-caps") == 0) which = 2; else if (strcmp(argv[i], "--id") == 0) which = 3; else if (strcmp(argv[i], "--init-grp") == 0) which = 4; else if (strcmp(argv[i], "--ambient") == 0) ambient = 1; else { usage(); return 0; } } switch (which) { case 1: capng_clear(CAPNG_SELECT_ALL); if (lock) { if (capng_lock()) { fprintf(stderr, "capng_lock failed\n"); return 1; } } if (capng_apply(CAPNG_SELECT_ALL)) { fprintf(stderr, "capng_apply failed\n"); return 1; } report(); break; case 2: capng_clear(CAPNG_SELECT_CAPS); if (ambient) capng_update(CAPNG_ADD, CAPNG_AMBIENT, CAP_CHOWN); if (lock) { if (capng_lock()) { fprintf(stderr, "capng_lock failed\n"); return 1; } } if (ambient) { if (capng_apply( CAPNG_SELECT_CAPS|CAPNG_SELECT_AMBIENT)) { fprintf(stderr, "capng_apply failed\n"); return 1; } } else { if (capng_apply(CAPNG_SELECT_CAPS)) { fprintf(stderr, "capng_apply failed\n"); return 1; } } report(); break; case 3: case 4: { int rc; capng_clear(CAPNG_SELECT_BOTH); capng_update(CAPNG_ADD, CAPNG_EFFECTIVE|CAPNG_PERMITTED, CAP_CHOWN); if (ambient) capng_update(CAPNG_ADD, CAPNG_INHERITABLE|CAPNG_AMBIENT, CAP_CHOWN); if (which == 4) rc = capng_change_id(99, 99, CAPNG_INIT_SUPP_GRP | CAPNG_CLEAR_BOUNDING); else rc = capng_change_id(99, 99, CAPNG_DROP_SUPP_GRP | CAPNG_CLEAR_BOUNDING); if (rc < 0) { printf("Error changing uid: %d\n", rc); capng_print_caps_text(CAPNG_PRINT_STDOUT, CAPNG_EFFECTIVE); printf("\n"); return 1; } printf("Keeping CAP_CHOWN to show capabilities across uid change.\n"); report(); } break; case 0: if (lock) { if (capng_lock()) { fprintf(stderr, "capng_lock failed\n"); return 1; } } report(); break; } return 0; } stevegrubb-libcap-ng-9ae4440/utils/filecap-path.c000066400000000000000000000026071524174023300217170ustar00rootroot00000000000000/* * filecap-path.c - PATH parsing helpers for filecap * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * filecap scans PATH when invoked without an explicit file or directory. * Empty PATH components are significant because the shell interprets them * as the current working directory. This helper exists so filecap keeps * those entries instead of dropping them the way strtok() would, and so * the edge cases can be tested under make check without walking a real * filesystem tree. */ #include "config.h" #include #include #include "filecap-path.h" int filecap_foreach_path(const char *path_env, filecap_path_cb cb, void *data) { const char *start; if (path_env == NULL) return 0; if (cb == NULL) return -1; start = path_env; while (start) { const char *end = strchr(start, ':'); size_t len = end ? (size_t)(end - start) : strlen(start); char *entry = NULL; const char *path = "."; int rc; if (len != 0) { entry = strndup(start, len); if (entry == NULL) return -1; path = entry; } rc = cb(path, data); free(entry); if (rc) return rc; if (!end) break; start = end + 1; } return 0; } stevegrubb-libcap-ng-9ae4440/utils/filecap-path.h000066400000000000000000000012151524174023300217160ustar00rootroot00000000000000/* * filecap-path.h - PATH parsing helpers for filecap * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef FILECAP_PATH_H #define FILECAP_PATH_H #include "gcc-attributes.h" typedef int (*filecap_path_cb)(const char *entry, void *data) __attr_access ((__read_only__, 1)); int filecap_foreach_path(const char *path_env, filecap_path_cb cb, void *data) __attr_access ((__read_only__, 1)) __wur; #endif stevegrubb-libcap-ng-9ae4440/utils/filecap.8000066400000000000000000000033061524174023300207070ustar00rootroot00000000000000.\" SPDX-License-Identifier: GPL-2.0-or-later .\" Copyright (c) 2009,2017-18 Red Hat Inc. .TH FILECAP: "8" "Aug 2018" "Red Hat" "System Administration Utilities" .SH NAME filecap \- a program to see capabilities .SH SYNOPSIS .B filecap [ \-a | \-d | /dir | /dir/file [cap1 cap2 ...] ] .SH DESCRIPTION \fBfilecap\fP is a program that prints out a report of programs with file based capabilities. If a file is not in the report or there is no report at all, no capabilities were found. For expedience, the default is to check only the directories in the PATH environmental variable. If the \-a command line option is given, then all directories will be checked. If a directory is passed, it will recursively check that directory. If a path to a file is given, it will only check that file. If a file is given followed by capabilities, then the capabilities are written to the file. .SH OPTIONS .TP .B \-a This tells the program to show all capabilities starting from the / directory. Normally the PATH environmental variable is used to show you capabilities on files you are likely to execute. .TP .B \-d This dumps all capabilities for reference. .SH "EXAMPLES" .nf To check file capabilities in $PATH: .B filecap To check file capabilities of whole system: .B filecap -a To check file capabilities recursively in a directory: .B filecap /usr To check file capabilities of a specific program: .B filecap /bin/passwd To list all possible capabilities: .B filecap -d To set a file capability on a specific program: .B filecap /bin/ping net_raw net_admin To remove file capabilities on a specific program: .B filecap /bin/ping none .fi .SH "SEE ALSO" .BR pscap (8), .BR netcap (8), .BR capabilities (7). .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/utils/filecap.c000066400000000000000000000155261524174023300207710ustar00rootroot00000000000000/* * filecap.c - A program that lists running processes with capabilities * Copyright (c) 2009-10,2012,2017-20,2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #include "config.h" #include #include #include #include #include #include "cap-ng.h" #include "filecap-path.h" #include #include #ifndef FTW_CONTINUE #define FTW_CONTINUE 0 #endif static int show_all = 0, header = 0, capabilities = 0, cremove = 0; static int single_file = 0; static int check_file(const char *fpath, const struct stat *sb, int typeflag_unused, struct FTW *s_unused) __attr_access ((__read_only__, 1)) __attr_access ((__read_only__, 2)); static int scan_path_entry(const char *entry, void *data) __attr_access ((__read_only__, 1)) __attr_access ((__read_write__, 2)); static int scan_path_env(const char *path_env, int nftw_flags) __attr_access ((__read_only__, 1)) __wur; static void usage(void) { fprintf(stderr, "usage: filecap [-a | -d | /dir | /dir/file [cap1 cap2 ...] ]\n"); exit(1); } // Returns 1 on error and 0 otherwise 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) { capng_results_t rc; int permitted = 0; capng_clear(CAPNG_SELECT_BOTH); if (capng_get_caps_fd(fd) < 0 && errno != ENODATA) { fprintf(stderr, "Unable to get capabilities of %s: %s\n", fpath, strerror(errno)); if (single_file) ret = 1; } rc = capng_have_capabilities(CAPNG_SELECT_CAPS); if (rc == CAPNG_NONE) { permitted = 1; rc = capng_have_permitted_capabilities(); } if (rc > CAPNG_NONE) { if (header == 0) { header = 1; printf("%-9s %-22s capabilities rootid\n", "set", "file"); } uid_t rootid = capng_get_rootid(); printf("%s %-22s ", permitted ? "permitted" : "effective", fpath); if (rc == CAPNG_FULL) printf("full"); else capng_print_caps_text(CAPNG_PRINT_STDOUT, CAPNG_PERMITTED); if (rootid != CAPNG_UNSET_ROOTID) printf(" %ju", (uintmax_t)rootid); printf("\n"); } close(fd); } return ret; } /* * scan_path_env - walk each PATH element while preserving empty entries. * * Empty PATH components mean the current working directory. strtok() drops * them, which makes the default filecap scan disagree with normal PATH * lookup semantics. */ struct path_scan_ctx { int nftw_flags; }; static int scan_path_entry(const char *entry, void *data) { struct path_scan_ctx *ctx = data; nftw(entry, check_file, 1024, ctx->nftw_flags); return 0; } static int scan_path_env(const char *path_env, int nftw_flags) { struct path_scan_ctx ctx; ctx.nftw_flags = nftw_flags; return filecap_foreach_path(path_env, scan_path_entry, &ctx); } // Use cases: // filecap // filecap -a // filecap /path/dir // filecap /path/file // filecap /path/file capability1 capability2 capability 3 ... // int main(int argc, char *argv[]) { #if CAP_LAST_CAP < 31 || !defined (VFS_CAP_U32) || \ (!defined (HAVE_ATTR_XATTR_H) && !defined(HAVE_SYS_XATTR_H)) fprintf(stderr, "File based capabilities are not supported\n"); #else char *path_env, *path = NULL, *dir = NULL; struct stat sbuf; struct stat path_sbuf; int nftw_flags = FTW_PHYS; int i, rc = 0; int have_path_stat = 0; if (argc >1) { for (i=1; i= 0) { if (path == NULL) usage(); capng_update(CAPNG_ADD, CAPNG_PERMITTED|CAPNG_EFFECTIVE, cap); capabilities = 1; } else if (strcmp("none", argv[i]) == 0) { capng_clear(CAPNG_SELECT_BOTH); capabilities = 1; cremove = 1; } else { fprintf(stderr, "Unrecognized capability.\n"); usage(); } } } } if (path == NULL && dir == NULL && show_all == 0) { path_env = getenv("PATH"); if (path_env != NULL) { if (scan_path_env(path_env, nftw_flags) < 0) return 1; } } else if (path == NULL && dir == NULL && show_all == 1) { // Find files nftw("/", check_file, 1024, nftw_flags); } else if (dir) { // Print out the dir nftw(dir, check_file, 1024, nftw_flags); }else if (path && capabilities == 0) { // Print out specific file single_file = 1; rc = check_file(path, &sbuf, 0, NULL); } else if (path && capabilities == 1) { // Write capabilities to file struct stat fbuf; int fd = open(path, O_WRONLY|O_NOFOLLOW|O_CLOEXEC); if (fd < 0) { fprintf(stderr, "Could not open %s for writing (%s)\n", path, strerror(errno)); return 1; } if (have_path_stat && fstat(fd, &fbuf) == 0 && (fbuf.st_dev != path_sbuf.st_dev || fbuf.st_ino != path_sbuf.st_ino)) { fprintf(stderr, "File changed during operation, refusing " "to continue\n"); close(fd); return 1; } if (capng_apply_caps_fd(fd) < 0) { fprintf(stderr, "Could not set capabilities on %s: %s\n", path, strerror(errno)); rc = 1; } close(fd); } #endif return rc; } stevegrubb-libcap-ng-9ae4440/utils/gcc-attributes.h000066400000000000000000000013751524174023300223100ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (c) 2026 Steve Grubb #ifndef GCC_ATTRIBUTES_H #define GCC_ATTRIBUTES_H #include /* These macros originate in sys/cdefs.h. Stub them when unavailable. */ #ifndef __returns_nonnull # define __returns_nonnull #endif #ifndef __attr_access # define __attr_access(x) #endif #ifndef __attr_dealloc # define __attr_dealloc(dealloc, argno) #endif #ifndef __attr_dealloc_free # 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 stevegrubb-libcap-ng-9ae4440/utils/libcap-ng.bash_completion000066400000000000000000000034371524174023300241440ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0-or-later # Copyright (c) 2026 Steve Grubb _pscap() { local cur prev local opts="-a -p --tree" COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" case "$prev" in -p) COMPREPLY=( $(compgen -W "$(ps -eo pid= 2>/dev/null)" \ -- "$cur") ) return 0 ;; esac if [[ "$cur" == -* ]]; then COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) fi } _netcap() { local cur prev local opts="--advanced --interface --list-interfaces --json --no-color" COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" case "$prev" in --interface) COMPREPLY=( $(compgen -W "$(command ls /sys/class/net \ 2>/dev/null)" -- "$cur") ) return 0 ;; esac if [[ "$cur" == -* ]]; then COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) fi } _filecap() { local cur prev local opts="-a -d" COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" if [[ "$cur" == -* ]]; then COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) return 0 fi case "$prev" in -a|-d) return 0 ;; esac if [[ "$cur" == /* ]]; then COMPREPLY=( $(compgen -d -- "$cur") $(compgen -f -- "$cur") ) fi } _cap_audit() { local cur prev local opts="-h --help -v --verbose -j --json -y --yaml -s --service --" COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" if [[ "$prev" == "--" ]]; then COMPREPLY=( $(compgen -c -- "$cur") ) return 0 fi case "$prev" in -s|--service) COMPREPLY=( $(compgen -f -- "$cur") ) return 0 ;; esac if [[ "$cur" == -* ]]; then COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) else COMPREPLY=( $(compgen -c -- "$cur") ) fi } complete -F _pscap pscap complete -F _netcap netcap complete -F _filecap filecap complete -F _cap_audit cap-audit stevegrubb-libcap-ng-9ae4440/utils/netcap-advanced.c000066400000000000000000003074431524174023300224050ustar00rootroot00000000000000/* * netcap-advanced.c - Advanced capability analysis * Copyright (c) 2026 Steve Grubb * 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 #if defined(HAVE_BLUETOOTH_BLUETOOTH_H) && defined(HAVE_BLUETOOTH_HCI_H) #include #include #define HAVE_NETCAP_BLUETOOTH 1 #endif #include #include #include #include #include #include #include #include #ifdef HAVE_NETCAP_VSOCK #include #include #ifdef HAVE_LINUX_VM_SOCKETS_DIAG_H #include #endif #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cap-ng.h" #include "netcap-advanced.h" #include "proc-output.h" #include "proc-sanitize.h" #include "proc-status.h" /* * Overview: * netcap --advanced builds a process/socket ownership model from procfs and * sock_diag, then renders it as a tree or JSON without changing system state. * * The flow is: discover interface addresses, map socket inode->process * ownership from /proc//fd, parse protocol-specific listener tables, * and project each endpoint onto interface/plane groupings for reporting. * * For internet sockets, wildcard binds are expanded onto concrete interface * addresses so the rendered tree/JSON can be consumed as an exposure map. * Bluetooth listeners are collected from /proc/net/rfcomm and /proc/net/hci, * with adapter names discovered via /sys/class/bluetooth and controller * addresses resolved through HCI ioctls before ownership is stitched back * through socket inodes gathered from /proc//fd. * VSOCK listeners are collected via sock_diag when available and fall back to * /proc parsing when not, with ownership stitched back through socket inodes. * Tree output supports colorized capability/flag severity, with --no-color * forcing plain text. SO_REUSEPORT is detected per socket via pidfd_open and * pidfd_getfd while scanning /proc//fd so the flag can be propagated into * endpoint rendering. * * Process metadata includes ambient capability enumeration with per-capability * detail and cgroup-unit extraction limited to system.slice services to keep * ownership context focused on service units. Line wrapping for tree output is * ANSI-escape-aware so colorized text wraps at display width without breaking * SGR sequences. * * Results depend on the current network namespace and procfs visibility; * restricted privileges can hide processes/sockets and yield partial output. */ #ifdef NETCAP_DIAG_DEBUG #define diag_dbg(fmt, ...) \ fprintf(stderr, "netcap-diag: " fmt "\n", ##__VA_ARGS__) #else #define diag_dbg(fmt, ...) do { } while (0) #endif enum plane_kind { PLANE_INET_EXTERNAL, PLANE_INET_LOOPBACK, PLANE_PACKET, PLANE_BLUETOOTH, PLANE_VSOCK, PLANE_COUNT, }; #define PLANE_PACKET_NAME "LINK-LAYER" #define PLANE_BLUETOOTH_NAME "BLUETOOTH" /* Keep user-facing key name centralized to avoid legacy regressions. */ #define DEFENSES_RUNS_AS_KEY "runs_as_nonroot" #define BT_ADAPTER_UNKNOWN "unknown" #define BT_ADAPTER_HEURISTIC "hci?" #define BT_ADDR_UNKNOWN "*" enum endpoint_flags { FLAG_WILDCARD_BIND = 1U << 0, FLAG_PRIVILEGED_CAPS = 1U << 2, FLAG_HYPERVISOR_PLANE = 1U << 4, FLAG_SSH_VSOCK_22 = 1U << 5, FLAG_REUSEPORT = 1U << 6, FLAG_PROXIMITY_PLANE = 1U << 7, }; struct strset { const char **slots; size_t slots_cap; size_t used; }; struct iface_addr { int af; char *addr; }; struct iface_info { char *name; struct iface_addr *addrs; size_t addrs_n; size_t addrs_cap; }; struct defense_info { char *runs_as_nonroot; char *no_new_privs; char *seccomp; char *lsm_label; }; struct process_info { int pid; uid_t uid; char *comm; char *exe; char *unit; char *caps; char *ambient_caps; int ambient_present; int open_ended_bounding; int has_privileged_caps; struct defense_info defenses; }; struct inode_proc { unsigned long inode; struct process_info **procs; size_t n; size_t cap; int reuseport; }; struct endpoint { char *proto; char *bind; char *label; unsigned int port; unsigned int vsock_cid; int has_vsock; enum plane_kind plane; char *ifname; char *ifaddr; struct process_info **procs; size_t procs_n; size_t procs_cap; int wildcard_bind; int reuseport; }; struct model { struct iface_info *ifaces; size_t ifaces_n; size_t ifaces_cap; struct process_info **procs; size_t procs_n; size_t procs_cap; struct inode_proc *inode_map; size_t inode_n; size_t inode_cap; size_t *inode_slots; size_t inode_slots_cap; struct endpoint *eps; size_t eps_n; size_t eps_cap; }; #define INODE_SLOT_EMPTY SIZE_MAX #define PIDSET_EMPTY INT_MIN struct pidset { int *slots; size_t cap; size_t used; }; struct endpoint_attrs { int wildcard; int reuseport; }; #ifdef HAVE_NETCAP_BLUETOOTH #define BT_ADAPTER_CACHE_MAX 8 struct bt_adapter_info { char name[IF_NAMESIZE]; int dev_id; char addr[18]; }; static struct bt_adapter_info bt_adapters[BT_ADAPTER_CACHE_MAX]; static size_t bt_adapters_n; static int bt_adapters_loaded; #endif static void free_process(struct process_info *p) __attr_access ((__read_write__, 1)); static void free_endpoint(struct endpoint *e) __attr_access ((__read_write__, 1)); static int str_is_bdaddr(const char *s) __attr_access ((__read_only__, 1)) __attribute_pure__; static char *xstrdup(const char *s) __attribute_malloc__ __attr_dealloc_free __attr_access ((__read_only__, 1)) __wur; static int vec_grow(void **v, size_t *cap, size_t item) __attr_access ((__read_write__, 1)) __attr_access ((__read_write__, 2)) __wur; static char *read_first_line(const char *path) __attribute_malloc__ __attr_dealloc_free __attr_access ((__read_only__, 1)) __wur; static char *extract_unit_from_cgroup(int pid) __attribute_malloc__ __attr_dealloc_free __wur; static char *caps_summary_for_pid(int pid, int *privileged, int *has_amb, int *has_bnd, char **amb_list) __attribute_malloc__ __attr_dealloc_free __attr_access ((__write_only__, 2)) __attr_access ((__write_only__, 3)) __attr_access ((__write_only__, 4)) __attr_access ((__write_only__, 5)) __wur; static int use_color; #define COLOR_ORANGE "\033[38;5;208m" #define COLOR_YELLOW "\033[38;5;226m" #define COLOR_GREEN "\033[38;5;82m" #define COLOR_RESET "\033[0m" enum cap_severity { CAP_SEV_NEUTRAL, CAP_SEV_YELLOW, CAP_SEV_ORANGE, }; static const char *orange_caps[] = { "sys_ptrace", "sys_module", "sys_rawio", "setuid", "setgid", "setpcap", "audit_control", }; static const char *yellow_caps[] = { "sys_admin", "dac_override", "dac_read_search", "net_admin", "net_raw", "chown", "fowner", "mknod", "sys_chroot", }; /* * cap_name_severity - classify one capability name into severity tiers. * @name: capability token without "cap_" prefix. * * Returns severity bucket used for tree color selection. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static enum cap_severity cap_name_severity(const char *name) { size_t i; for (i = 0; i < sizeof(orange_caps) / sizeof(orange_caps[0]); i++) { if (strcmp(name, orange_caps[i]) == 0) return CAP_SEV_ORANGE; } for (i = 0; i < sizeof(yellow_caps) / sizeof(yellow_caps[0]); i++) { if (strcmp(name, yellow_caps[i]) == 0) return CAP_SEV_YELLOW; } return CAP_SEV_NEUTRAL; } /* * caps_contains_token - test whether @token appears as a capability list item. * @caps: comma/space separated capability summary text. * @token: capability token to locate. * * Returns non-zero when @token is present as a whole list element, else 0. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int caps_contains_token(const char *caps, const char *token) { size_t len; const char *p; if (!caps || !token) return 0; len = strlen(token); for (p = caps; (p = strstr(p, token)) != NULL; p++) { int left_ok = (p == caps) || (p > caps + 1 && p[-1] == ' ' && p[-2] == ','); char right = p[len]; int right_ok = right == 0 || right == ',' || right == ' ' || right == '['; if (left_ok && right_ok) return 1; } return 0; } /* * sev_color - map severity level to ANSI color sequence. * @sev: severity class to map. * * Returns static color code pointer, or NULL when uncolored. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static const char *sev_color(enum cap_severity sev) { if (sev == CAP_SEV_ORANGE) return COLOR_ORANGE; if (sev == CAP_SEV_YELLOW) return COLOR_YELLOW; return NULL; } /* * caps_worst_severity - find highest severity capability in @caps text. * @caps: capability list text from caps_summary_for_pid(). * * Returns highest matched severity, or CAP_SEV_NEUTRAL. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static enum cap_severity caps_worst_severity(const char *caps) { size_t i; if (!caps) return CAP_SEV_NEUTRAL; if (strcmp(caps, "(full)") == 0) return CAP_SEV_ORANGE; for (i = 0; i < sizeof(orange_caps) / sizeof(orange_caps[0]); i++) { if (caps_contains_token(caps, orange_caps[i])) return CAP_SEV_ORANGE; } for (i = 0; i < sizeof(yellow_caps) / sizeof(yellow_caps[0]); i++) { if (caps_contains_token(caps, yellow_caps[i])) return CAP_SEV_YELLOW; } return CAP_SEV_NEUTRAL; } static void free_model(struct model *m); static void json_escape(const char *s); static int bind_sort_cmp(const char *a, const char *b); static struct inode_proc *lookup_inode(struct model *m, unsigned long inode); static void render_interfaces_text(struct model *m); static void render_interfaces_json(struct model *m); /* * iface_addr_cmp - order interface addresses by family then text. * @av/@bv: qsort pointers to struct iface_addr elements. * * Returns strcmp-style ordering suitable for qsort(). */ static int iface_addr_cmp(const void *av, const void *bv) { const struct iface_addr *a = av; const struct iface_addr *b = bv; if (a->af != b->af) return a->af - b->af; return strcmp(a->addr, b->addr); } /* * iface_info_cmp - order interfaces by name for stable output. * @av/@bv: qsort pointers to struct iface_info elements. * * Returns strcmp-style ordering suitable for qsort(). */ static int iface_info_cmp(const void *av, const void *bv) { const struct iface_info *a = av; const struct iface_info *b = bv; return strcmp(a->name, b->name); } /* * sort_interfaces - sort interfaces and per-interface addresses in place. * @m: model containing interface inventory from getifaddrs(). * * Returns no value. */ static void sort_interfaces(struct model *m) { size_t i; if (m->ifaces_n > 1) qsort(m->ifaces, m->ifaces_n, sizeof(struct iface_info), iface_info_cmp); for (i = 0; i < m->ifaces_n; i++) { if (m->ifaces[i].addrs_n > 1) qsort(m->ifaces[i].addrs, m->ifaces[i].addrs_n, sizeof(struct iface_addr), iface_addr_cmp); } } /* * str_hash - hash a string key for open-addressed set placement. * @s: NUL-terminated key string. * @slots_cap: destination hash table capacity. * * Returns index in [0, @slots_cap). * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static size_t str_hash(const char *s, size_t slots_cap) { uint64_t x = 1469598103934665603ULL; for (; *s; s++) { x ^= (unsigned char)*s; x *= 1099511628211ULL; } return (size_t)(x % slots_cap); } /* * strset_rebuild - resize and rehash string set storage. * @set: set object whose slots array is replaced. * @new_cap: requested slot capacity before minimum clamping. * * Returns 0 on success, -1 on allocation failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int strset_rebuild(struct strset *set, size_t new_cap) { const char **slots; size_t i; if (new_cap < 16) new_cap = 16; slots = calloc(new_cap, sizeof(*slots)); if (!slots) { fprintf(stderr, "Out of memory\n"); return -1; } for (i = 0; i < set->slots_cap; i++) { size_t pos; if (!set->slots[i]) continue; pos = str_hash(set->slots[i], new_cap); while (slots[pos]) pos = (pos + 1) % new_cap; slots[pos] = set->slots[i]; } free(set->slots); set->slots = slots; set->slots_cap = new_cap; return 0; } /* * strset_add - insert @s into the string set if absent. * @set: destination hash set. * @s: caller-owned string pointer stored by reference. * * Returns 1 when inserted, 0 when already present, -1 on failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int strset_add(struct strset *set, const char *s) { size_t pos; if (set->slots_cap == 0) { if (strset_rebuild(set, 16) != 0) return -1; } if ((set->used + 1) * 4 >= set->slots_cap * 3) { size_t new_cap = set->slots_cap * 2; if (new_cap < set->slots_cap) return -1; if (strset_rebuild(set, new_cap) != 0) return -1; } pos = str_hash(s, set->slots_cap); while (set->slots[pos]) { if (strcmp(set->slots[pos], s) == 0) return 0; pos = (pos + 1) % set->slots_cap; } set->slots[pos] = s; set->used++; return 1; } /* * strset_free - release dynamic storage owned by @set. * @set: hash set to reset. * * Returns no value. * Side effects/assumptions: Frees heap memory referenced by @set. */ static void strset_free(struct strset *set) { free(set->slots); set->slots = NULL; set->slots_cap = 0; set->used = 0; } /* * inode_hash - hash inode key for inode ownership map slot selection. * @inode: socket inode value. * @slots_cap: destination hash table capacity. * * Returns index in [0, @slots_cap). * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static size_t inode_hash(unsigned long inode, size_t slots_cap) { uint64_t x = inode; x ^= x >> 33; x *= 0xff51afd7ed558ccdULL; x ^= x >> 33; x *= 0xc4ceb9fe1a85ec53ULL; x ^= x >> 33; return (size_t)(x % slots_cap); } /* * inode_hash_rebuild - resize and repopulate inode hash slots. * @m: model containing inode_map entries and slot metadata. * @new_cap: requested new slot capacity. * * Returns 0 on success, -1 on allocation failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int inode_hash_rebuild(struct model *m, size_t new_cap) { size_t i; size_t *slots; if (new_cap < 16) new_cap = 16; slots = malloc(new_cap * sizeof(*slots)); if (!slots) { fprintf(stderr, "Out of memory\n"); return -1; } for (i = 0; i < new_cap; i++) slots[i] = INODE_SLOT_EMPTY; for (i = 0; i < m->inode_n; i++) { size_t pos = inode_hash(m->inode_map[i].inode, new_cap); while (slots[pos] != INODE_SLOT_EMPTY) pos = (pos + 1) % new_cap; slots[pos] = i; } free(m->inode_slots); m->inode_slots = slots; m->inode_slots_cap = new_cap; return 0; } /* * inode_hash_ensure_capacity - grow inode hash table when load is high. * @m: model whose inode slot table may be resized. * * Returns 0 when capacity is sufficient or grown, -1 on failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int inode_hash_ensure_capacity(struct model *m) { size_t new_cap; if (m->inode_slots_cap == 0) return inode_hash_rebuild(m, 16); if ((m->inode_n + 1) * 4 < m->inode_slots_cap * 3) return 0; new_cap = m->inode_slots_cap * 2; if (new_cap < m->inode_slots_cap) return -1; return inode_hash_rebuild(m, new_cap); } /* * inode_hash_find - find inode_map index for @inode. * @m: model containing inode hash slots. * @inode: inode key to search. * * Returns non-negative inode_map index on hit, -1 on miss. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static ssize_t inode_hash_find(struct model *m, unsigned long inode) { size_t pos; size_t start; if (m->inode_slots_cap == 0) return -1; pos = inode_hash(inode, m->inode_slots_cap); start = pos; while (m->inode_slots[pos] != INODE_SLOT_EMPTY) { size_t idx = m->inode_slots[pos]; if (m->inode_map[idx].inode == inode) return idx; pos = (pos + 1) % m->inode_slots_cap; if (pos == start) break; } return -1; } /* * inode_hash_insert - place one inode_map index into hash slots. * @m: model containing destination hash slots. * @idx: inode_map entry index to insert. * * Returns no value. * Side effects/assumptions: Mutates @m->inode_slots insertion state. */ static void inode_hash_insert(struct model *m, size_t idx) { size_t pos = inode_hash(m->inode_map[idx].inode, m->inode_slots_cap); while (m->inode_slots[pos] != INODE_SLOT_EMPTY) pos = (pos + 1) % m->inode_slots_cap; m->inode_slots[pos] = idx; } /* * xstrdup - duplicate @s into a newly allocated string. * @s: source string, or NULL. * * Returns caller-owned copy, or NULL for NULL input or allocation failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static char *xstrdup(const char *s) { char *p; if (!s) return NULL; p = strdup(s); if (!p) fprintf(stderr, "Out of memory\n"); return p; } /* * vec_grow - grow vector storage for dynamic arrays. * @v: pointer to heap buffer pointer updated on success. * @cap: current/new capacity element count. * @item: size in bytes of each element. * * Returns 0 on success, -1 on allocation failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int vec_grow(void **v, size_t *cap, size_t item) { void *p; size_t ncap; if (*cap) ncap = *cap * 2; else ncap = 8; p = realloc(*v, ncap * item); if (!p) { fprintf(stderr, "Out of memory\n"); return -1; } *v = p; *cap = ncap; return 0; } /* * pid_hash - hash process ID for pidset probing. * @pid: process id key. * @cap: pidset slot capacity. * * Returns slot index in [0, @cap). * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static size_t pid_hash(int pid, size_t cap) { uint32_t v = (uint32_t)pid; v ^= v >> 16; v *= 0x7feb352dU; v ^= v >> 15; v *= 0x846ca68bU; v ^= v >> 16; return v % cap; } /* * pidset_rehash - resize/reinsert pidset contents. * @ps: pidset to grow. * @new_cap: requested slot capacity. * * Returns 0 on success, -1 on allocation failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int pidset_rehash(struct pidset *ps, size_t new_cap) { int *new_slots; size_t i; new_slots = malloc(new_cap * sizeof(*new_slots)); if (!new_slots) return -1; for (i = 0; i < new_cap; i++) new_slots[i] = PIDSET_EMPTY; for (i = 0; i < ps->cap; i++) { size_t pos; if (ps->slots[i] == PIDSET_EMPTY) continue; pos = pid_hash(ps->slots[i], new_cap); while (new_slots[pos] != PIDSET_EMPTY) pos = (pos + 1) % new_cap; new_slots[pos] = ps->slots[i]; } free(ps->slots); ps->slots = new_slots; ps->cap = new_cap; return 0; } /* * pidset_init - initialize pidset with empty hash table state. * @ps: pidset object to initialize. * * Returns 0 on success, -1 on allocation failure. * Side effects/assumptions: Allocates heap storage for @ps slots. */ static int pidset_init(struct pidset *ps) { ps->cap = 16; ps->used = 0; ps->slots = malloc(ps->cap * sizeof(*ps->slots)); if (!ps->slots) return -1; for (size_t i = 0; i < ps->cap; i++) ps->slots[i] = PIDSET_EMPTY; return 0; } /* * pidset_free - release pidset storage and reset fields. * @ps: pidset object to clear. * * Returns no value. * Side effects/assumptions: Frees heap memory referenced by @ps. */ static void pidset_free(struct pidset *ps) { free(ps->slots); ps->slots = NULL; ps->cap = 0; ps->used = 0; } /* * pidset_test_and_add - query/insert PID in dedup set. * @ps: pidset tracking seen process IDs. * @pid: process id to test and insert. * * Returns 1 if already present, 0 if newly inserted, -1 on failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int pidset_test_and_add(struct pidset *ps, int pid) { size_t pos; if ((ps->used + 1) * 10 >= ps->cap * 7) { if (pidset_rehash(ps, ps->cap * 2)) return -1; } pos = pid_hash(pid, ps->cap); while (ps->slots[pos] != PIDSET_EMPTY) { if (ps->slots[pos] == pid) return 1; pos = (pos + 1) % ps->cap; } ps->slots[pos] = pid; ps->used++; return 0; } /* * print_flag_nodes - render endpoint/process flag leaves under "flags" node. * @pfx_flags: tree prefix for flag child lines. * @width: wrap width for tree nodes. * @flags: bitmask of endpoint/process flags to print. * @priv_sev: privileged capability severity for colorizing that flag. * * Returns no value. * Side effects/assumptions: Writes formatted output to stdout. */ static void print_flag_nodes(const char *pfx_flags, int width, unsigned int flags, enum cap_severity priv_sev) { static const struct { unsigned int bit; const char *name; const char *color; } map[] = { { FLAG_PROXIMITY_PLANE, "proximity-plane", COLOR_YELLOW }, { FLAG_HYPERVISOR_PLANE, "hypervisor-plane", COLOR_YELLOW }, { FLAG_SSH_VSOCK_22, "ssh-on-vsock-port-22", NULL }, { FLAG_WILDCARD_BIND, "wildcard-bind", COLOR_YELLOW }, { FLAG_REUSEPORT, "reuseport", COLOR_YELLOW }, { FLAG_PRIVILEGED_CAPS, "privileged-caps", NULL }, }; size_t i; size_t n = 0; size_t printed = 0; char node[256]; for (i = 0; i < sizeof(map) / sizeof(map[0]); i++) if (flags & map[i].bit) n++; if (!n) { proc_tree_print_node(pfx_flags, 1, "(none)", width); return; } for (i = 0; i < sizeof(map) / sizeof(map[0]); i++) { const char *color = map[i].color; if (!(flags & map[i].bit)) continue; if (map[i].bit == FLAG_PRIVILEGED_CAPS) color = sev_color(priv_sev); if (use_color && color) snprintf(node, sizeof(node), "%s%s%s", color, map[i].name, COLOR_RESET); else snprintf(node, sizeof(node), "%s", map[i].name); printed++; proc_tree_print_node(pfx_flags, printed == n, node, width); } } /* * str_is_loopback - check whether textual @addr is loopback for @af. * @af: address family used to interpret @addr. * @addr: textual IPv4/IPv6 address to classify. * * Returns non-zero for loopback addresses, else 0. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int str_is_loopback(int af, const char *addr) { struct in_addr a4; struct in6_addr a6; if (af == AF_INET) { if (inet_pton(AF_INET, addr, &a4) != 1) return 0; return (ntohl(a4.s_addr) & 0xff000000U) == 0x7f000000U; } if (af == AF_INET6) { if (inet_pton(AF_INET6, addr, &a6) != 1) return 0; return IN6_IS_ADDR_LOOPBACK(&a6); } return 0; } /* * str_is_wildcard - check whether textual @addr is wildcard-any for @af. * @af: address family used to interpret @addr. * @addr: textual IPv4/IPv6 bind address to classify. * * Returns non-zero for 0.0.0.0/:: wildcard binds, else 0. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int str_is_wildcard(int af, const char *addr) { if (af == AF_INET) return strcmp(addr, "0.0.0.0") == 0; if (af == AF_INET6) return strcmp(addr, "::") == 0; return 0; } /* * str_is_multicast - check whether textual @addr is multicast for @af. * @af: address family used to interpret @addr. * @addr: textual IPv4/IPv6 bind address to classify. * * Returns non-zero for multicast addresses, else 0. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int str_is_multicast(int af, const char *addr) { struct in_addr a4; struct in6_addr a6; if (af == AF_INET) { if (inet_pton(AF_INET, addr, &a4) != 1) return 0; return IN_MULTICAST(ntohl(a4.s_addr)); } if (af == AF_INET6) { if (inet_pton(AF_INET6, addr, &a6) != 1) return 0; return IN6_IS_ADDR_MULTICAST(&a6); } return 0; } /* * find_iface - locate interface record by name in @m. * @m: model containing interface inventory. * @name: interface name key (borrowed, not owned). * * Returns a mutable iface pointer on match, or NULL if not found. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static struct iface_info *find_iface(struct model *m, const char *name) { size_t i; for (i = 0; i < m->ifaces_n; i++) if (strcmp(m->ifaces[i].name, name) == 0) return &m->ifaces[i]; return NULL; } /* * add_iface_addr - append one interface address if not already present. * @ifc: interface record to update (takes ownership of duplicated @addr). * @af: address family for @addr. * @addr: textual address to copy into @ifc. * * Returns 0 on success, -1 on allocation failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int add_iface_addr(struct iface_info *ifc, int af, const char *addr) { size_t i; for (i = 0; i < ifc->addrs_n; i++) { if (ifc->addrs[i].af == af && strcmp(ifc->addrs[i].addr, addr) == 0) return 0; } if (ifc->addrs_n == ifc->addrs_cap && vec_grow((void **)&ifc->addrs, &ifc->addrs_cap, sizeof(struct iface_addr))) return -1; ifc->addrs[ifc->addrs_n].af = af; ifc->addrs[ifc->addrs_n].addr = xstrdup(addr); if (!ifc->addrs[ifc->addrs_n].addr) return -1; ifc->addrs_n++; return 0; } /* * collect_interfaces - snapshot AF_INET/AF_INET6 iface addresses in @m. * @m: model populated from getifaddrs() in the current network namespace. * * Returns 0 on success, -1 on getifaddrs/allocation failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int collect_interfaces(struct model *m) { struct ifaddrs *ifa, *cur; char buf[INET6_ADDRSTRLEN]; struct iface_info *ifc; if (getifaddrs(&ifa) != 0) return -1; for (cur = ifa; cur; cur = cur->ifa_next) { if (!cur->ifa_name || !cur->ifa_addr) continue; if (cur->ifa_addr->sa_family != AF_INET && cur->ifa_addr->sa_family != AF_INET6) continue; ifc = find_iface(m, cur->ifa_name); if (!ifc) { if (m->ifaces_n == m->ifaces_cap && vec_grow((void **)&m->ifaces, &m->ifaces_cap, sizeof(struct iface_info))) goto fail; ifc = &m->ifaces[m->ifaces_n++]; memset(ifc, 0, sizeof(*ifc)); ifc->name = xstrdup(cur->ifa_name); if (!ifc->name) goto fail; } if (cur->ifa_addr->sa_family == AF_INET) { if (!inet_ntop(AF_INET, &((struct sockaddr_in *)cur->ifa_addr)->sin_addr, buf, sizeof(buf))) continue; } else { if (!inet_ntop(AF_INET6, &((struct sockaddr_in6 *)cur->ifa_addr)->sin6_addr, buf, sizeof(buf))) continue; } if (add_iface_addr(ifc, cur->ifa_addr->sa_family, buf)) goto fail; } freeifaddrs(ifa); return 0; fail: freeifaddrs(ifa); return -1; } /* * read_first_line - read and trim the first line from @path. * @path: procfs/sysfs-style file path to read. * * Returns a caller-owned string, or NULL on open/read/allocation failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static char *read_first_line(const char *path) { int fd; char *buf; ssize_t len; fd = open(path, O_RDONLY | O_CLOEXEC); if (fd < 0) return NULL; buf = malloc(1024); if (!buf) { close(fd); return NULL; } len = read(fd, buf, 1023); close(fd); if (len <= 0) { free(buf); return NULL; } buf[len] = 0; while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) buf[--len] = 0; return buf; } /* * extract_unit_from_cgroup - best-effort unit name lookup for @pid. * @pid: process ID whose /proc//cgroup is inspected. * * Returns a caller-owned unit/scope name, or NULL if not found/readable. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static char *extract_unit_from_cgroup(int pid) { char path[64], line[512]; FILE *f; snprintf(path, sizeof(path), "/proc/%d/cgroup", pid); f = fopen(path, "rte"); if (!f) return NULL; __fsetlocking(f, FSETLOCKING_BYCALLER); while (fgets(line, sizeof(line), f)) { char *s; s = strstr(line, ".service"); if (!s) s = strstr(line, ".scope"); if (s && !strstr(line, "system.slice")) continue; if (s) { while (s > line && *s != '/') s--; if (*s == '/') s++; char *e = s; while (*e && *e != '\n' && *e != '/') e++; *e = 0; fclose(f); return xstrdup(s); } } fclose(f); return NULL; } /* * caps_summary_for_pid - format capability summary for one process. * @pid: process ID inspected through libcap-ng APIs. * @privileged: out flag set when notable privileged effective caps exist. * @has_amb: out flag set when ambient capabilities are present. * @has_bnd: out flag set when bounding set entries are present. * * Returns caller-owned summary text; errors degrade to "(none)" style text. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static char *caps_summary_for_pid(int pid, int *privileged, int *has_amb, int *has_bnd, char **amb_list) { char out[4096]; char amb_out[1024]; char *amb_dst = amb_out; char *dst = out; size_t amb_left = sizeof(amb_out); size_t left = sizeof(out); int i, first = 1, amb_first = 1; capng_results_t c; *privileged = 0; *has_amb = 0; *has_bnd = 0; *amb_list = NULL; capng_clear(CAPNG_SELECT_ALL); capng_setpid(pid); if (capng_get_caps_process()) return xstrdup("(none)"); if (capng_have_capability(CAPNG_EFFECTIVE, CAP_SYS_ADMIN) || capng_have_capability(CAPNG_EFFECTIVE, CAP_SYS_PTRACE) || capng_have_capability(CAPNG_EFFECTIVE, CAP_DAC_READ_SEARCH) || capng_have_capability(CAPNG_EFFECTIVE, CAP_NET_ADMIN) || capng_have_capability(CAPNG_EFFECTIVE, CAP_NET_RAW)) *privileged = 1; c = capng_have_capabilities(CAPNG_SELECT_CAPS); if (c == CAPNG_FULL) { strncpy(out, "(full)", sizeof(out)); out[sizeof(out) - 1] = 0; } else if (c <= CAPNG_NONE) { strncpy(out, "(none)", sizeof(out)); out[sizeof(out) - 1] = 0; } else { *dst = 0; for (i = 0; i <= CAP_LAST_CAP; i++) { size_t n; size_t sep = first ? 0 : 2; if (!capng_have_capability(CAPNG_PERMITTED, i)) continue; const char *name = capng_capability_to_name(i); if (!name) continue; if (strncmp(name, "cap_", 4) == 0) name += 4; n = strlen(name); if (left <= sep + n) break; if (!first) { *dst++ = ','; *dst++ = ' '; left -= 2; } memcpy(dst, name, n); dst += n; left -= n; *dst = 0; first = 0; } if (out[0] == 0) { strncpy(out, "(none)", sizeof(out)); out[sizeof(out) - 1] = 0; } } if (capng_have_capabilities(CAPNG_SELECT_AMBIENT) > CAPNG_NONE) *has_amb = 1; if (*has_amb) { *amb_dst = 0; for (i = 0; i <= CAP_LAST_CAP; i++) { size_t n; size_t sep = amb_first ? 0 : 2; const char *name; if (!capng_have_capability(CAPNG_AMBIENT, i)) continue; name = capng_capability_to_name(i); if (!name) continue; if (strncmp(name, "cap_", 4) == 0) name += 4; n = strlen(name); if (amb_left <= sep + n) break; if (!amb_first) { *amb_dst++ = ','; *amb_dst++ = ' '; amb_left -= 2; } memcpy(amb_dst, name, n); amb_dst += n; amb_left -= n; *amb_dst = 0; amb_first = 0; } if (amb_out[0]) *amb_list = xstrdup(amb_out); } if (capng_have_capabilities(CAPNG_SELECT_BOUNDS) > CAPNG_NONE) *has_bnd = 1; return xstrdup(out); } /* * parse_status_defenses - read process hardening metadata into @d. * @pid: process ID whose procfs status/attr files are parsed. * @uid: process real UID, used for root/non-root interpretation. * @d: destination struct receiving caller-freed string fields. * @status: parsed /proc//status fields consumed for hardening decode. * * Missing fields are tolerated; function leaves best-effort defaults. * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void parse_status_defenses(int pid, uid_t uid, struct defense_info *d, const struct proc_status *status) { char path[64]; d->runs_as_nonroot = xstrdup(uid != 0 ? "yes" : "no"); d->no_new_privs = xstrdup("unknown"); d->seccomp = xstrdup("disabled"); d->lsm_label = NULL; if (status->seen_no_new_privs) { free(d->no_new_privs); d->no_new_privs = xstrdup(status->no_new_privs ? "yes" : "no"); } if (status->seen_seccomp) { free(d->seccomp); if (status->seccomp == 0) d->seccomp = xstrdup("disabled"); else if (status->seccomp == 1) d->seccomp = xstrdup("strict"); else d->seccomp = xstrdup("filter"); } snprintf(path, sizeof(path), "/proc/%d/attr/current", pid); d->lsm_label = read_first_line(path); if (sanitize_untrusted_owned(&d->lsm_label) < 0) { free(d->lsm_label); d->lsm_label = NULL; } } /* * add_process - collect process metadata and append it to @m. * @m: model taking ownership of the created process_info on success. * @pid: numeric process ID to read from /proc. * * Returns stored process pointer on success, or NULL on parse/allocation error. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static struct process_info *add_process(struct model *m, int pid) { char path[64]; char exepath[PATH_MAX]; ssize_t exelen; int has_amb = 0, has_bnd = 0; char *amb_list = NULL; struct proc_status status; struct process_info *p; if (proc_read_status(pid, &status) < 0 || !status.seen_uid) return NULL; p = calloc(1, sizeof(*p)); if (!p) { fprintf(stderr, "Out of memory\n"); return NULL; } p->pid = pid; p->uid = status.uid; p->comm = xstrdup(status.seen_name ? status.name : "?"); if (sanitize_untrusted_owned(&p->comm) < 0) goto fail; snprintf(path, sizeof(path), "/proc/%d/exe", pid); exelen = readlink(path, exepath, sizeof(exepath) - 1); if (exelen >= 0) { size_t deleted_len = strlen(" (deleted)"); exepath[exelen] = '\0'; if ((size_t)exelen > deleted_len && strcmp(exepath + exelen - deleted_len, " (deleted)") == 0) exepath[exelen - deleted_len] = '\0'; p->exe = xstrdup(exepath); if (sanitize_untrusted_owned(&p->exe) < 0) goto fail; } p->unit = extract_unit_from_cgroup(pid); if (sanitize_untrusted_owned(&p->unit) < 0) goto fail; p->caps = caps_summary_for_pid(pid, &p->has_privileged_caps, &has_amb, &has_bnd, &amb_list); p->ambient_caps = amb_list; p->ambient_present = has_amb; p->open_ended_bounding = has_bnd; parse_status_defenses(pid, status.uid, &p->defenses, &status); if (!p->comm || (exelen >= 0 && !p->exe) || !p->caps || (has_amb && !p->ambient_caps) || !p->defenses.runs_as_nonroot || !p->defenses.no_new_privs || !p->defenses.seccomp) goto fail; if (m->procs_n == m->procs_cap && vec_grow((void **)&m->procs, &m->procs_cap, sizeof(struct process_info *))) goto fail; m->procs[m->procs_n++] = p; return p; fail: free_process(p); return NULL; } /* * add_inode_proc - add inode->process ownership mapping into @m. * @m: model containing inode map storage. * @inode: socket inode key from procfs/netlink tables. * @p: process entry pointer that must remain valid for @m lifetime. * * Returns 0 on success, -1 on allocation failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int add_inode_proc(struct model *m, unsigned long inode, struct process_info *p) { size_t j; ssize_t idx; struct inode_proc *ip = NULL; idx = inode_hash_find(m, inode); if (idx >= 0) ip = &m->inode_map[idx]; if (!ip) { if (inode_hash_ensure_capacity(m) != 0) return -1; if (m->inode_n == m->inode_cap && vec_grow((void **)&m->inode_map, &m->inode_cap, sizeof(struct inode_proc))) return -1; ip = &m->inode_map[m->inode_n++]; memset(ip, 0, sizeof(*ip)); ip->inode = inode; inode_hash_insert(m, m->inode_n - 1); } for (j = 0; j < ip->n; j++) if (ip->procs[j]->pid == p->pid) return 0; if (ip->n == ip->cap && vec_grow((void **)&ip->procs, &ip->cap, sizeof(struct process_info *))) return -1; ip->procs[ip->n++] = p; return 0; } /* * dup_target_fd - duplicate one target process fd into this process. * @pid: process id owning @fdnum. * @fdnum: fd number inside @pid to duplicate. * * Uses pidfd_open + pidfd_getfd when supported by the running kernel. * Returns duplicated fd on success, or -1 when unsupported/inaccessible. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int dup_target_fd(int pid, int fdnum) { #if defined(__NR_pidfd_open) && defined(__NR_pidfd_getfd) static int warned_unavail; int pidfd; int dupfd; pidfd = syscall(__NR_pidfd_open, pid, 0); if (pidfd < 0) { if (errno == ENOSYS && !warned_unavail) { diag_dbg("pidfd_getfd unavailable; fd inspection disabled"); warned_unavail = 1; } return -1; } dupfd = syscall(__NR_pidfd_getfd, pidfd, fdnum, 0); if (dupfd < 0) { if (errno == ENOSYS && !warned_unavail) { diag_dbg("pidfd_getfd unavailable; fd inspection disabled"); warned_unavail = 1; } close(pidfd); return -1; } close(pidfd); return dupfd; #else (void)pid; (void)fdnum; return -1; #endif } /* * probe_reuseport_fd - probe SO_REUSEPORT on one duplicated socket fd. * @fd: socket fd already duplicated into this process. * * Returns 1/0 on successful getsockopt, or -1 when unsupported/inaccessible. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int probe_reuseport_fd(int fd) { int val = 0; socklen_t len = sizeof(val); int rc; rc = getsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &val, &len); if (rc < 0) return -1; return val ? 1 : 0; } /* * collect_proc_inodes - build inode ownership map from /proc//fd links. * @m: model receiving process entries and inode->process associations. * * This is best-effort and skips tasks/fds hidden by permissions or races. * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void collect_proc_inodes(struct model *m) { DIR *d; struct dirent *ent; d = opendir("/proc"); if (!d) return; while ((ent = readdir(d))) { int pid; DIR *fds; struct dirent *fdent; char fdpath[64]; struct process_info *p; if (ent->d_name[0] < '0' || ent->d_name[0] > '9') continue; pid = atoi(ent->d_name); if (pid <= 0) continue; p = add_process(m, pid); if (!p) continue; snprintf(fdpath, sizeof(fdpath), "/proc/%d/fd", pid); fds = opendir(fdpath); if (!fds) continue; while ((fdent = readdir(fds))) { char lpath[128], link[256], *s; ssize_t l; unsigned long inode; int fdnum; int dupfd; int reuseport; struct inode_proc *ip; if (fdent->d_name[0] == '.') continue; snprintf(lpath, sizeof(lpath), "%s/%s", fdpath, fdent->d_name); l = readlink(lpath, link, sizeof(link) - 1); if (l < 0) continue; link[l] = 0; /* * procfs may expose socket links in kernel-dependent formats. * Handle both common "socket:[inode]" and "[0000]:inode" forms. */ if (strncmp(link, "socket:[", 8) == 0) { s = link + 8; } else if (strncmp(link, "[0000]:", 7) == 0) { s = link + 7; } else { continue; } inode = strtoul(s, NULL, 10); if (!inode) continue; add_inode_proc(m, inode, p); fdnum = atoi(fdent->d_name); if (fdnum < 0) continue; dupfd = dup_target_fd(pid, fdnum); if (dupfd < 0) continue; /* * Probe SO_REUSEPORT here because this is the only stage with both * a live target socket and the owning process context; store the * result on inode_proc so endpoint projection can reuse it later. */ reuseport = probe_reuseport_fd(dupfd); close(dupfd); if (reuseport != 1) continue; ip = lookup_inode(m, inode); if (ip) ip->reuseport |= 1; } closedir(fds); } closedir(d); } /* * lookup_inode - locate inode ownership entry in @m. * @m: model containing inode ownership map. * @inode: socket inode key to resolve. * * Returns mutable map entry pointer, or NULL if inode is unknown. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static struct inode_proc *lookup_inode(struct model *m, unsigned long inode) { ssize_t idx; idx = inode_hash_find(m, inode); if (idx < 0) return NULL; return &m->inode_map[idx]; } /* * add_endpoint - add/merge one inet or packet endpoint in @m. * @m: model receiving endpoint data. * @proto/@bind/@ifname/@ifaddr: copied strings for endpoint identity. * @port/@plane: endpoint attributes for rendering/grouping. * @attrs: wildcard/reuseport flags carried from parser/projection stages. * @ip: inode-owner mapping whose process pointers are attached to endpoint. * * Returns 0 on success, -1 on allocation failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int add_endpoint(struct model *m, const char *proto, const char *bind, unsigned int port, enum plane_kind plane, const char *ifname, const char *ifaddr, const struct endpoint_attrs *attrs, struct inode_proc *ip) { size_t i, j; struct endpoint *e; char label[256]; if (strchr(bind, ':') && !str_is_bdaddr(bind)) snprintf(label, sizeof(label), "%s:[%s]:%u", proto, bind, port); else snprintf(label, sizeof(label), "%s:%s:%u", proto, bind, port); for (i = 0; i < m->eps_n; i++) { e = &m->eps[i]; if (strcmp(e->label, label) == 0 && strcmp(e->ifname, ifname) == 0 && strcmp(e->ifaddr, ifaddr) == 0) { e->reuseport |= attrs->reuseport; goto add_procs; } } if (m->eps_n == m->eps_cap && vec_grow((void **)&m->eps, &m->eps_cap, sizeof(struct endpoint))) return -1; e = &m->eps[m->eps_n]; memset(e, 0, sizeof(*e)); e->proto = xstrdup(proto); e->bind = xstrdup(bind); e->label = xstrdup(label); e->port = port; e->vsock_cid = 0; e->has_vsock = 0; e->plane = plane; e->ifname = xstrdup(ifname); e->ifaddr = xstrdup(ifaddr); e->wildcard_bind = attrs->wildcard; e->reuseport = attrs->reuseport; if (!e->proto || !e->bind || !e->label || !e->ifname || !e->ifaddr) { free(e->proto); free(e->bind); free(e->label); free(e->ifname); free(e->ifaddr); memset(e, 0, sizeof(*e)); return -1; } m->eps_n++; add_procs: for (j = 0; j < ip->n; j++) { size_t k; for (k = 0; k < e->procs_n; k++) if (e->procs[k]->pid == ip->procs[j]->pid) goto next; if (e->procs_n == e->procs_cap && vec_grow((void **)&e->procs, &e->procs_cap, sizeof(struct process_info *))) return -1; e->procs[e->procs_n++] = ip->procs[j]; next: ; } return 0; } #ifdef HAVE_NETCAP_VSOCK /* * add_vsock_endpoint - add/merge one VSOCK endpoint in @m. * @m: model receiving endpoint data. * @type: socket type label (stream/dgram/seqpacket) copied into model. * @cid/@port: source CID/port; @cid may be VMADDR_CID_ANY. * @ip: inode-owner mapping whose process pointers are attached to endpoint. * * Returns 0 on success, -1 on allocation failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int add_vsock_endpoint(struct model *m, const char *type, unsigned int cid, unsigned int port, struct inode_proc *ip) { size_t i, j; struct endpoint *e; char label[128]; char cidbuf[32]; if (cid == VMADDR_CID_ANY) strcpy(cidbuf, "ANY"); else snprintf(cidbuf, sizeof(cidbuf), "%u", cid); snprintf(label, sizeof(label), "%s:cid=%s:%u", type, cidbuf, port); for (i = 0; i < m->eps_n; i++) { e = &m->eps[i]; if (e->plane == PLANE_VSOCK && strcmp(e->label, label) == 0) goto add_procs; } if (m->eps_n == m->eps_cap && vec_grow((void **)&m->eps, &m->eps_cap, sizeof(struct endpoint))) return -1; e = &m->eps[m->eps_n]; memset(e, 0, sizeof(*e)); e->proto = xstrdup(type); e->bind = xstrdup(cidbuf); e->label = xstrdup(label); e->port = port; e->vsock_cid = cid; e->has_vsock = 1; e->plane = PLANE_VSOCK; e->ifname = xstrdup(""); e->ifaddr = xstrdup(""); e->reuseport = 0; if (!e->proto || !e->bind || !e->label || !e->ifname || !e->ifaddr) { free(e->proto); free(e->bind); free(e->label); free(e->ifname); free(e->ifaddr); memset(e, 0, sizeof(*e)); return -1; } m->eps_n++; add_procs: for (j = 0; j < ip->n; j++) { size_t k; for (k = 0; k < e->procs_n; k++) if (e->procs[k]->pid == ip->procs[j]->pid) goto next; if (e->procs_n == e->procs_cap && vec_grow((void **)&e->procs, &e->procs_cap, sizeof(struct process_info *))) return -1; e->procs[e->procs_n++] = ip->procs[j]; next: ; } return 0; } #endif /* * endpoint_to_ifaces - project one inet bind onto iface/address groupings. * @m: model containing iface inventory and endpoint lists. * @proto/@af/@bind/@port: socket identity from procfs/sock_diag. * @ip: inode-owner mapping used to attach owning processes. * * Wildcard binds expand across non-loopback ifaces in current netns. * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void endpoint_to_ifaces(struct model *m, const char *proto, int af, const char *bind, unsigned int port, int reuseport, struct inode_proc *ip) { size_t i, j; int wildcard = str_is_wildcard(af, bind); int multicast = str_is_multicast(af, bind); int matched = 0; struct endpoint_attrs attrs; attrs.wildcard = wildcard; attrs.reuseport = reuseport; for (i = 0; i < m->ifaces_n; i++) { struct iface_info *ifc = &m->ifaces[i]; for (j = 0; j < ifc->addrs_n; j++) { if (ifc->addrs[j].af != af) continue; if (wildcard) { /* * 0.0.0.0/:: listeners are treated as externally reachable; keep * loopback out of this expansion to avoid duplicate exposure rows. */ if (strcmp(ifc->name, "lo") == 0) continue; add_endpoint(m, proto, bind, port, PLANE_INET_EXTERNAL, ifc->name, ifc->addrs[j].addr, &attrs, ip); matched = 1; } else if (strcmp(ifc->addrs[j].addr, bind) == 0) { enum plane_kind plane = str_is_loopback(af, bind) ? PLANE_INET_LOOPBACK : PLANE_INET_EXTERNAL; add_endpoint(m, proto, bind, port, plane, ifc->name, ifc->addrs[j].addr, &attrs, ip); matched = 1; } } } if (!matched) add_endpoint(m, proto, bind, port, str_is_loopback(af, bind) ? PLANE_INET_LOOPBACK : PLANE_INET_EXTERNAL, str_is_loopback(af, bind) ? "lo" : (multicast ? "multicast/group" : "unknown"), bind, &attrs, ip); } /* * parse_inet_file - parse one procfs inet socket table into endpoints. * @m: model receiving endpoint mappings. * @path: procfs table path (tcp/udp/raw variants). * @proto: protocol label used in rendered endpoint names. * @af: address family used to decode local bind addresses. * * Non-listeners/unowned sockets are skipped; output is best-effort. * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void parse_inet_file(struct model *m, const char *path, const char *proto, int af) { FILE *f; char line[512]; int row = 0; f = fopen(path, "rte"); if (!f) return; __fsetlocking(f, FSETLOCKING_BYCALLER); while (fgets(line, sizeof(line), f)) { unsigned int lport, rport, state; unsigned long txq, rxq, tmr, when, retr, inode; int d, uid, timeout; char laddrh[96], raddrh[96], more[128]; char addr[INET6_ADDRSTRLEN]; struct inode_proc *ip; more[0] = 0; if (!row++) continue; if (sscanf(line, "%d: %95[0-9A-Fa-f]:%X %95[0-9A-Fa-f]:%X %X " "%lX:%lX %lX:%lX %lX %d %d %lu %127s", &d, laddrh, &lport, raddrh, &rport, &state, &txq, &rxq, &tmr, &when, &retr, &uid, &timeout, &inode, more) < 14) continue; if ((strcmp(proto, "tcp") == 0 || strcmp(proto, "tcp6") == 0) && state != 0x0A) continue; if ((strcmp(proto, "udp") == 0 || strcmp(proto, "udp6") == 0 || strcmp(proto, "udplite") == 0 || strcmp(proto, "udplite6") == 0) && lport == 0) continue; ip = lookup_inode(m, inode); if (!ip) continue; if (af == AF_INET) { struct in_addr v4; unsigned int host; if (sscanf(laddrh, "%8x", &host) != 1) continue; /* * procfs inet tables print IPv4 addresses as host- * order hex. Assigning directly to s_addr keeps the * bytes correct on both little- and big-endian systems. */ v4.s_addr = host; if (!inet_ntop(AF_INET, &v4, addr, sizeof(addr))) continue; } else { unsigned char bytes[16] = { 0 }; int i; int ok = 1; if (strlen(laddrh) != 32) continue; for (i = 0; i < 4; i++) { uint32_t host; uint32_t net; if (sscanf(laddrh + (i * 8), "%8x", &host) != 1) { ok = 0; break; } net = htonl(host); memcpy(bytes + (i * 4), &net, sizeof(net)); } if (!ok) continue; if (!inet_ntop(AF_INET6, bytes, addr, sizeof(addr))) continue; } endpoint_to_ifaces(m, proto, af, addr, lport, ip->reuseport, ip); } fclose(f); } /* * parse_packet_file - parse /proc/net/packet and add packet endpoints. * @m: model receiving packet-plane endpoint/process mappings. * * Visibility depends on current netns and procfs access permissions. * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void parse_packet_file(struct model *m) { FILE *f; char line[512], ifn[IF_NAMESIZE]; int row = 0; f = fopen("/proc/net/packet", "rte"); if (!f) return; __fsetlocking(f, FSETLOCKING_BYCALLER); while (fgets(line, sizeof(line), f)) { unsigned long sk, inode; unsigned int ref, type, proto, iface, r, rmem, uid; struct inode_proc *ip; struct endpoint_attrs attrs; char bind[64], addr[64], name[64]; if (!row++) continue; if (sscanf(line, "%lX %u %u %X %u %u %u %u %lu", &sk, &ref, &type, &proto, &iface, &r, &rmem, &uid, &inode) < 9) continue; ip = lookup_inode(m, inode); if (!ip) continue; if (!if_indextoname(iface, ifn)) strcpy(ifn, "unknown"); snprintf(bind, sizeof(bind), "::"); snprintf(addr, sizeof(addr), "ifindex:%u", iface); snprintf(name, sizeof(name), "packet"); attrs.wildcard = 0; attrs.reuseport = 0; add_endpoint(m, name, bind, proto, PLANE_PACKET, ifn, addr, &attrs, ip); } fclose(f); } #ifdef HAVE_NETCAP_VSOCK /* * parse_u32_hex_or_dec - parse @s as decimal or hexadecimal u32. * @s: numeric token from procfs/netlink text fields. * @out: destination value on successful parse. * * Returns 0 on success, -1 if @s is not a valid integer token. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int parse_u32_hex_or_dec(const char *s, unsigned int *out) { char *end; unsigned long v; int base = 10; const char *p; for (p = s; *p; p++) { if ((*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F')) { base = 16; break; } } if (strncmp(s, "0x", 2) == 0 || strncmp(s, "0X", 2) == 0) base = 16; if (base == 10 && strlen(s) > 3 && s[0] == '0') base = 16; v = strtoul(s, &end, base); if (end == s || *end) return -1; /* * The caller stores CIDs, ports, and similar procfs fields in u32s. * Reject oversized text values so they cannot wrap into a false match. */ if (v > UINT_MAX) return -1; *out = (unsigned int)v; return 0; } #endif /* * str_is_bdaddr - validate one string as a Bluetooth device address. * @s: text to classify. * * Returns non-zero for XX:XX:XX:XX:XX:XX tokens, else zero. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int str_is_bdaddr(const char *s) { size_t i; if (!s || strlen(s) != 17) return 0; for (i = 0; i < 17; i++) { char c = s[i]; if ((i + 1) % 3 == 0) { if (c != ':') return 0; continue; } if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) return 0; } return 1; } #ifdef HAVE_NETCAP_BLUETOOTH /* * bt_format_addr - render one controller address into XX:XX:XX:XX:XX:XX. * @bdaddr: kernel Bluetooth address to stringify. * @addr: destination buffer. * @addr_sz: size of @addr in bytes. * * Returns no value. * Side effects/assumptions: Writes a formatted controller address into @addr. */ static void bt_format_addr(const bdaddr_t *bdaddr, char *addr, size_t addr_sz) { snprintf(addr, addr_sz, "%02X:%02X:%02X:%02X:%02X:%02X", bdaddr->b[5], bdaddr->b[4], bdaddr->b[3], bdaddr->b[2], bdaddr->b[1], bdaddr->b[0]); } /* * bt_parse_adapter_dev_id - convert one sysfs adapter name into a device id. * @name: adapter name such as hci0. * @dev_id: destination device index. * * Returns 0 on success, -1 when @name is not an HCI adapter name. * Side effects/assumptions: Operates on in-memory data and does not change * system state. */ static int bt_parse_adapter_dev_id(const char *name, int *dev_id) { unsigned int id; char tail; if (sscanf(name, "hci%u%c", &id, &tail) != 1 || id > UINT16_MAX) return -1; *dev_id = (int)id; return 0; } /* * bt_load_adapters - populate the adapter cache from sysfs and HCI ioctls. * * Adapter names come from /sys/class/bluetooth. Each name is resolved to its * BD_ADDR with HCIGETDEVINFO so later procfs parsing can map a single visible * adapter onto HCI and RFCOMM sockets. If the HCI socket or any ioctl fails, * the cache remains empty and lookups fall back to "unknown". * Returns no value. * Side effects/assumptions: Reads sysfs and issues HCI ioctls; it does not * change kernel configuration. */ static void bt_load_adapters(void) { DIR *d; struct dirent *ent; struct bt_adapter_info tmp[BT_ADAPTER_CACHE_MAX]; size_t tmp_n = 0; int fd; if (bt_adapters_loaded) return; bt_adapters_loaded = 1; d = opendir("/sys/class/bluetooth"); if (!d) return; fd = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI); if (fd < 0) goto out; while ((ent = readdir(d))) { struct hci_dev_info di; int dev_id; if (ent->d_name[0] == '.') continue; if (tmp_n == BT_ADAPTER_CACHE_MAX) break; if (bt_parse_adapter_dev_id(ent->d_name, &dev_id)) continue; memset(&di, 0, sizeof(di)); di.dev_id = (uint16_t)dev_id; if (ioctl(fd, HCIGETDEVINFO, &di) < 0) goto fail; snprintf(tmp[tmp_n].name, sizeof(tmp[tmp_n].name), "%s", ent->d_name); tmp[tmp_n].dev_id = dev_id; bt_format_addr(&di.bdaddr, tmp[tmp_n].addr, sizeof(tmp[tmp_n].addr)); tmp_n++; } if (tmp_n) { memcpy(bt_adapters, tmp, tmp_n * sizeof(tmp[0])); bt_adapters_n = tmp_n; } close(fd); out: closedir(d); return; fail: close(fd); bt_adapters_n = 0; closedir(d); } /* * resolve_bt_adapter_name - map one HCI device index to an adapter name. * @dev_id: HCI device index. * @name: destination buffer for adapter name or "unknown". * @name_sz: size of @name in bytes. * * Returns 0 on match, -1 when the cache has no matching adapter. * Side effects/assumptions: Reads sysfs adapter cache but does not change * system state. */ static int resolve_bt_adapter_name(int dev_id, char *name, size_t name_sz) { size_t i; bt_load_adapters(); for (i = 0; i < bt_adapters_n; i++) { if (bt_adapters[i].dev_id != dev_id) continue; snprintf(name, name_sz, "%s", bt_adapters[i].name); return 0; } snprintf(name, name_sz, "%s", BT_ADAPTER_UNKNOWN); return -1; } /* * resolve_bt_adapter_addr - copy the cached controller address for @name. * @name: adapter name such as hci0. * @addr: destination buffer. * @addr_sz: size of @addr in bytes. * * Returns 0 on success, -1 when @name is unknown in the cache. * Side effects/assumptions: Reads sysfs adapter cache but does not change * system state. */ static int resolve_bt_adapter_addr(const char *name, char *addr, size_t addr_sz) { size_t i; bt_load_adapters(); for (i = 0; i < bt_adapters_n; i++) { if (strcmp(bt_adapters[i].name, name) != 0) continue; snprintf(addr, addr_sz, "%s", bt_adapters[i].addr); return 0; } snprintf(addr, addr_sz, "%s", BT_ADAPTER_UNKNOWN); return -1; } /* * bt_resolve_single_adapter - pick the only adapter when the cache is unique. * @name: destination buffer for adapter name. * @name_sz: size of @name in bytes. * @addr: destination buffer for adapter BD_ADDR, or "*". * @addr_sz: size of @addr in bytes. * * Current /proc/net/hci and /proc/net/rfcomm output does not identify the * bound adapter. When exactly one controller is available, use it; otherwise * report the adapter as hci? and the bind address as *. * Returns no value. * Side effects/assumptions: Reads the in-memory adapter cache and does not * change system state. */ static void bt_resolve_single_adapter(char *name, size_t name_sz, char *addr, size_t addr_sz) { bt_load_adapters(); if (bt_adapters_n == 1 && resolve_bt_adapter_name(bt_adapters[0].dev_id, name, name_sz) == 0 && resolve_bt_adapter_addr(name, addr, addr_sz) == 0) return; snprintf(name, name_sz, "%s", BT_ADAPTER_HEURISTIC); snprintf(addr, addr_sz, "%s", BT_ADDR_UNKNOWN); } /* * bt_add_endpoint - project one Bluetooth listener into the common endpoint model. * @m: model receiving Bluetooth endpoint/process mappings. * @proto: Bluetooth protocol label (rfcomm/hci). * @port: 0 because current Bluetooth procfs data does not expose channels. * @ifname: adapter name such as hci0 or hci?. * @ifaddr: adapter BD_ADDR or "*" when it cannot be resolved. * @ip: inode-owner mapping whose process pointers are attached to endpoint. * * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void bt_add_endpoint(struct model *m, const char *proto, unsigned int port, const char *ifname, const char *ifaddr, struct inode_proc *ip) { struct endpoint_attrs attrs = { 0, 0 }; add_endpoint(m, proto, ifaddr, port, PLANE_BLUETOOTH, ifname, ifaddr, &attrs, ip); } /* * parse_bluetooth_rfcomm - parse /proc/net/rfcomm into Bluetooth endpoints. * @m: model receiving Bluetooth RFCOMM listener mappings. * * The current kernel RFCOMM table exports only generic socket fields: * sk, RefCnt, Rmem, Wmem, User, Inode, Parent. Adapter, channel, and state * are not exposed, so any owned RFCOMM socket is reported with port 0. * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void parse_bluetooth_rfcomm(struct model *m) { FILE *f; char line[512]; f = fopen("/proc/net/rfcomm", "rte"); if (!f) return; __fsetlocking(f, FSETLOCKING_BYCALLER); while (fgets(line, sizeof(line), f)) { unsigned long sk, inode, parent; unsigned int refcnt, rmem, wmem, uid; char ifname[IF_NAMESIZE]; char ifaddr[32]; struct inode_proc *ip; if (strncmp(line, "sk", 2) == 0) continue; /* * /proc/net/rfcomm columns are: * sk RefCnt Rmem Wmem User Inode Parent */ if (sscanf(line, "%lx %u %u %u %u %lu %lu", &sk, &refcnt, &rmem, &wmem, &uid, &inode, &parent) != 7) continue; (void)sk; (void)refcnt; (void)rmem; (void)wmem; (void)uid; (void)parent; ip = lookup_inode(m, inode); if (!ip) continue; /* * /proc/net/rfcomm does not expose channel or state, so report all * owned RFCOMM sockets with port 0 on the best available adapter. */ bt_resolve_single_adapter(ifname, sizeof(ifname), ifaddr, sizeof(ifaddr)); bt_add_endpoint(m, "rfcomm", 0, ifname, ifaddr, ip); } fclose(f); } /* * parse_bluetooth_hci - parse /proc/net/hci into raw HCI management endpoints. * @m: model receiving Bluetooth HCI socket mappings. * * The current kernel HCI table exports only generic socket fields: * sk, RefCnt, Rmem, Wmem, User, Inode, Parent. Adapter identity is not * exposed, so the parser falls back to the single-adapter heuristic. * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void parse_bluetooth_hci(struct model *m) { FILE *f; char line[512]; f = fopen("/proc/net/hci", "rte"); if (!f) return; __fsetlocking(f, FSETLOCKING_BYCALLER); while (fgets(line, sizeof(line), f)) { unsigned long sk, inode, parent; unsigned int refcnt, rmem, wmem, uid; char ifname[IF_NAMESIZE]; char ifaddr[32]; struct inode_proc *ip; if (strncmp(line, "sk", 2) == 0) continue; /* * /proc/net/hci columns are: * sk RefCnt Rmem Wmem User Inode Parent */ if (sscanf(line, "%lx %u %u %u %u %lu %lu", &sk, &refcnt, &rmem, &wmem, &uid, &inode, &parent) != 7) continue; (void)sk; (void)refcnt; (void)rmem; (void)wmem; (void)uid; (void)parent; ip = lookup_inode(m, inode); if (!ip) continue; bt_resolve_single_adapter(ifname, sizeof(ifname), ifaddr, sizeof(ifaddr)); bt_add_endpoint(m, "hci", 0, ifname, ifaddr, ip); } fclose(f); } #else /* * parse_bluetooth_rfcomm - Bluetooth support stub when headers are unavailable. * @m: unused model pointer. * * Returns no value. */ static void parse_bluetooth_rfcomm(struct model *m) { (void)m; } /* * parse_bluetooth_hci - Bluetooth support stub when headers are unavailable. * @m: unused model pointer. * * Returns no value. */ static void parse_bluetooth_hci(struct model *m) { (void)m; } #endif #ifdef HAVE_NETCAP_VSOCK /* * parse_vsock_file - fallback VSOCK parser using /proc/net/vsock. * @m: model receiving parsed VSOCK endpoint/process mappings. * * Used when sock_diag support is unavailable or denied. * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void parse_vsock_file(struct model *m) { FILE *f; char line[512]; f = fopen("/proc/net/vsock", "rte"); if (!f) return; __fsetlocking(f, FSETLOCKING_BYCALLER); while (fgets(line, sizeof(line), f)) { char work[512]; char *tok[24]; char *save = NULL; char *local, *s, *sep; int tcnt = 0; unsigned long inode; unsigned int st, type, cid, port; struct inode_proc *ip; const char *kind; if (strstr(line, "Local") || strstr(line, "local") || strstr(line, "Num")) continue; snprintf(work, sizeof(work), "%s", line); s = strtok_r(work, " \t\n", &save); while (s && tcnt < (int)(sizeof(tok) / sizeof(tok[0]))) { tok[tcnt++] = s; s = strtok_r(NULL, " \t\n", &save); } if (tcnt < 5) continue; local = NULL; for (int i = 0; i < tcnt; i++) { if (strchr(tok[i], ':')) { local = tok[i]; break; } } if (!local) continue; sep = strchr(local, ':'); if (!sep) continue; *sep = '\0'; if (parse_u32_hex_or_dec(local, &cid) || parse_u32_hex_or_dec(sep + 1, &port)) continue; if (parse_u32_hex_or_dec(tok[tcnt - 2], &st)) continue; if (parse_u32_hex_or_dec(tok[tcnt - 3], &type)) continue; inode = strtoul(tok[tcnt - 1], NULL, 10); if (!inode) continue; if (type == SOCK_STREAM) { if (st != 0x0A) continue; kind = "stream"; } else if (type == SOCK_SEQPACKET) { if (st != 0x0A) continue; kind = "seqpacket"; } else if (type == SOCK_DGRAM) { if (port == 0) continue; kind = "dgram"; } else { continue; } ip = lookup_inode(m, inode); if (!ip) continue; add_vsock_endpoint(m, kind, cid, port, ip); } fclose(f); } /* * vsock_type_to_name - map VSOCK socket type to display label. * @type: SOCK_* type value from kernel socket metadata. * * Returns static string label, or NULL for unknown/unsupported type. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static const char *vsock_type_to_name(unsigned int type) { if (type == SOCK_STREAM) return "stream"; if (type == SOCK_DGRAM) return "dgram"; if (type == SOCK_SEQPACKET) return "seqpacket"; return NULL; } #ifdef HAVE_LINUX_VM_SOCKETS_DIAG_H /* * parse_vsock_diag_messages - consume VSOCK sock_diag dump replies. * @m: model receiving VSOCK endpoint/process mappings. * @fd: open NETLINK_SOCK_DIAG socket with pending VSOCK responses. * * Returns 0 on NLMSG_DONE, or -1 on malformed/error netlink messages. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int parse_vsock_diag_messages(struct model *m, int fd) { char buf[8192]; ssize_t len; while (1) { len = recv(fd, buf, sizeof(buf), 0); if (len < 0) { if (errno == EINTR) continue; return -1; } if (len == 0) return -1; struct nlmsghdr *nlh; unsigned int rem; if (len > UINT_MAX) return -1; rem = (unsigned int)len; for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, rem); nlh = NLMSG_NEXT(nlh, rem)) { struct vsock_diag_msg *r; struct inode_proc *ip; const char *kind; if (nlh->nlmsg_type == NLMSG_DONE) return 0; if (nlh->nlmsg_type == NLMSG_ERROR) { struct nlmsgerr *e; if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*e))) return -1; e = NLMSG_DATA(nlh); if (e->error == 0) continue; errno = -e->error; return -1; } if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*r))) continue; r = NLMSG_DATA(nlh); if (r->vdiag_family != AF_VSOCK) continue; kind = vsock_type_to_name(r->vdiag_type); if (!kind) continue; if ((r->vdiag_type == SOCK_STREAM || r->vdiag_type == SOCK_SEQPACKET) && r->vdiag_state != TCP_LISTEN) continue; if (r->vdiag_type == SOCK_DGRAM && r->vdiag_src_port == 0) continue; ip = lookup_inode(m, r->vdiag_ino); if (!ip) continue; diag_dbg("vsock type=%u state=%u src=%u:%u dst=%u:%u ino=%u", r->vdiag_type, r->vdiag_state, r->vdiag_src_cid, r->vdiag_src_port, r->vdiag_dst_cid, r->vdiag_dst_port, r->vdiag_ino); add_vsock_endpoint(m, kind, r->vdiag_src_cid, r->vdiag_src_port, ip); } } } /* * parse_vsock_diag - request VSOCK listener dump via sock_diag netlink. * @m: model receiving parsed VSOCK endpoint/process mappings. * * Returns 0 on success, -1 with errno on permission/support/socket errors. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int parse_vsock_diag(struct model *m) { struct { struct nlmsghdr nlh; struct vsock_diag_req req; } req; struct sockaddr_nl sa; int fd; int rc = -1; fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG); if (fd < 0) return -1; memset(&req, 0, sizeof(req)); req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(req.req)); req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY; req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; req.req.sdiag_family = AF_VSOCK; req.req.sdiag_protocol = 0; req.req.vdiag_states = ~0U; memset(&sa, 0, sizeof(sa)); sa.nl_family = AF_NETLINK; sa.nl_pid = 0; if (sendto(fd, &req, req.nlh.nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa)) < 0) goto out; rc = parse_vsock_diag_messages(m, fd); out: close(fd); return rc; } #else /* * parse_vsock_diag - unsupported-build stub for VSOCK sock_diag path. * @m: unused model pointer. * * Returns -1 and sets errno=EOPNOTSUPP. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int parse_vsock_diag(struct model *m) { (void)m; errno = EOPNOTSUPP; return -1; } #endif #endif /* * parse_diag_messages - consume inet sock_diag replies for @proto/@af. * @m: model receiving parsed endpoint mappings. * @fd: open NETLINK_SOCK_DIAG socket with pending responses. * @proto: requested protocol (SCTP/DCCP). * @af: requested address family (AF_INET/AF_INET6). * * Returns 0 on NLMSG_DONE, or -1 on malformed/error netlink messages. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int parse_diag_messages(struct model *m, int fd, int proto, int af) { char buf[8192]; ssize_t len; while (1) { len = recv(fd, buf, sizeof(buf), 0); if (len < 0) { if (errno == EINTR) continue; return -1; } if (len == 0) return -1; diag_dbg("recv proto=%d af=%d len=%zd", proto, af, len); struct nlmsghdr *nlh; unsigned int rem; if (len > UINT_MAX) return -1; rem = (unsigned int)len; for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, rem); nlh = NLMSG_NEXT(nlh, rem)) { struct inet_diag_msg *r; char addr[INET6_ADDRSTRLEN]; unsigned int port; struct inode_proc *ip; if (nlh->nlmsg_type == NLMSG_DONE) return 0; if (nlh->nlmsg_type == NLMSG_ERROR) { struct nlmsgerr *e; if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*e))) return -1; e = NLMSG_DATA(nlh); if (e->error == 0) continue; errno = -e->error; diag_dbg("error proto=%d af=%d err=%d", proto, af, -e->error); return -1; } if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*r))) continue; r = NLMSG_DATA(nlh); ip = lookup_inode(m, r->idiag_inode); if (!ip) continue; port = ntohs(r->id.idiag_sport); if (!port) continue; if (af == AF_INET) { if (!inet_ntop(AF_INET, r->id.idiag_src, addr, sizeof(addr))) continue; } else { if (!inet_ntop(AF_INET6, r->id.idiag_src, addr, sizeof(addr))) continue; } endpoint_to_ifaces(m, proto == IPPROTO_SCTP ? "sctp" : "dccp", af, addr, port, ip->reuseport, ip); } } } /* * parse_diag_for_proto_af - issue one inet sock_diag listener dump request. * @m: model receiving parsed endpoint mappings. * @proto: protocol selector for the request. * @af: address family selector for the request. * * Best-effort helper; failures are tolerated by callers. * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void parse_diag_for_proto_af(struct model *m, int proto, int af) { struct { struct nlmsghdr nlh; struct inet_diag_req_v2 req; } req; struct sockaddr_nl sa; int fd; fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG); if (fd < 0) return; memset(&req, 0, sizeof(req)); req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(req.req)); req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY; req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; req.req.sdiag_family = af; req.req.sdiag_protocol = proto; req.req.idiag_states = 1U << TCP_LISTEN; memset(&sa, 0, sizeof(sa)); sa.nl_family = AF_NETLINK; sa.nl_pid = 0; if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) goto out; diag_dbg("send proto=%d af=%d len=%u", proto, af, req.nlh.nlmsg_len); if (send(fd, &req, req.nlh.nlmsg_len, 0) < 0) goto out; parse_diag_messages(m, fd, proto, af); out: close(fd); } /* * parse_diag_listeners - collect SCTP/DCCP listeners via sock_diag. * @m: model receiving discovered listener endpoints. * * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void parse_diag_listeners(struct model *m) { parse_diag_for_proto_af(m, IPPROTO_SCTP, AF_INET); parse_diag_for_proto_af(m, IPPROTO_SCTP, AF_INET6); parse_diag_for_proto_af(m, IPPROTO_DCCP, AF_INET); parse_diag_for_proto_af(m, IPPROTO_DCCP, AF_INET6); } /* * collect_endpoints - gather all endpoint classes into @m. * @m: model receiving inet, diag, packet, Bluetooth, and vsock mappings. * * Data source is current netns procfs/netlink visibility. * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void collect_endpoints(struct model *m) { parse_inet_file(m, "/proc/net/tcp", "tcp", AF_INET); parse_inet_file(m, "/proc/net/tcp6", "tcp6", AF_INET6); parse_inet_file(m, "/proc/net/udp", "udp", AF_INET); parse_inet_file(m, "/proc/net/udp6", "udp6", AF_INET6); parse_inet_file(m, "/proc/net/udplite", "udplite", AF_INET); parse_inet_file(m, "/proc/net/udplite6", "udplite6", AF_INET6); parse_inet_file(m, "/proc/net/raw", "raw", AF_INET); parse_inet_file(m, "/proc/net/raw6", "raw6", AF_INET6); parse_diag_listeners(m); parse_packet_file(m); #ifdef HAVE_NETCAP_VSOCK if (parse_vsock_diag(m) < 0) { diag_dbg("vsock diag unavailable (%s), falling back to /proc", strerror(errno)); parse_vsock_file(m); } #endif parse_bluetooth_rfcomm(m); parse_bluetooth_hci(m); } /* * endpoint_cmp - qsort comparator for stable endpoint grouping. * @a: pointer to first endpoint element. * @b: pointer to second endpoint element. * * Sort order is plane, interface name, interface address, then label. * Returns negative/zero/positive qsort ordering result. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int endpoint_cmp(const void *a, const void *b) { const struct endpoint *ea = a, *eb = b; /* * Sort by plane for single-pass render grouping, then interface/protocol, * then bind (wildcards first), then port, then ifaddr/label for stability. */ if (ea->plane != eb->plane) return ea->plane - eb->plane; if (strcmp(ea->ifname, eb->ifname) != 0) return strcmp(ea->ifname, eb->ifname); if (strcmp(ea->proto, eb->proto) != 0) return strcmp(ea->proto, eb->proto); if (bind_sort_cmp(ea->bind, eb->bind) != 0) return bind_sort_cmp(ea->bind, eb->bind); if (ea->port != eb->port) return ea->port < eb->port ? -1 : 1; if (strcmp(ea->ifaddr, eb->ifaddr) != 0) return strcmp(ea->ifaddr, eb->ifaddr); return strcmp(ea->label, eb->label); } /* * format_bind_node - normalize bind text for tree display. * @dst: output buffer receiving display text. * @dst_sz: size of @dst in bytes. * @bind: endpoint bind address to format. * * Returns no value. * Side effects/assumptions: Writes formatted bind label into @dst. */ static void format_bind_node(char *dst, size_t dst_sz, const char *bind) { if (strcmp(bind, "0.0.0.0") == 0 || strcmp(bind, "::") == 0) snprintf(dst, dst_sz, "*"); else if (strchr(bind, ':') && !str_is_bdaddr(bind)) snprintf(dst, dst_sz, "[%s]", bind); else snprintf(dst, dst_sz, "%s", bind); } /* * bind_sort_cmp - compare bind addresses with wildcard-first ordering. * @a: first bind address. * @b: second bind address. * * Returns negative/zero/positive ordering suitable for qsort tie-breaks. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static int bind_sort_cmp(const char *a, const char *b) { int a_star = strcmp(a, "0.0.0.0") == 0 || strcmp(a, "::") == 0 || strcmp(a, "*") == 0; int b_star = strcmp(b, "0.0.0.0") == 0 || strcmp(b, "::") == 0 || strcmp(b, "*") == 0; if (a_star != b_star) return a_star ? -1 : 1; return strcmp(a, b); } /* * render_tree_process_details - emit one process subtree under an endpoint. * @prefix: parent tree prefix for process node. * @is_last: non-zero when process node is final sibling. * @p: process metadata to render. * @e: endpoint context supplying endpoint-derived flags. * @width: display width used for wrapping rendered nodes. * * Returns no value. * Side effects/assumptions: Writes formatted tree output to stdout. */ static void render_tree_process_details(const char *prefix, int is_last, struct process_info *p, const struct endpoint *e, int width) { char pfx_child[256], pfx_def[256], pfx_flags[256]; char line[4096]; const char *def_nodes[8]; char def_buf[8][512]; size_t def_n = 0; size_t ai; unsigned int flags = 0; enum cap_severity priv_sev = caps_worst_severity(p->caps); snprintf(line, sizeof(line), "%s (pid=%d uid=%u%s%s%s%s)", p->comm, p->pid, (unsigned int)p->uid, p->exe ? " exe=" : "", p->exe ? p->exe : "", p->unit ? " unit=" : "", p->unit ? p->unit : ""); proc_tree_print_node(prefix, is_last, line, width); proc_tree_build_child_prefix(pfx_child, sizeof(pfx_child), prefix, is_last); if (strcmp(p->caps, "(full)") == 0 || strcmp(p->caps, "(none)") == 0) { const char *c = strcmp(p->caps, "(full)") == 0 ? COLOR_ORANGE : COLOR_GREEN; if (use_color) snprintf(line, sizeof(line), "caps: %s%s%s", c, p->caps, COLOR_RESET); else snprintf(line, sizeof(line), "caps: %s", p->caps); } else { char capsbuf[3072] = ""; char *tmp = xstrdup(p->caps); char *save = NULL; char *tok; int first = 1; if (!tmp) { snprintf(line, sizeof(line), "caps: %s", p->caps); } else { for (tok = strtok_r(tmp, ",", &save); tok; tok = strtok_r(NULL, ",", &save)) { char part[256]; char *t = tok; enum cap_severity sev; while (*t == ' ') t++; sev = cap_name_severity(t); if (use_color && sev_color(sev)) snprintf(part, sizeof(part), "%s%s%s", sev_color(sev), t, COLOR_RESET); else snprintf(part, sizeof(part), "%s", t); if (!first) strncat(capsbuf, ", ", sizeof(capsbuf)-strlen(capsbuf)-1); strncat(capsbuf, part, sizeof(capsbuf)-strlen(capsbuf)-1); first = 0; } free(tmp); if (p->ambient_present) { if (use_color) strncat(capsbuf, " [\033[38;5;208mambient-present\033[0m]", sizeof(capsbuf)-strlen(capsbuf)-1); else strncat(capsbuf, " [ambient-present]", sizeof(capsbuf)-strlen(capsbuf)-1); } if (p->open_ended_bounding) { if (use_color && strcmp(p->caps, "(full)") != 0 && caps_contains_token(p->caps, "setpcap")) strncat(capsbuf, " [\033[38;5;208mopen-ended-bounding\033[0m]", sizeof(capsbuf) - strlen(capsbuf) - 1); else strncat(capsbuf, " [open-ended-bounding]", sizeof(capsbuf) - strlen(capsbuf) - 1); } snprintf(line, sizeof(line), "caps: %s", capsbuf); } } proc_tree_print_node(pfx_child, 0, line, width); if (p->ambient_caps) { if (use_color) snprintf(line, sizeof(line), "ambient: %s%s%s", COLOR_ORANGE, p->ambient_caps, COLOR_RESET); else snprintf(line, sizeof(line), "ambient: %s", p->ambient_caps); proc_tree_print_node(pfx_child, 0, line, width); } snprintf(def_buf[def_n], sizeof(def_buf[def_n]), DEFENSES_RUNS_AS_KEY ": %s%s%s", strcmp(p->defenses.runs_as_nonroot, "yes") == 0 && use_color ? COLOR_GREEN : (strcmp(p->defenses.runs_as_nonroot, "no") == 0 && use_color ? COLOR_YELLOW : ""), p->defenses.runs_as_nonroot, use_color && (strcmp(p->defenses.runs_as_nonroot, "yes") == 0 || strcmp(p->defenses.runs_as_nonroot, "no") == 0) ? COLOR_RESET : ""); def_nodes[def_n] = def_buf[def_n]; def_n++; if (strcmp(p->defenses.no_new_privs, "yes") == 0 && use_color) snprintf(def_buf[def_n], sizeof(def_buf[def_n]), "no_new_privs: %syes%s", COLOR_GREEN, COLOR_RESET); else if (strcmp(p->defenses.no_new_privs, "no") == 0 && use_color) snprintf(def_buf[def_n], sizeof(def_buf[def_n]), "no_new_privs: %sno%s", COLOR_YELLOW, COLOR_RESET); else snprintf(def_buf[def_n], sizeof(def_buf[def_n]), "no_new_privs: %s", p->defenses.no_new_privs); def_nodes[def_n] = def_buf[def_n]; def_n++; if ((strcmp(p->defenses.seccomp, "filter") == 0 || strcmp(p->defenses.seccomp, "strict") == 0) && use_color) snprintf(def_buf[def_n], sizeof(def_buf[def_n]), "seccomp: %s%s%s", COLOR_GREEN, p->defenses.seccomp, COLOR_RESET); else if (strcmp(p->defenses.seccomp, "disabled") == 0 && use_color) snprintf(def_buf[def_n], sizeof(def_buf[def_n]), "seccomp: %sdisabled%s", COLOR_YELLOW, COLOR_RESET); else snprintf(def_buf[def_n], sizeof(def_buf[def_n]), "seccomp: %s", p->defenses.seccomp); def_nodes[def_n] = def_buf[def_n]; def_n++; if (p->defenses.lsm_label) { if (use_color && strstr(p->defenses.lsm_label, "unconfined_t")) snprintf(def_buf[def_n], sizeof(def_buf[def_n]), "lsm: %s%s%s", COLOR_ORANGE, p->defenses.lsm_label, COLOR_RESET); else if (use_color && p->defenses.lsm_label[0]) snprintf(def_buf[def_n], sizeof(def_buf[def_n]), "lsm: %s%s%s", COLOR_GREEN, p->defenses.lsm_label, COLOR_RESET); else snprintf(def_buf[def_n], sizeof(def_buf[def_n]), "lsm: %s", p->defenses.lsm_label); def_nodes[def_n] = def_buf[def_n]; def_n++; } proc_tree_print_node(pfx_child, 0, "defenses", width); proc_tree_build_child_prefix(pfx_def, sizeof(pfx_def), pfx_child, 0); for (ai = 0; ai < def_n; ai++) proc_tree_print_node(pfx_def, ai + 1 == def_n, def_nodes[ai], width); if (e->plane == PLANE_VSOCK) { flags |= FLAG_HYPERVISOR_PLANE; if (e->port == 22) flags |= FLAG_SSH_VSOCK_22; } else { if (e->plane == PLANE_BLUETOOTH) flags |= FLAG_PROXIMITY_PLANE; if (e->wildcard_bind) flags |= FLAG_WILDCARD_BIND; if (e->reuseport) flags |= FLAG_REUSEPORT; } if (p->has_privileged_caps) flags |= FLAG_PRIVILEGED_CAPS; proc_tree_print_node(pfx_child, 1, "flags", width); proc_tree_build_child_prefix(pfx_flags, sizeof(pfx_flags), pfx_child, 1); print_flag_nodes(pfx_flags, width, flags, priv_sev); } /* * render_json_process - emit one process object inside endpoint JSON arrays. * @p: process metadata record to serialize. * @ep: endpoint context contributing endpoint-related flags. * @indent: indentation prefix already prepared by caller. * * Returns no value. * Side effects/assumptions: Writes JSON fragments to stdout. */ static void render_json_process(struct process_info *p, const struct endpoint *ep, const char *indent) { int firstf = 1; printf("%s{\"comm\": ", indent); json_escape(p->comm); if (p->exe) { printf(", \"exe\": "); json_escape(p->exe); } printf(", \"pid\": %d, \"uid\": %u", p->pid, (unsigned int)p->uid); if (p->unit) { printf(", \"unit\": "); json_escape(p->unit); } printf(", \"caps\": "); json_escape(p->caps); printf(", \"ambient_present\": %s", p->ambient_present ? "true" : "false"); if (p->ambient_caps) { printf(", \"ambient_caps\": "); json_escape(p->ambient_caps); } printf(", \"open_ended_bounding\": %s", p->open_ended_bounding ? "true" : "false"); printf(", \"defenses\": {\"" DEFENSES_RUNS_AS_KEY "\": "); json_escape(p->defenses.runs_as_nonroot); printf(", \"no_new_privs\": "); json_escape(p->defenses.no_new_privs); printf(", \"seccomp\": "); json_escape(p->defenses.seccomp); if (p->defenses.lsm_label) { printf(", \"lsm\": "); json_escape(p->defenses.lsm_label); } printf("}, \"flags\": ["); if (ep->plane == PLANE_VSOCK) { json_escape("hypervisor-plane"); firstf = 0; if (ep->port == 22) { printf(", "); json_escape("ssh-on-vsock-port-22"); } } else { if (ep->plane == PLANE_BLUETOOTH) { json_escape("proximity-plane"); firstf = 0; } if (ep->wildcard_bind) { if (!firstf) printf(", "); json_escape("wildcard-bind"); firstf = 0; } if (ep->reuseport) { if (!firstf) printf(", "); json_escape("reuseport"); firstf = 0; } } if (p->has_privileged_caps) { if (!firstf) printf(", "); json_escape("privileged-caps"); } printf("]}"); } /* * render_tree - print human-readable advanced report as a tree. * @m: model to render; endpoint array is sorted in place before printing. * * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void render_tree(struct model *m) { size_t i; int planes[PLANE_COUNT]; size_t plane_n = 0; int width = proc_output_width(); if (m->eps_n > 1) qsort(m->eps, m->eps_n, sizeof(struct endpoint), endpoint_cmp); for (i = 0; i < PLANE_COUNT; i++) { size_t j; for (j = 0; j < m->eps_n; j++) { if (m->eps[j].plane == (enum plane_kind)i) { planes[plane_n++] = i; break; } } } for (i = 0; i < plane_n; i++) { /* Tree level: plane (INET external/loopback, packet, Bluetooth, vsock). */ int plane = planes[i]; int plane_last = (i + 1 == plane_n); char pfx_plane[256] = ""; char pfx_iface[256]; const char *plane_name = plane == PLANE_INET_EXTERNAL ? "INET (external)" : plane == PLANE_INET_LOOPBACK ? "INET (loopback)" : plane == PLANE_PACKET ? PLANE_PACKET_NAME : plane == PLANE_BLUETOOTH ? PLANE_BLUETOOTH_NAME : "VSOCK"; size_t j = 0; proc_tree_print_node(pfx_plane, plane_last, plane_name, width); proc_tree_build_child_prefix(pfx_iface, sizeof(pfx_iface), pfx_plane, plane_last); if (plane == PLANE_VSOCK) { /* * VSOCK endpoints are not projected onto interfaces, so render * them directly under the plane. */ for (j = 0; j < m->eps_n; j++) { struct endpoint *e = &m->eps[j]; char pfx_proc[256]; int ep_last; size_t k; if (e->plane != (enum plane_kind)plane) continue; ep_last = 1; for (size_t n = j + 1; n < m->eps_n; n++) { if (m->eps[n].plane == (enum plane_kind)plane) { ep_last = 0; break; } } proc_tree_print_node(pfx_iface, ep_last, e->label, width); proc_tree_build_child_prefix(pfx_proc, sizeof(pfx_proc), pfx_iface, ep_last); for (k = 0; k < e->procs_n; k++) { struct process_info *p = e->procs[k]; int proc_last = (k + 1 == e->procs_n); render_tree_process_details(pfx_proc, proc_last, p, e, width); } } continue; } while (j < m->eps_n) { /* Tree level: interface grouping within the current plane. */ size_t iface_start, iface_end; char iface_line[160]; char pfx_iface_child[256]; int iface_last; if (m->eps[j].plane != (enum plane_kind)plane) { j++; continue; } iface_start = j; iface_end = j + 1; while (iface_end < m->eps_n && m->eps[iface_end].plane == (enum plane_kind)plane && strcmp(m->eps[iface_end].ifname, m->eps[iface_start].ifname) == 0) iface_end++; iface_last = 1; if (iface_end < m->eps_n && m->eps[iface_end].plane == (enum plane_kind)plane) iface_last = 0; snprintf(iface_line, sizeof(iface_line), "%s", m->eps[iface_start].ifname); proc_tree_print_node(pfx_iface, iface_last, iface_line, width); proc_tree_build_child_prefix(pfx_iface_child, sizeof(pfx_iface_child), pfx_iface, iface_last); { char pfx_proto_root[256]; snprintf(pfx_proto_root, sizeof(pfx_proto_root), "%s", pfx_iface_child); for (j = iface_start; j < iface_end; ) { /* Tree level: protocol grouping on this interface. */ size_t proto_start = j, proto_end; char pfx_bind[256]; int proto_last; proto_end = j + 1; while (proto_end < iface_end && strcmp(m->eps[proto_end].proto, m->eps[proto_start].proto) == 0) proto_end++; proto_last = (proto_end == iface_end); /* Highlight higher-risk raw/packet protocol families. */ if (use_color && (strcmp(m->eps[proto_start].proto, "raw") == 0 || strcmp(m->eps[proto_start].proto, "raw6") == 0 || strcmp(m->eps[proto_start].proto, "packet") == 0 || strcmp(m->eps[proto_start].proto, "hci") == 0)) { char pbuf[64]; snprintf(pbuf, sizeof(pbuf), "%s%s%s", COLOR_YELLOW, m->eps[proto_start].proto, COLOR_RESET); proc_tree_print_node(pfx_proto_root, proto_last, pbuf, width); } else { proc_tree_print_node(pfx_proto_root, proto_last, m->eps[proto_start].proto, width); } proc_tree_build_child_prefix(pfx_bind, sizeof(pfx_bind), pfx_proto_root, proto_last); { size_t bi = proto_start; while (bi < proto_end) { /* Tree level: bind address (wildcard/specific). */ size_t bind_start = bi; size_t bind_end; char bind_line[128], pfx_port[256]; int bind_last; bind_end = bi + 1; while (bind_end < proto_end && strcmp(m->eps[bind_end].bind, m->eps[bind_start].bind) == 0) bind_end++; bind_last = (bind_end == proto_end); format_bind_node(bind_line, sizeof(bind_line), m->eps[bind_start].bind); proc_tree_print_node(pfx_bind, bind_last, bind_line, width); proc_tree_build_child_prefix(pfx_port, sizeof(pfx_port), pfx_bind, bind_last); for (bi = bind_start; bi < bind_end; ) { /* Tree level: port number under each bind. */ size_t port_start = bi; size_t port_end; char pfx_proc[256], port_line[64]; int port_last; size_t k; struct pidset seen; port_end = bi + 1; while (port_end < bind_end && m->eps[port_end].port == m->eps[port_start].port) port_end++; port_last = (port_end == bind_end); snprintf(port_line, sizeof(port_line), "%u", m->eps[port_start].port); proc_tree_print_node(pfx_port, port_last, port_line, width); proc_tree_build_child_prefix(pfx_proc, sizeof(pfx_proc), pfx_port, port_last); if (pidset_init(&seen)) { bi = port_end; continue; } for (k = port_start; k < port_end; k++) { /* Tree level: process details under the current port. */ struct endpoint *e = &m->eps[k]; size_t pi; for (pi = 0; pi < e->procs_n; pi++) { int seen_rc; struct process_info *p = e->procs[pi]; int proc_last; /* * Deduplicate processes that appear under multiple * endpoints sharing this grouped port. */ seen_rc = pidset_test_and_add(&seen, p->pid); if (seen_rc) continue; proc_last = (k + 1 == port_end) && (pi + 1 == e->procs_n); render_tree_process_details(pfx_proc, proc_last, p, e, width); } } pidset_free(&seen); bi = port_end; } } } j = proto_end; } } j = iface_end; } } } /* * render_interfaces_text - print interface inventory in human-readable form. * @m: model whose interface inventory is rendered. * * Returns no value. */ static void render_interfaces_text(struct model *m) { size_t i; sort_interfaces(m); for (i = 0; i < m->ifaces_n; i++) printf("%s\n", m->ifaces[i].name); } /* * json_escape - write @s as a quoted JSON string to stdout. * @s: UTF-8/text string to emit as one JSON string literal. * * Control characters and quotes are escaped; caller handles separators. * Returns no value. * Side effects/assumptions: Writes to stdout and may read procfs/netns * state indirectly via caller-supplied model-derived strings. */ static void json_escape(const char *s) { const unsigned char *p = (const unsigned char *)s; putchar('"'); for (; *p; p++) { if (*p == '"') fputs("\\\"", stdout); else if (*p == '\\') fputs("\\\\", stdout); else if (*p < 0x20) printf("\\u%04x", *p); else putchar(*p); } putchar('"'); } /* * render_json - print machine-readable advanced report JSON to stdout. * @m: model to render; endpoint array is sorted in place before printing. * * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void render_json(struct model *m) { size_t i, j, k, l; int first_plane = 1; if (m->eps_n > 1) qsort(m->eps, m->eps_n, sizeof(struct endpoint), endpoint_cmp); puts("{"); puts(" \"schema_version\": 1,"); puts(" \"planes\": ["); for (i = 0; i < PLANE_COUNT; i++) { const char *pname = i == PLANE_INET_EXTERNAL ? "INET" : i == PLANE_INET_LOOPBACK ? "INET" : i == PLANE_PACKET ? PLANE_PACKET_NAME : i == PLANE_BLUETOOTH ? PLANE_BLUETOOTH_NAME : "VSOCK"; const char *scope = i == PLANE_INET_EXTERNAL ? "external" : i == PLANE_INET_LOOPBACK ? "loopback" : NULL; struct strset seen_ifaces = { 0 }; int first_if = 1; #ifdef HAVE_NETCAP_VSOCK int first_vsock = 1; #endif #ifndef HAVE_NETCAP_VSOCK if (i == PLANE_VSOCK) continue; #endif if (!first_plane) puts(","); first_plane = 0; printf(" {\"name\": "); json_escape(pname); if (scope) { printf(", \"scope\": "); json_escape(scope); } #ifdef HAVE_NETCAP_VSOCK if (i == PLANE_VSOCK) puts(", \"endpoints\": ["); else #endif puts(", \"ifaces\": ["); #ifdef HAVE_NETCAP_VSOCK if (i == PLANE_VSOCK) { for (j = 0; j < m->eps_n; j++) { struct endpoint *ep = &m->eps[j]; if (ep->plane != (enum plane_kind)i) continue; if (!first_vsock) puts(","); first_vsock = 0; printf(" {\"label\": "); json_escape(ep->label); printf(", \"vsock_type\": "); json_escape(ep->proto); printf(", \"cid\": "); if (ep->vsock_cid == VMADDR_CID_ANY) json_escape("ANY"); else printf("%u", ep->vsock_cid); printf(", \"port\": %u", ep->port); puts(", \"processes\": ["); for (size_t pi = 0; pi < ep->procs_n; pi++) { struct process_info *p = ep->procs[pi]; render_json_process(p, ep, " "); if (pi + 1 != ep->procs_n) puts(","); else putchar('\n'); } puts(" ]}"); } puts(" ]}"); strset_free(&seen_ifaces); continue; } #endif for (j = 0; j < m->eps_n; j++) { struct strset seen_addrs = { 0 }; const char *ifn = m->eps[j].ifname; int first_addr = 1; int seen; if (m->eps[j].plane != (enum plane_kind)i) continue; seen = strset_add(&seen_ifaces, ifn); if (seen < 0) { strset_free(&seen_addrs); continue; } if (seen == 0) continue; if (!first_if) puts(","); first_if = 0; printf(" {\"name\": "); json_escape(ifn); puts(", \"addrs\": ["); for (k = 0; k < m->eps_n; k++) { const char *ifa = m->eps[k].ifaddr; int first_ep = 1; int addr_seen; if (m->eps[k].plane != (enum plane_kind)i || strcmp(m->eps[k].ifname, ifn) != 0) continue; addr_seen = strset_add(&seen_addrs, ifa); if (addr_seen < 0) break; if (addr_seen == 0) continue; if (!first_addr) puts(","); first_addr = 0; printf(" {\"addr\": "); json_escape(ifa); puts(", \"endpoints\": ["); for (l = 0; l < m->eps_n; l++) { struct endpoint *ep = &m->eps[l]; if (ep->plane != (enum plane_kind)i || strcmp(ep->ifname, ifn) != 0 || strcmp(ep->ifaddr, ifa) != 0) continue; if (!first_ep) puts(","); first_ep = 0; printf(" {\"label\": "); json_escape(ep->label); printf(", \"proto\": "); json_escape(ep->proto); printf(", \"bind\": "); json_escape(ep->bind); printf(", \"port\": %u", ep->port); puts(", \"processes\": ["); for (size_t pi = 0; pi < ep->procs_n; pi++) { struct process_info *p = ep->procs[pi]; render_json_process(p, ep, " "); if (pi + 1 != ep->procs_n) puts(","); else putchar('\n'); } puts(" ]}"); } puts(" ]}"); } puts(" ]}"); strset_free(&seen_addrs); } puts(" ]}"); strset_free(&seen_ifaces); } puts(" ]"); puts("}"); } /* * render_interfaces_json - print interface inventory as JSON. * @m: model whose interface inventory is rendered. * * Returns no value. */ static void render_interfaces_json(struct model *m) { size_t i; sort_interfaces(m); puts("{"); puts(" \"schema_version\": 1,"); puts(" \"interfaces\": ["); for (i = 0; i < m->ifaces_n; i++) { printf(" "); json_escape(m->ifaces[i].name); printf("%s\n", i + 1 == m->ifaces_n ? "" : ","); } puts(" ]"); puts("}"); } /* * free_endpoint - free one endpoint and all owned dynamic fields. * @e: endpoint entry pointer, or NULL. * * Returns no value. */ static void free_endpoint(struct endpoint *e) { if (!e) return; free(e->proto); free(e->bind); free(e->label); free(e->ifname); free(e->ifaddr); free(e->procs); } /* * filter_model_interface - keep only one named interface in @m. * @m: model container to prune in place. * @ifname: interface name to retain. * * Returns no value. */ static void filter_model_interface(struct model *m, const char *ifname) { size_t i, out; if (!m || !ifname) return; for (i = 0, out = 0; i < m->ifaces_n; i++) { if (strcmp(m->ifaces[i].name, ifname) == 0) { if (out != i) m->ifaces[out] = m->ifaces[i]; out++; continue; } free(m->ifaces[i].name); for (size_t j = 0; j < m->ifaces[i].addrs_n; j++) free(m->ifaces[i].addrs[j].addr); free(m->ifaces[i].addrs); } m->ifaces_n = out; for (i = 0, out = 0; i < m->eps_n; i++) { if (strcmp(m->eps[i].ifname, ifname) == 0) { if (out != i) m->eps[out] = m->eps[i]; out++; continue; } free_endpoint(&m->eps[i]); } m->eps_n = out; } /* * free_process - free one process_info and all owned dynamic fields. * @p: process entry pointer, or NULL. * * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void free_process(struct process_info *p) { if (!p) return; free(p->comm); free(p->exe); free(p->unit); free(p->caps); free(p->ambient_caps); free(p->defenses.runs_as_nonroot); free(p->defenses.no_new_privs); free(p->defenses.seccomp); free(p->defenses.lsm_label); free(p); } /* * free_model - free all heap allocations referenced by @m. * @m: model container whose internal arrays/strings are released. * * Returns no value. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ static void free_model(struct model *m) { size_t i, j; for (i = 0; i < m->ifaces_n; i++) { free(m->ifaces[i].name); for (j = 0; j < m->ifaces[i].addrs_n; j++) free(m->ifaces[i].addrs[j].addr); free(m->ifaces[i].addrs); } free(m->ifaces); for (i = 0; i < m->procs_n; i++) free_process(m->procs[i]); free(m->procs); for (i = 0; i < m->inode_n; i++) free(m->inode_map[i].procs); free(m->inode_map); free(m->inode_slots); for (i = 0; i < m->eps_n; i++) free_endpoint(&m->eps[i]); free(m->eps); } /* * netcap_advanced_main - entry point for "netcap --advanced" mode. * @opts: parsed options; must be non-NULL and have @advanced set. * * Returns 0 after rendering advanced output, or 1 when advanced mode * is not requested. * Side effects/assumptions: Reads procfs/netlink in the current network * namespace, prints to stdout/stderr, and root is typically needed for a * fuller process-to-socket ownership mapping. */ int netcap_advanced_main(const struct netcap_opts *opts) { struct model m; if (!opts || !opts->advanced) return 1; memset(&m, 0, sizeof(m)); if (collect_interfaces(&m) != 0) { fprintf(stderr, "warning: failed to enumerate interfaces\n"); } if (opts->interface) filter_model_interface(&m, opts->interface); if (opts->list_interfaces) { if (opts->json) render_interfaces_json(&m); else render_interfaces_text(&m); free_model(&m); return 0; } collect_proc_inodes(&m); collect_endpoints(&m); if (opts->interface) filter_model_interface(&m, opts->interface); use_color = !opts->json && !opts->no_color && isatty(STDOUT_FILENO); if (opts->json) render_json(&m); else render_tree(&m); free_model(&m); return 0; } stevegrubb-libcap-ng-9ae4440/utils/netcap-advanced.h000066400000000000000000000022611524174023300224000ustar00rootroot00000000000000/* * netcap-advanced.h - Advanced capability analysis header file * Copyright (c) 2026 Steve Grubb * 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 NETCAP_ADVANCED_H #define NETCAP_ADVANCED_H #include "gcc-attributes.h" struct netcap_opts { int advanced; int list_interfaces; int json; int no_color; const char *interface; }; int netcap_advanced_main(const struct netcap_opts *opts) __attr_access ((__read_only__, 1)) __wur; #endif stevegrubb-libcap-ng-9ae4440/utils/netcap.8.in000066400000000000000000000313041524174023300211620ustar00rootroot00000000000000.\" SPDX-License-Identifier: GPL-2.0-or-later .\" Copyright (c) 2009,2020,2026 Red Hat Inc. .TH NETCAP: "8" "March 2026" "Red Hat" "System Administration Utilities" .SH NAME netcap \- a program to review network capability posture .SH SYNOPSIS .B netcap .\" BEGIN_ADVANCED .RI [ --advanced .RI [ --interface " IFACE ] .RI [ --list-interfaces ] .RI [ --json ] .RI [ --no-color ]] .\" END_ADVANCED .SH DESCRIPTION \fBnetcap\fP is a program that prints out a report of process capabilities. By default, it runs in its historical filtered mode: if an application is using tcp, udp, raw, or packet family sockets AND has any capabilities, it will be in the report. If the process has partial capabilities, it is further examined to see if it has an open-ended bounding set. If this is found to be true, a '+' symbol is added. If the process has ambient capabilities, a '@' symbol is added. Bluetooth sockets are not discovered in this historical filtered mode; use \fB\-\-advanced\fP when Bluetooth listener discovery is needed. .\" BEGIN_ADVANCED With \fB\-\-advanced\fP, \fBnetcap\fP switches to bind/listener inventory mode for the current network namespace. In this mode, reachable binds/listeners are reported regardless of whether the owning process has capabilities, including Bluetooth listeners that are not visible in classic mode. .\" END_ADVANCED Some directories in the /proc file system are readonly by root. The program will try to access and report what it can. .\" BEGIN_ADVANCED Full output normally requires root, or a combination of \fBCAP_DAC_READ_SEARCH\fP (to read other processes' \fB/proc//fd\fP) and \fBCAP_NET_ADMIN\fP (for \fBNETLINK_SOCK_DIAG\fP queries that cover SCTP, DCCP, and, when built with VSOCK support, VSOCK). Without these permissions, the report is partial; missing sock_diag-derived protocols are omitted without an explicit warning. .\" END_ADVANCED .\" BEGIN_ADVANCED .SH OPTIONS .TP .B \-\-advanced Enable the posture/tree view. This mode inventories reachable binds in the current network namespace and shows per-process capabilities, defenses, and flags. .TP .BI \-\-interface " IFACE" With \fB\-\-advanced\fP, limit the report to the named interface. Output format and wrapping are unchanged; only interface-scoped rows for that interface are kept. This also filters \fB\-\-list\-interfaces\fP output to the named interface. .TP .B \-\-list\-interfaces With \fB\-\-advanced\fP, list the current network namespace interface names instead of the exposure tree. .TP .B \-\-json With \fB\-\-advanced\fP, emit machine-readable JSON instead of the tree view. When combined with \fB\-\-list\-interfaces\fP, emit machine-readable interface inventory JSON instead of text. .TP .B \-\-no\-color Suppress colored output. By default, when stdout is a terminal, keywords are colorized by severity to highlight problems, cautions, and positive hardening state. .\" END_ADVANCED .\" BEGIN_ADVANCED .SH "ADVANCED MODE" Advanced mode is an attack-surface and posture inventory for the current network namespace. It is intended for administrators who need a structured view of bind ownership and process hardening state. With \fB\-\-advanced --list\-interfaces\fP, the advanced-mode interface snapshot is rendered directly without enumerating listeners. This is intended as a simple interface enumeration step before later per-interface analysis. With \fB\-\-advanced --interface IFACE\fP, the same inventory and tree/JSON renderers are used, but only endpoints projected onto that interface are retained. The tree layout is: .RS Planes \-> interface \-> protocol \-> bind \-> port \-> process \-> caps/defenses/flags .RE For VSOCK, the interface/address hierarchy is skipped and rendered as: .RS Planes \-> endpoint \-> process \-> caps/defenses/flags .RE For BLUETOOTH, the interface is the HCI adapter (for example \fBhci0\fP). Bluetooth bind nodes are the local controller address or \fB*\fP for wildcard binds. Planes are grouped as INET (external), INET (loopback), BLUETOOTH, VSOCK, and LINK-LAYER. These names match the tree output exactly. The console tree is a causal exposure chain: protocol, then bind address, then port, then owning process. Wildcard binds are printed as \fB*\fP. In tree view, a single \fB0.0.0.0\fP or \fB::\fP listener is expanded onto every non-loopback interface in the current namespace. The same daemon may therefore appear under multiple interfaces, reflecting wildcard-bind reachability. Some endpoints may appear under interface \fBmulticast/group\fP when bound to multicast addresses that do not map to a single interface; these represent group-joined sockets rather than point-to-point listeners. Under each protocol, \fBbind\fP nodes are socket bind addresses (exposure), and \fBport\fP nodes are children of binds. Each process entry may include a \fBunit\fP field showing the systemd service or scope name, extracted from the process cgroup hierarchy; it is omitted when the process is not managed by systemd. Each process line also includes an optional \fBexe\fP field showing the full executable path read from \fB/proc//exe\fP; it is omitted for kernel threads or when the path is unreadable. The kernel \fBName:\fP field shown as \fBcomm\fP is truncated to 15 characters, so \fBexe\fP provides the complete binary path. Socket selection is protocol-specific: .IP \[bu] 2 TCP: LISTEN sockets only. .IP \[bu] 2 UDP/UDPLITE: bound sockets. .IP \[bu] 2 RFCOMM: all owned RFCOMM sockets (state and channel are not exposed by the kernel's proc interface). .IP \[bu] 2 HCI: all bound raw management sockets; semantics differ from port listeners and are analogous to packet sockets. .IP \[bu] 2 SCTP/DCCP: listener sockets only; established flows are not shown. These protocols are discovered via \fBNETLINK_SOCK_DIAG\fP, which requires \fBCAP_NET_ADMIN\fP; without that capability, SCTP and DCCP listeners are silently absent from the report. .IP \[bu] 2 VSOCK: when built with \fBlinux/vm_sockets.h\fP, stream LISTEN sockets, plus bound datagram/seqpacket sockets. .IP \[bu] 2 RAW/PACKET: shown as binds; semantics differ from port listeners. The defenses section is best-effort: \fBno_new_privs\fP and \fBseccomp\fP mode are read from \fB/proc//status\fP when available, and the LSM label is read from \fB/proc//attr/current\fP when available. \fBruns_as_nonroot\fP is always reported. Flags annotate notable conditions: .IP \[bu] 2 \fBwildcard-bind\fP: bind is bound to a wildcard address (for example 0.0.0.0 or ::). .IP \[bu] 2 \fBhypervisor-plane\fP: bind is in the VSOCK communication plane. .IP \[bu] 2 \fBproximity-plane\fP: bind is reachable by any Bluetooth device within radio range. .IP \[bu] 2 \fBssh-on-vsock-port-22\fP: VSOCK bind uses port 22. .IP \[bu] 2 \fBprivileged-caps\fP: process has capabilities considered privileged for attack-surface review (CAP_SYS_ADMIN, CAP_SYS_PTRACE, CAP_DAC_READ_SEARCH, CAP_NET_ADMIN, and CAP_NET_RAW). The \fBcaps\fP field may include bracketed annotations: \fB[ambient-present]\fP indicates ambient capabilities are set, and \fB[open-ended-bounding]\fP indicates the bounding set is non-empty (the process or its children can potentially gain additional capabilities). .SH "JSON OUTPUT" When \fB\-\-advanced --json\fP is used, the top-level object contains: .IP \[bu] 2 \fBschema_version\fP (integer). .IP \[bu] 2 \fBplanes\fP (array). Each plane object contains \fBname\fP, optional \fBscope\fP (\fBexternal\fP or \fBloopback\fP), and either: .IP \[bu] 2 \fBifaces\fP (INET/LINK-LAYER/BLUETOOTH planes), or .IP \[bu] 2 \fBendpoints\fP (VSOCK plane). Under \fBifaces\fP: each interface has \fBname\fP and \fBaddrs\fP (array). Each addr object has \fBaddr\fP and \fBendpoints\fP (array). For INET, LINK-LAYER, and BLUETOOTH planes, each endpoint object has \fBlabel\fP, \fBproto\fP, \fBbind\fP, \fBport\fP, and \fBprocesses\fP (array). For BLUETOOTH planes, interface names are HCI adapter names (for example \fBhci0\fP); \fBport\fP is 0 for HCI sockets and for RFCOMM sockets whose channel is not exposed by procfs. For VSOCK planes, endpoint objects use \fBlabel\fP, \fBvsock_type\fP (socket type string: \fBstream\fP/\fBdgram\fP/\fBseqpacket\fP), \fBcid\fP (source CID or the string \fBANY\fP), \fBport\fP, and \fBprocesses\fP; they do not include \fBproto\fP or \fBbind\fP. When built without \fBlinux/vm_sockets.h\fP, the VSOCK plane and VSOCK flags are omitted. Each process object has \fBcomm\fP, optional \fBexe\fP, \fBpid\fP, \fBuid\fP, optional \fBunit\fP, \fBcaps\fP, \fBambient_present\fP (boolean), \fBopen_ended_bounding\fP (boolean), \fBdefenses\fP object, and \fBflags\fP array. \fBambient_present\fP and \fBopen_ended_bounding\fP are the structured equivalents of the \fB[ambient-present]\fP and \fB[open-ended-bounding]\fP annotations that may appear in the caps text string; both fields are always present in JSON output. Fields under endpoints/processes/defenses may be omitted when unavailable. When process defenses are present, they include \fBruns_as_nonroot\fP, derived from the real UID (first field of \fBUid:\fP in \fB/proc//status\fP): \fByes\fP when real uid != 0, \fBno\fP when real uid == 0. A process that started as root and later changed only effective UID via setuid still reports \fBno\fP. If a plane has no discovered binds, it appears with an empty list. If a consumer does not receive a given plane, treat it as none found or omitted by the producer. When \fB\-\-advanced --list\-interfaces --json\fP is used, the top-level object contains \fBschema_version\fP (integer) and \fBinterfaces\fP (array of interface-name strings). .\" END_ADVANCED .SH INTERPRETATION \fBnetcap\fP reports processes that are both network-facing and running with capabilities. The capability set shown is the permitted set. A '+' annotation means the process has an open-ended bounding set, and an '@' annotation means the process has ambient capabilities. .\" BEGIN_ADVANCED Colorized tree output is intended to prioritize remediation quickly: orange highlights problems to fix, yellow highlights cautionary conditions to review, green highlights good hardening state, and uncolored output is neutral context. The \fBcaps\fP line shows permitted capabilities. Orange-tier capabilities represent severe privilege that is rarely necessary for network-facing daemons: \fBsys_ptrace\fP, \fBsys_module\fP, \fBsys_rawio\fP, \fBsetuid\fP, \fBsetgid\fP, \fBsetpcap\fP, and \fBaudit_control\fP. Yellow-tier capabilities are common but still materially increase attack impact: \fBsys_admin\fP, \fBdac_override\fP, \fBdac_read_search\fP, \fBnet_admin\fP, \fBnet_raw\fP, \fBchown\fP, \fBfowner\fP, \fBmknod\fP, and \fBsys_chroot\fP. \fBcaps: (full)\fP means all capabilities are present (effectively full root privilege). \fBcaps: (none)\fP is ideal. Ambient capabilities are inherited across \fBexecve\fP by child processes (unless constrained by \fBno_new_privs\fP), so their presence on a network daemon is typically a problem. The \fBambient:\fP line lists active ambient capabilities. \fB[open-ended-bounding]\fP means the process has not trimmed its bounding set ceiling. This is not active privilege by itself, but it leaves future privilege-acquisition paths open (for example via file capabilities or ambient inheritance). For libcap-ng users, trimming is typically a one-flag change (\fBCAPNG_SELECT_BOUNDS\fP) in existing capability-set calls; with libcap, bounding capabilities are dropped individually. Defense fields summarize exploit-resistance posture: \fBno_new_privs: yes\fP blocks privilege gain through setuid/file-capability exec paths and is often the highest-value single toggle. \fBseccomp: filter\fP restricts available syscalls (\fBdisabled\fP means no syscall filtering). \fBruns_as_nonroot: yes\fP indicates non-UID-0 execution. Flags provide reachability and interception context: \fBwildcard-bind\fP means the daemon listens on every interface, \fBhypervisor-plane\fP indicates host/ guest boundary exposure (VSOCK), \fBproximity-plane\fP indicates that the bind is reachable by Bluetooth devices within radio range, and \fBreuseport\fP means another process with the same UID could bind and potentially intercept traffic. \fBraw\fP/\fBraw6\fP and \fBpacket\fP protocol nodes denote sockets with IP- or link-layer packet craft/sniff capabilities, significantly expanding what an attacker can do post-compromise. Bluetooth protocol nodes denote proximity-based attack surface. \fBhci\fP nodes provide raw controller management access and are shown in yellow. \fBrfcomm\fP sockets are reachable by nearby devices without prior host access and, unless the adapter is configured to require pairing, may be reachable without authentication. Adapter BD_ADDR resolution requires HCI ioctl access; without it, adapter identity is shown as \fBhci?\fP/\fB*\fP. .\" END_ADVANCED .SH "SEE ALSO" .BR pscap (8), .BR filecap (8), .BR capabilities (7), .BR netstat (8). .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/utils/netcap.c000066400000000000000000000510051524174023300206300ustar00rootroot00000000000000/* * netcap.c - A program that lists network apps with capabilities * Copyright (c) 2009-10,2012,2019-20,2024,2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb * * The /proc/net/tcp|udp|raw parsing code was borrowed from netstat.c */ #include "config.h" #include #ifdef HAVE_NETCAP_ADVANCED #include #include #include #ifdef HAVE_NETCAP_VSOCK #include #include #ifdef HAVE_LINUX_VM_SOCKETS_DIAG_H #include #endif #endif #endif #include #include #include #include #include #include #include #include #include #include #include #include "cap-ng.h" #include "proc-account.h" #include "proc-llist.h" #include "netcap-advanced.h" #include "proc-sanitize.h" #ifndef NETCAP_NO_MAIN static llist l; static int perm_warn = 0, header = 0; static uid_t last_uid = (uid_t)-1; static char *tacct = NULL; #endif #ifdef NETCAP_TEST #define NETCAP_TESTABLE #else #define NETCAP_TESTABLE static #endif /* * VSOCK is the production user of this parser. Keep it available to the * direct utility test even when configure disables all advanced support. */ #if defined(HAVE_NETCAP_VSOCK) || defined(NETCAP_TEST) NETCAP_TESTABLE int parse_u32_hex_or_dec(const char *s, unsigned int *out) __attr_access ((__read_only__, 1)) __attr_access ((__write_only__, 2)) __wur; #endif #ifndef NETCAP_NO_MAIN /* * utility_logic_test only exercises parse_u32_hex_or_dec(). Keep the rest * of netcap out of NETCAP_NO_MAIN builds so the test object does not carry * main-program helpers that trigger unused-function warnings. */ static void usage(void) { fprintf(stderr, "usage: netcap [--advanced " "[--interface IFACE] [--list-interfaces] [--json] " "[--no-color]]\n"); exit(1); } static int collect_process_info(void) { DIR *d, *f; struct dirent *ent; d = opendir("/proc"); if (d == NULL) { fprintf(stderr, "Can't open /proc: %s\n", strerror(errno)); return 1; } while (( ent = readdir(d) )) { FILE *sf; int pid, ppid; capng_results_t caps; char buf[100]; char *safe_cmd; char *tmp, cmd[16], state; char *text = NULL, *bounds = NULL, *ambient = NULL; int fd, len; uid_t euid = (uid_t)-1; // Skip non-process dir entries if(*ent->d_name<'0' || *ent->d_name>'9') continue; errno = 0; pid = strtol(ent->d_name, NULL, 10); if (errno) continue; // Parse up the stat file for the proc snprintf(buf, sizeof(buf), "/proc/%d/stat", pid); fd = open(buf, O_RDONLY|O_CLOEXEC, 0); if (fd < 0) continue; len = read(fd, buf, sizeof buf - 1); close(fd); if (len < 40) continue; buf[len] = 0; tmp = strrchr(buf, ')'); if (tmp) *tmp = 0; else continue; memset(cmd, 0, sizeof(cmd)); if (sscanf(buf, "%d (%15c", &ppid, cmd) != 2) continue; if (sscanf(tmp+2, "%c %d", &state, &ppid) != 2) continue; // Skip kthreads if (pid == 2 || ppid == 2) continue; // now get the capabilities capng_clear(CAPNG_SELECT_ALL); capng_setpid(pid); if (capng_get_caps_process()) continue; caps = capng_have_capabilities(CAPNG_SELECT_CAPS); if (caps <= CAPNG_NONE) continue; if (caps == CAPNG_FULL) { text = strdup("full"); if (!text) { fprintf(stderr, "Out of memory\n"); continue; } } else { text = capng_print_caps_text(CAPNG_PRINT_BUFFER, CAPNG_PERMITTED); if (text == NULL) { fprintf(stderr, "Out of memory doing pid %d\n", pid); continue; } } // Get the effective uid snprintf(buf, sizeof(buf), "/proc/%d/status", pid); sf = fopen(buf, "rte"); if (sf != NULL) { int line = 0; __fsetlocking(sf, FSETLOCKING_BYCALLER); while (fgets(buf, sizeof(buf), sf)) { if (line == 0) { line++; continue; } if (memcmp(buf, "Uid:", 4) == 0) { uid_t id; if (sscanf(buf, "Uid: %u %u", &id, &euid) == 2) break; } } fclose(sf); } caps = capng_have_capabilities(CAPNG_SELECT_AMBIENT); if (caps > CAPNG_NONE) ambient = strdup("@"); else ambient = strdup(""); if (!ambient) { fprintf(stderr, "Out of memory\n"); free(text); continue; } // Now record the bounding set information caps = capng_have_capabilities(CAPNG_SELECT_BOUNDS); if (caps > CAPNG_NONE) bounds = strdup("+"); else bounds = strdup(""); if (!bounds) { fprintf(stderr, "Out of memory\n"); free(text); free(ambient); continue; } // Now lets get the inodes each process has open snprintf(buf, sizeof(buf), "/proc/%d/fd", pid); f = opendir(buf); if (f == NULL) { if (errno == EACCES) { if (perm_warn == 0) { fprintf(stderr, "You may need to be root to " "get a full report\n"); perm_warn = 1; } } else fprintf(stderr, "Can't open %s: %s\n", buf, strerror(errno)); free(text); free(bounds); free(ambient); continue; } // For each file in the fd dir... struct dirent *fd_ent; while (( fd_ent = readdir(f) )) { char line[256], ln[256], *s, *e; unsigned long inode; lnode node; int llen; if (fd_ent->d_name[0] == '.') continue; snprintf(ln, 256, "%s/%s", buf, fd_ent->d_name); if ((llen = readlink(ln, line, sizeof(line)-1)) < 0) continue; line[llen] = 0; // Only look at the socket entries if (memcmp(line, "socket:", 7) == 0) { // Type 1 sockets s = strchr(line+7, '['); if (s == NULL) continue; s++; e = strchr(s, ']'); if (e == NULL) continue; *e = 0; } else if (memcmp(line, "[0000]:", 7) == 0) { // Type 2 sockets s = line + 8; } else continue; errno = 0; inode = strtoul(s, NULL, 10); if (errno) continue; safe_cmd = sanitize_untrusted_field(cmd); if (!safe_cmd) continue; node.ppid = ppid; node.pid = pid; node.uid = euid; node.cmd = safe_cmd; node.inode = inode; node.capabilities = strdup(text); node.bounds = strdup(bounds); node.ambient = strdup(ambient); if (node.cmd && node.capabilities && node.bounds && node.ambient) // We make one entry for each socket inode list_append(&l, &node); else { free(node.cmd); free(node.capabilities); free(node.bounds); free(node.ambient); } } closedir(f); free(text); free(bounds); free(ambient); } closedir(d); return 0; } static void report_finding(const lnode *n, unsigned int port, const char *type, const char *ifc) { // And print out anything with capabilities if (header == 0) { printf("%-7s %-7s %-16s %-15s %-8s %-15s %s\n", "ppid", "pid", "acct", "command", "type", "port", "capabilities"); header = 1; } netcap_update_account_cache(n->uid, &last_uid, (const char **)&tacct); if (tacct) { printf("%-7d %-7d %-16s", n->ppid, n->pid, tacct); } else printf("%-7d %-7d %-16u", n->ppid, n->pid, (unsigned int)last_uid); printf(" %-15s %-8s", n->cmd, type); if (ifc) printf(" %-15s", ifc); else printf(" %-15u", port); printf(" %s %s%s\n", n->capabilities, n->ambient, n->bounds); } /* * Multiple processes can legitimately hold descriptors for the same socket * inode. Report every owner so preforked or reuseport listeners are not * collapsed into a single row. */ static void report_inode_findings(unsigned long inode, unsigned int port, const char *type, const char *ifc) { lnode *n; for (n = list_find_inode(&l, inode); n; n = list_next_inode(&l, inode)) report_finding(n, port, type, ifc); } static void read_net(const char *proc, const char *type, int use_local_port) { int line = 0; FILE *f; char buf[256]; unsigned long rxq, txq, time_len, retr, inode; unsigned int local_port, rem_port, state, timer_run; int d, uid, timeout; char rem_addr[128], local_addr[128], more[512]; f = fopen(proc, "rte"); if (f == NULL) { if (errno != ENOENT) fprintf(stderr, "Can't open %s: %s\n", proc, strerror(errno)); return; } __fsetlocking(f, FSETLOCKING_BYCALLER); while (fgets(buf, sizeof(buf), f)) { if (line == 0) { line++; continue; } more[0] = 0; if (sscanf(buf, "%d: %64[0-9A-Fa-f]:%X %64[0-9A-Fa-f]:%X %X " "%lX:%lX %X:%lX %lX %d %d %lu %511s\n", &d, local_addr, &local_port, rem_addr, &rem_port, &state, &txq, &rxq, &timer_run, &time_len, &retr, &uid, &timeout, &inode, more) < 14) continue; report_inode_findings(inode, use_local_port ? local_port : 0, type, NULL); } fclose(f); } // Caller must have buffer >= 65 bytes static void get_interface(unsigned int iface, char *ifc) { unsigned int line = 0; FILE *f; char buf[256], more[256]; // Terminate the interface in case of error *ifc = 0; // Offset the interface number since header is 2 lines long iface += 2; f = fopen("/proc/net/dev", "rte"); if (f == NULL) { if (errno != ENOENT) fprintf(stderr, "Can't open /proc/net/dev: %s\n", strerror(errno)); return; } __fsetlocking(f, FSETLOCKING_BYCALLER); while (fgets(buf, sizeof(buf), f)) { if (line == iface) { char *c; char *safe_ifc; sscanf(buf, "%16s: %255s\n", ifc, more); c = strchr(ifc, ':'); if (c) *c = 0; safe_ifc = sanitize_untrusted_field(ifc); if (safe_ifc) { strncpy(ifc, safe_ifc, 64); ifc[64] = '\0'; free(safe_ifc); } else *ifc = 0; fclose(f); return; } line++; } fclose(f); } static void read_packet(void) { int line = 0; FILE *f; char buf[256]; unsigned long sk, inode; unsigned int ref_cnt, type, proto, iface, r, rmem, uid; char more[256], ifc[65]; f = fopen("/proc/net/packet", "rte"); if (f == NULL) { if (errno != ENOENT) fprintf(stderr, "Can't open /proc/net/packet: %s\n", strerror(errno)); return; } __fsetlocking(f, FSETLOCKING_BYCALLER); while (fgets(buf, sizeof(buf), f)) { if (line == 0) { line++; continue; } more[0] = 0; if (sscanf(buf, "%lX %u %u %X %u %u %u %u %lu %255s\n", &sk, &ref_cnt, &type, &proto, &iface, &r, &rmem, &uid, &inode, more) < 9) continue; get_interface(iface, ifc); report_inode_findings(inode, 0, "pkt", ifc); } fclose(f); } #endif #if defined(HAVE_NETCAP_VSOCK) || defined(NETCAP_TEST) NETCAP_TESTABLE int parse_u32_hex_or_dec(const char *s, unsigned int *out) { char *end; unsigned long v; int base = 10; const char *p; /* strtoul accepts signs, but these kernel fields are unsigned. */ if (*s == '+' || *s == '-') return -1; for (p = s; *p; p++) { if ((*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F')) { base = 16; break; } } if (strncmp(s, "0x", 2) == 0 || strncmp(s, "0X", 2) == 0) base = 16; if (base == 10 && strlen(s) > 3 && s[0] == '0') base = 16; errno = 0; v = strtoul(s, &end, base); if (errno == ERANGE || end == s || *end) return -1; /* * /proc and diag inputs are meant to fit in u32 fields. On platforms * where unsigned long is wider, reject values that would be truncated. */ if (v > UINT_MAX) return -1; *out = (unsigned int)v; return 0; } #endif #ifdef HAVE_NETCAP_ADVANCED #ifndef NETCAP_NO_MAIN static int read_diag_messages(int fd, int proto, const char *type) { char buf[8192]; ssize_t len; struct sockaddr_nl nladdr; socklen_t nladdr_len; while (1) { nladdr_len = sizeof(nladdr); memset(&nladdr, 0, sizeof(nladdr)); len = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *)&nladdr, &nladdr_len); if (len < 0) { if (errno == EINTR) continue; return -1; } if (len == 0) return -1; if (nladdr_len < sizeof(nladdr) || nladdr.nl_pid != 0) continue; struct nlmsghdr *nlh; unsigned int rem; if (len > UINT_MAX) return -1; rem = (unsigned int)len; for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, rem); nlh = NLMSG_NEXT(nlh, rem)) { struct inet_diag_msg *r; unsigned int port; if (nlh->nlmsg_type == NLMSG_DONE) return 0; if (nlh->nlmsg_type == NLMSG_ERROR) { struct nlmsgerr *e; if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*e))) return -1; e = NLMSG_DATA(nlh); if (e->error == 0) continue; errno = -e->error; return -1; } if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*r))) continue; r = NLMSG_DATA(nlh); if (!list_find_inode(&l, r->idiag_inode)) continue; port = ntohs(r->id.idiag_sport); if (!port) continue; if (proto == IPPROTO_SCTP || proto == IPPROTO_DCCP) report_inode_findings(r->idiag_inode, port, type, NULL); } } } static int read_diag_for_proto_af(int proto, int af, const char *type) { struct { struct nlmsghdr nlh; struct inet_diag_req_v2 req; } req; struct sockaddr_nl sa; int fd; int rc = -1; fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG); if (fd < 0) return -1; memset(&req, 0, sizeof(req)); req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(req.req)); req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY; req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; req.req.sdiag_family = af; req.req.sdiag_protocol = proto; req.req.idiag_states = 1U << TCP_LISTEN; memset(&sa, 0, sizeof(sa)); sa.nl_family = AF_NETLINK; sa.nl_pid = 0; if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) goto out; if (send(fd, &req, req.nlh.nlmsg_len, 0) < 0) goto out; rc = read_diag_messages(fd, proto, type); out: close(fd); return rc; } static void read_diag_listeners(void) { int sctp_ok = 0; int dccp_ok = 0; if (read_diag_for_proto_af(IPPROTO_SCTP, AF_INET, "sctp") == 0) sctp_ok = 1; if (read_diag_for_proto_af(IPPROTO_SCTP, AF_INET6, "sctp") == 0) sctp_ok = 1; if (read_diag_for_proto_af(IPPROTO_DCCP, AF_INET, "dccp") == 0) dccp_ok = 1; if (read_diag_for_proto_af(IPPROTO_DCCP, AF_INET6, "dccp") == 0) dccp_ok = 1; if (!dccp_ok) { read_net("/proc/net/dccp", "dccp", 1); read_net("/proc/net/dccp6", "dccp", 1); } (void)sctp_ok; } #ifdef HAVE_NETCAP_VSOCK #ifdef HAVE_LINUX_VM_SOCKETS_DIAG_H static int read_vsock_diag_messages(int fd) { char buf[8192]; ssize_t len; struct sockaddr_nl nladdr; socklen_t nladdr_len; while (1) { nladdr_len = sizeof(nladdr); memset(&nladdr, 0, sizeof(nladdr)); len = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *)&nladdr, &nladdr_len); if (len < 0) { if (errno == EINTR) continue; return -1; } if (len == 0) return -1; if (nladdr_len < sizeof(nladdr) || nladdr.nl_pid != 0) continue; struct nlmsghdr *nlh; unsigned int rem; if (len > UINT_MAX) return -1; rem = (unsigned int)len; for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, rem); nlh = NLMSG_NEXT(nlh, rem)) { struct vsock_diag_msg *r; if (nlh->nlmsg_type == NLMSG_DONE) return 0; if (nlh->nlmsg_type == NLMSG_ERROR) { struct nlmsgerr *e; if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*e))) return -1; e = NLMSG_DATA(nlh); if (e->error == 0) continue; errno = -e->error; return -1; } if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*r))) continue; r = NLMSG_DATA(nlh); if (r->vdiag_family != AF_VSOCK) continue; if (r->vdiag_type != SOCK_STREAM || r->vdiag_state != TCP_LISTEN) continue; if (!list_find_inode(&l, r->vdiag_ino)) continue; if (r->vdiag_src_port == 0) continue; report_inode_findings(r->vdiag_ino, r->vdiag_src_port, "vsock", NULL); } } } static int read_vsock_diag(void) { struct { struct nlmsghdr nlh; struct vsock_diag_req req; } req; struct sockaddr_nl sa; int fd; int rc = -1; fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG); if (fd < 0) return -1; memset(&req, 0, sizeof(req)); req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(req.req)); req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY; req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; req.req.sdiag_family = AF_VSOCK; req.req.sdiag_protocol = 0; req.req.vdiag_states = ~0U; memset(&sa, 0, sizeof(sa)); sa.nl_family = AF_NETLINK; sa.nl_pid = 0; if (sendto(fd, &req, req.nlh.nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa)) < 0) goto out; rc = read_vsock_diag_messages(fd); out: close(fd); return rc; } #else static int read_vsock_diag(void) { errno = EOPNOTSUPP; return -1; } #endif static void read_vsock_proc(void) { FILE *f; char line[512]; f = fopen("/proc/net/vsock", "rte"); if (f == NULL) { if (errno != ENOENT) fprintf(stderr, "Can't open /proc/net/vsock: %s\n", strerror(errno)); return; } __fsetlocking(f, FSETLOCKING_BYCALLER); while (fgets(line, sizeof(line), f)) { char work[512]; char *tok[24]; char *save = NULL; char *local, *sep, *s; int tcnt = 0; unsigned long inode; unsigned int st, type, cid, port; if (strstr(line, "Local") || strstr(line, "local") || strstr(line, "Num")) continue; snprintf(work, sizeof(work), "%s", line); s = strtok_r(work, " \t\n", &save); while (s && tcnt < (int)(sizeof(tok) / sizeof(tok[0]))) { tok[tcnt++] = s; s = strtok_r(NULL, " \t\n", &save); } if (tcnt < 5) continue; local = NULL; int i; for (i = 0; i < tcnt; i++) { if (strchr(tok[i], ':')) { local = tok[i]; break; } } if (!local) continue; sep = strchr(local, ':'); if (!sep) continue; *sep = '\0'; if (parse_u32_hex_or_dec(local, &cid) || parse_u32_hex_or_dec(sep + 1, &port)) continue; if (parse_u32_hex_or_dec(tok[tcnt - 2], &st)) continue; if (parse_u32_hex_or_dec(tok[tcnt - 3], &type)) continue; inode = strtoul(tok[tcnt - 1], NULL, 10); if (!inode) continue; if (type != SOCK_STREAM || st != 0x0A || port == 0) continue; if (!list_find_inode(&l, inode)) continue; (void)cid; report_inode_findings(inode, port, "vsock", NULL); } fclose(f); } static void read_vsock(void) { if (read_vsock_diag() < 0) read_vsock_proc(); } #endif #endif #endif #ifndef NETCAP_NO_MAIN int main(int argc, char **argv) { struct netcap_opts opts = { 0, 0, 0, 0, NULL }; int i; for (i = 1; i < argc; i++) { if (strcmp(argv[i], "--advanced") == 0) opts.advanced = 1; else if (strcmp(argv[i], "--interface") == 0) { if (i + 1 >= argc) { fputs("--interface requires an argument\n", stderr); usage(); } opts.interface = argv[++i]; } else if (strncmp(argv[i], "--interface=", 12) == 0) { if (argv[i][12] == 0) { fputs("--interface requires an argument\n", stderr); usage(); } opts.interface = argv[i] + 12; } else if (strcmp(argv[i], "--list-interfaces") == 0) opts.list_interfaces = 1; else if (strcmp(argv[i], "--json") == 0) opts.json = 1; else if (strcmp(argv[i], "--no-color") == 0) opts.no_color = 1; else { fprintf(stderr, "Unknown option: %s\n", argv[i]); usage(); } } if (opts.json && !opts.advanced) { fputs("--json is only valid with --advanced\n", stderr); usage(); } if (opts.list_interfaces && !opts.advanced) { fputs("--list-interfaces is only valid with --advanced\n", stderr); usage(); } if (opts.interface && !opts.advanced) { fputs("--interface is only valid with --advanced\n", stderr); usage(); } if (opts.advanced) { #ifdef HAVE_NETCAP_ADVANCED return netcap_advanced_main(&opts); #else fputs("netcap --advanced was disabled at configure time\n", stderr); fputs("because required kernel headers were not available\n", stderr); return 1; #endif } if (argc > 1) { fputs("Too many arguments\n", stderr); usage(); } list_create(&l); collect_process_info(); // Now we check the tcp socket list... read_net("/proc/net/tcp", "tcp", 1); read_net("/proc/net/tcp6", "tcp6", 1); // Next udp sockets... read_net("/proc/net/udp", "udp", 1); read_net("/proc/net/udp6", "udp6", 1); read_net("/proc/net/udplite", "udplite", 1); read_net("/proc/net/udplite6", "udplite6", 1); // Next, raw sockets... read_net("/proc/net/raw", "raw", 0); read_net("/proc/net/raw6", "raw6", 0); // And last, read packet sockets read_packet(); // Add listeners from protocols supported in advanced mode #ifdef HAVE_NETCAP_ADVANCED read_diag_listeners(); #ifdef HAVE_NETCAP_VSOCK read_vsock(); #endif #endif // Could also do icmp,netlink,unix list_clear(&l); return 0; } #endif stevegrubb-libcap-ng-9ae4440/utils/proc-account.c000066400000000000000000000052371524174023300217610ustar00rootroot00000000000000/* * proc-account.c - Shared account name helpers for proc-based utilities * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * pscap and netcap used to each carry their own owner-formatting logic. * The helpers live here so the reporting policy stays consistent and so * make check can cover the failure cases directly. In particular, unknown * proc owners must never be rewritten as "root", and a failed passwd * lookup must not leak a stale cached username into later rows. */ #include "config.h" #include #include #include #include "proc-account.h" void proc_format_account_name_from_euid(uid_t euid, char *account, size_t account_len) { struct passwd *p; if (euid == (uid_t)-1) { strncpy(account, "unknown", account_len - 1); account[account_len - 1] = '\0'; return; } if (euid == 0) { strncpy(account, "root", account_len - 1); account[account_len - 1] = '\0'; return; } p = getpwuid(euid); if (p && p->pw_name) { strncpy(account, p->pw_name, account_len - 1); account[account_len - 1] = '\0'; return; } snprintf(account, account_len, "%u", (unsigned int)euid); } /* * proc_update_account_cache - update cached passwd lookup state for @uid. * @uid: effective UID to format. * @last_uid: caller's cached UID value, updated when @uid is looked up. * @cached_name: caller's cached passwd name pointer, updated in place. * * Returns no value. @cached_name is NULL when @uid has no passwd entry. */ void proc_update_account_cache(uid_t uid, uid_t *last_uid, const char **cached_name) { struct passwd *p; if (uid == 0) { *cached_name = "root"; *last_uid = 0; return; } if (uid == (uid_t)-1) { *cached_name = "unknown"; *last_uid = (uid_t)-1; return; } if (*last_uid == uid) return; /* * Clear the cached pointer before looking up the next uid so a failed * passwd lookup cannot leak the previous account name into output. */ *cached_name = NULL; p = getpwuid(uid); *last_uid = uid; if (p) *cached_name = p->pw_name; } /* * netcap_update_account_cache - compatibility wrapper for netcap callers. * @uid: effective UID to format. * @last_uid: caller's cached UID value, updated when @uid is looked up. * @cached_name: caller's cached passwd name pointer, updated in place. * * Returns no value. */ void netcap_update_account_cache(uid_t uid, uid_t *last_uid, const char **cached_name) { proc_update_account_cache(uid, last_uid, cached_name); } stevegrubb-libcap-ng-9ae4440/utils/proc-account.h000066400000000000000000000017031524174023300217600ustar00rootroot00000000000000/* * proc-account.h - Shared account name helpers for proc-based utilities * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef PROC_ACCOUNT_H #define PROC_ACCOUNT_H #include #include #include "gcc-attributes.h" void proc_format_account_name_from_euid(uid_t euid, char *account, size_t account_len) __attr_access ((__write_only__, 2, 3)); void proc_update_account_cache(uid_t uid, uid_t *last_uid, const char **cached_name) __attr_access ((__read_write__, 2)) __attr_access ((__write_only__, 3)); void netcap_update_account_cache(uid_t uid, uid_t *last_uid, const char **cached_name) __attr_access ((__read_write__, 2)) __attr_access ((__write_only__, 3)); #endif stevegrubb-libcap-ng-9ae4440/utils/proc-llist.c000066400000000000000000000051621524174023300214510ustar00rootroot00000000000000/* * proc-llist.c - Minimal linked list library * Copyright (c) 2009, 2020, 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #include "config.h" #include #include #include "proc-llist.h" void list_create(llist *l) { l->head = NULL; l->cur = NULL; l->cnt = 0; } void list_append(llist *l, lnode *node) { lnode* newnode; if (node == NULL || l == NULL) return; newnode = malloc(sizeof(lnode)); if (newnode == NULL) return; newnode->ppid = node->ppid; newnode->pid = node->pid; newnode->uid = node->uid; newnode->inode = node->inode; // Take custody of the memory newnode->cmd = node->cmd; newnode->capabilities = node->capabilities; newnode->bounds = node->bounds; newnode->ambient = node->ambient; newnode->next = NULL; // 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; l->cnt++; } void list_clear(llist* l) { lnode* nextnode; register lnode* cur; cur = l->head; while (cur) { nextnode=cur->next; free(cur->cmd); free(cur->capabilities); free(cur->bounds); free(cur->ambient); free(cur); cur=nextnode; } l->head = NULL; l->cur = NULL; l->cnt = 0; } lnode *list_find_inode(llist *l, unsigned long i) { register lnode* cur; cur = l->head; /* start at the beginning */ while (cur) { if (cur->inode == i) { l->cur = cur; return cur; } else cur = cur->next; } return NULL; } /* * Continue iterating over entries that share the same inode. netcap records * one node per process/socket pair, so callers need to walk every match. */ lnode *list_next_inode(llist *l, unsigned long i) { register lnode *cur; if (l == NULL || l->cur == NULL) return NULL; cur = l->cur->next; while (cur) { if (cur->inode == i) { l->cur = cur; return cur; } cur = cur->next; } return NULL; } stevegrubb-libcap-ng-9ae4440/utils/proc-llist.h000066400000000000000000000045141524174023300214560ustar00rootroot00000000000000/* * proc-llist.h * Copyright (c) 2009, 2020, 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #ifndef PROC_HEADER #define PROC_HEADER #include "config.h" #include /* Ensure types in _lnode are defined on all systems */ #include "gcc-attributes.h" /* This is the node of the linked list. Any data elements that are per * record goes here. */ typedef struct _lnode{ pid_t ppid; // parent process ID pid_t pid; // process ID uid_t uid; // user ID char *cmd; // command run by user unsigned long inode; // inode of socket char *capabilities; // Text of partial capabilities char *bounds; // Text for bounding set char *ambient; // Text for ambient set 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; void list_create(llist *l) __attr_access ((__write_only__, 1)); static inline lnode *list_get_cur(llist *l) __attr_access ((__read_only__, 1)); static inline lnode *list_get_cur(llist *l) { return l->cur; } void list_append(llist *l, lnode *node) __attr_access ((__read_write__, 1)) __attr_access ((__read_only__, 2)); void list_clear(llist* l) __attr_access ((__read_write__, 1)); /* Given a message type, find the matching node */ lnode *list_find_inode(llist *l, unsigned long i) __attr_access ((__read_write__, 1)); lnode *list_next_inode(llist *l, unsigned long i) __attr_access ((__read_write__, 1)); #endif stevegrubb-libcap-ng-9ae4440/utils/proc-output.c000066400000000000000000000147551524174023300216720ustar00rootroot00000000000000/* * proc-output.c - Shared terminal output helpers for proc utilities * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #include "config.h" #include #include #include #include #include #include #include "proc-output.h" #define PROC_COLOR_RESET "\033[0m" /* * proc_output_width - choose the output width for wrapped terminal text. * @none: function takes no parameters. * * Returns terminal columns from ioctl(), then $COLUMNS, else 80. */ int proc_output_width(void) { struct winsize ws; const int fds[] = { STDOUT_FILENO, STDERR_FILENO, STDIN_FILENO }; const char *env; char *end = NULL; unsigned long v; size_t i; for (i = 0; i < sizeof(fds) / sizeof(fds[0]); i++) { if (ioctl(fds[i], TIOCGWINSZ, &ws) == 0 && ws.ws_col) return ws.ws_col; } env = getenv("COLUMNS"); if (env) { errno = 0; v = strtoul(env, &end, 10); if (errno == 0 && end != env && *end == '\0' && v > 0 && v < 400) return (int)v; } return 80; } /* * proc_wrap_plain - choose a wrap index for plain text. * @text: source string to wrap. * @max: maximum number of bytes to include before wrapping. * * Returns the byte offset where output should wrap, preferring commas and * spaces when possible. */ size_t proc_wrap_plain(const char *text, size_t max) { size_t len = strlen(text); size_t i; if (len <= max) return len; for (i = max; i > 0; i--) { if (text[i - 1] == ',') { if (i < len && text[i] == ' ') return i + 1; return i; } if (text[i - 1] == ' ') return i; } return max; } /* * skip_ansi_sgr - skip one ANSI SGR escape sequence. * @text: source string that may contain an SGR sequence at @i. * @i: byte offset to inspect. * * Returns the first byte after the SGR sequence, or @i when @text[@i] does * not start one. */ static int skip_ansi_sgr(const char *text, int i) { if (text[i] != '\033' || text[i + 1] != '[') return i; i += 2; while (text[i] && text[i] != 'm') i++; if (text[i] == 'm') i++; return i; } /* * copy_sgr - copy the active ANSI SGR sequence into @dst. * @dst: destination buffer for the active SGR sequence. * @dst_sz: size of @dst in bytes. * @text: source string containing an SGR sequence at @i. * @i: byte offset to inspect. * * Returns the first byte after the SGR sequence, or @i when no sequence was * present. A reset sequence clears @dst. */ static int copy_sgr(char *dst, size_t dst_sz, const char *text, int i) { int start = i; int end; size_t len; end = skip_ansi_sgr(text, i); if (end == start) return i; len = end - start; if (strncmp(text + start, PROC_COLOR_RESET, strlen(PROC_COLOR_RESET)) == 0) { dst[0] = '\0'; return end; } if (len < dst_sz) { memcpy(dst, text + start, len); dst[len] = '\0'; } return end; } /* * scan_color_state - update active color while scanning a wrapped segment. * @text: source string containing the rendered segment. * @from: first byte offset in the segment. * @to: byte offset just past the segment. * @active: destination buffer holding the active SGR sequence. * @active_sz: size of @active in bytes. * * Returns no value. @active is updated in place for continuation lines. */ static void scan_color_state(const char *text, int from, int to, char *active, size_t active_sz) { int i; for (i = from; i < to && text[i]; ) { if (text[i] == '\033' && text[i + 1] == '[') { i = copy_sgr(active, active_sz, text, i); continue; } i++; } } /* * wrap_ansi - choose a wrap index for text containing ANSI SGR escapes. * @text: source string to wrap. * @from: byte offset where this line starts. * @limit: maximum visible columns to include. * * Returns the byte offset where output should continue; SGR bytes do not * count toward @limit. */ static int wrap_ansi(const char *text, int from, int limit) { int i; int vis = 0; int break_at = -1; for (i = from; text[i] && vis < limit; ) { if (text[i] == '\033' && text[i + 1] == '[') { i = skip_ansi_sgr(text, i); continue; } if (text[i] == ' ' || text[i] == ',') break_at = i + 1; i++; vis++; } if (!text[i]) return i; if (break_at > from) return break_at; if (i == from) return from + 1; return i; } /* * proc_print_wrapped - print wrapped text with separate prefixes. * @head: prefix used for the first output line. * @cont: prefix used for continuation lines. * @text: text to render. * @width: target terminal width. * * Returns no value. ANSI SGR color is preserved across wrapped lines. */ void proc_print_wrapped(const char *head, const char *cont, const char *text, int width) { char active[32] = ""; int pos = 0; int first = 1; while (1) { const char *lead = first ? head : cont; int lead_len = strlen(lead); int avail = width - lead_len; int to; if (avail < 10) avail = 10; if (!text[pos]) { printf("%s\n", lead); return; } to = wrap_ansi(text, pos, avail); printf("%s", lead); if (!first && active[0]) fputs(active, stdout); printf("%.*s", to - pos, text + pos); scan_color_state(text, pos, to, active, sizeof(active)); if (active[0]) fputs(PROC_COLOR_RESET, stdout); putchar('\n'); while (text[to] == ' ') to++; pos = to; first = 0; if (!text[pos]) return; } } /* * proc_tree_print_node - print one wrapped tree node. * @prefix: existing tree prefix before this node. * @is_last: non-zero when this node is the last sibling. * @txt: node text to render. * @width: target terminal width. * * Returns no value. */ void proc_tree_print_node(const char *prefix, int is_last, const char *txt, int width) { char head[512]; char cont[512]; snprintf(head, sizeof(head), "%s%s", prefix, is_last ? "└─ " : "├─ "); snprintf(cont, sizeof(cont), "%s%s", prefix, is_last ? " " : "│ "); proc_print_wrapped(head, cont, txt, width); } /* * proc_tree_build_child_prefix - extend a tree prefix for child nodes. * @dst: destination buffer receiving the child prefix. * @dst_sz: size of @dst in bytes. * @prefix: parent prefix to extend. * @parent_is_last: non-zero when the parent is the last sibling. * * Returns no value. */ void proc_tree_build_child_prefix(char *dst, size_t dst_sz, const char *prefix, int parent_is_last) { snprintf(dst, dst_sz, "%s%s", prefix, parent_is_last ? " " : "│ "); } stevegrubb-libcap-ng-9ae4440/utils/proc-output.h000066400000000000000000000022001524174023300216550ustar00rootroot00000000000000/* * proc-output.h - Shared terminal output helpers for proc utilities * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef PROC_OUTPUT_H #define PROC_OUTPUT_H #include #include "gcc-attributes.h" int proc_output_width(void); size_t proc_wrap_plain(const char *text, size_t max) __attr_access ((__read_only__, 1)) __attribute_pure__; void proc_print_wrapped(const char *head, const char *cont, const char *text, int width) __attr_access ((__read_only__, 1)) __attr_access ((__read_only__, 2)) __attr_access ((__read_only__, 3)); void proc_tree_print_node(const char *prefix, int is_last, const char *txt, int width) __attr_access ((__read_only__, 1)) __attr_access ((__read_only__, 3)); void proc_tree_build_child_prefix(char *dst, size_t dst_sz, const char *prefix, int parent_is_last) __attr_access ((__write_only__, 1, 2)) __attr_access ((__read_only__, 3)); #endif stevegrubb-libcap-ng-9ae4440/utils/proc-sanitize.c000066400000000000000000000044331524174023300221500ustar00rootroot00000000000000/* * proc-sanitize.c - Shared terminal sanitization helpers for proc text * Copyright (c) 2026 Steve Grubb * 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 "proc-sanitize.h" /* * sanitize_untrusted_field - escape terminal control bytes in untrusted text. * @src: source text gathered from procfs/cgroup metadata. * * Returns caller-owned sanitized text, or NULL on allocation failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ char *sanitize_untrusted_field(const char *src) { size_t in_len; char *dst; char *out; size_t i; if (!src) return NULL; in_len = strlen(src); dst = malloc(in_len * 4 + 1); if (!dst) return NULL; out = dst; for (i = 0; i < in_len; i++) { unsigned char c = (unsigned char)src[i]; if (c < 0x20 || c == 0x7f) { snprintf(out, 5, "\\x%02X", c); out += 4; } else { *out++ = (char)c; } } *out = '\0'; return dst; } /* * sanitize_untrusted_owned - replace owned string with sanitized version. * @s: pointer to owned string pointer that will be replaced in place. * * Returns 0 on success, -1 on allocation failure. * Side effects/assumptions: Operates on in-memory data and may read * procfs/netns state; it does not change kernel configuration. */ int sanitize_untrusted_owned(char **s) { char *safe; if (!s || !*s) return 0; safe = sanitize_untrusted_field(*s); if (!safe) return -1; free(*s); *s = safe; return 0; } stevegrubb-libcap-ng-9ae4440/utils/proc-sanitize.h000066400000000000000000000022731524174023300221550ustar00rootroot00000000000000/* * proc-sanitize.h - Shared terminal sanitization helpers for proc text * Copyright (c) 2026 Steve Grubb * 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 PROC_SANITIZE_H #define PROC_SANITIZE_H #include "gcc-attributes.h" char *sanitize_untrusted_field(const char *src) __attribute_malloc__ __attr_dealloc_free __attr_access ((__read_only__, 1)) __wur; int sanitize_untrusted_owned(char **s) __attr_access ((__read_write__, 1)) __wur; #endif stevegrubb-libcap-ng-9ae4440/utils/proc-status.c000066400000000000000000000033021524174023300216370ustar00rootroot00000000000000/* * proc-status.c - Shared /proc//status helpers * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #include "config.h" #include #include #include #include "proc-status.h" /* * proc_read_status - read selected fields from /proc//status. * @pid: process ID whose status file should be read. * @status: destination structure cleared and populated on success. * * Returns 0 when the status file was opened and parsed, -1 when it could not * be opened. */ int proc_read_status(pid_t pid, struct proc_status *status) { char path[64], line[256]; FILE *f; memset(status, 0, sizeof(*status)); status->uid = (uid_t)-1; status->euid = (uid_t)-1; snprintf(path, sizeof(path), "/proc/%d/status", pid); f = fopen(path, "rte"); if (!f) return -1; __fsetlocking(f, FSETLOCKING_BYCALLER); while (fgets(line, sizeof(line), f)) { uid_t uid, euid; int fields; if (sscanf(line, "Name: %63s", status->name) == 1) { status->seen_name = 1; continue; } fields = sscanf(line, "Uid: %u %u", &uid, &euid); if (fields >= 1) { status->uid = uid; status->seen_uid = 1; if (fields == 2) { status->euid = euid; status->seen_euid = 1; } continue; } if (sscanf(line, "NoNewPrivs: %lu", &status->no_new_privs) == 1) { status->seen_no_new_privs = 1; continue; } if (sscanf(line, "Seccomp: %lu", &status->seccomp) == 1) { status->seen_seccomp = 1; continue; } } fclose(f); return 0; } stevegrubb-libcap-ng-9ae4440/utils/proc-status.h000066400000000000000000000014601524174023300216470ustar00rootroot00000000000000/* * proc-status.h - Shared /proc//status helpers * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. */ #ifndef PROC_STATUS_H #define PROC_STATUS_H #include #include "gcc-attributes.h" #define PROC_STATUS_NAME_LEN 64 struct proc_status { char name[PROC_STATUS_NAME_LEN]; uid_t uid; uid_t euid; unsigned long no_new_privs; unsigned long seccomp; int seen_name; int seen_uid; int seen_euid; int seen_no_new_privs; int seen_seccomp; }; int proc_read_status(pid_t pid, struct proc_status *status) __attr_access ((__write_only__, 2)) __wur; #endif stevegrubb-libcap-ng-9ae4440/utils/pscap.8000066400000000000000000000025761524174023300204220ustar00rootroot00000000000000.\" SPDX-License-Identifier: GPL-2.0-or-later .\" Copyright (c) 2009,2019-20,2023,2026 Red Hat Inc. .TH PSCAP: "8" "March 2026" "Red Hat" "System Administration Utilities" .SH NAME pscap \- a program to see capabilities .SH SYNOPSIS .B pscap [ \-a ] [ \-p pid ] [ \-\-tree ] .SH DESCRIPTION \fBpscap\fP is a program that prints out a report of process capabilities. If the application has any capabilities, it will be in the report. By giving a pid with the \-p command line option, only the process specified with the pid is reported. If a process is not in the report, it has dropped all capabilities. If the process has partial capabilities, it is further examined to see if it has an open-ended bounding set. If this is found to be true, a '+' symbol is added. If the process has ambient capabilities, an '@' symbol is added. The command name in the output may be followed by an asterisk mark (*). This mark denotes processes which run in child user namespaces (relative to the user namespace of pscap itself). The \fB\-\-tree\fP option causes the output of the program to be mapped into a tree structure so that the relationships between processes can be studied to see what capabilities might be leaked to child processes. In tree mode, each process label is shown as command(pid:account). .SH "SEE ALSO" .BR netcap (8), .BR filecap (8), .BR capabilities (7), .BR ps (8). .SH AUTHOR Steve Grubb stevegrubb-libcap-ng-9ae4440/utils/pscap.c000066400000000000000000000326311524174023300204700ustar00rootroot00000000000000/* * pscap.c - A program that lists running processes with capabilities * Copyright (c) 2009,2012,2019-20,2023-24,2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor * Boston, MA 02110-1335, USA. * * Authors: * Steve Grubb */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include "cap-ng.h" #include "proc-account.h" #include "proc-output.h" #include "proc-sanitize.h" #include "proc-status.h" #define CMD_LEN 16 #define ACCOUNT_LEN 32 #define USERNS_MARK_LEN 3 // two characters plus '\0'. #ifndef PSCAP_NO_MAIN static void usage(void) { fprintf(stderr, "usage: pscap [-a] [-p pid] [--tree]\n"); exit(1); } struct proc_info { pid_t pid; pid_t ppid; char *cmd; char account[ACCOUNT_LEN]; char *caps_text; }; static int append_marker(char **text, const char *marker) __attr_access ((__read_write__, 1)) __attr_access ((__read_only__, 2)) __wur; static char *format_caps(int caps, bool ambient, bool bounds) __attribute_malloc__ __attr_dealloc_free __wur; /* * read_euid - read one process effective UID from procfs. * @pid: process ID whose status file should be inspected. * * Returns the effective UID on success, or (uid_t)-1 when it cannot be read. */ static uid_t read_euid(int pid) { struct proc_status status; if (proc_read_status(pid, &status) < 0 || !status.seen_euid) return (uid_t)-1; return status.euid; } /* * get_account_name - format the account name for a process effective UID. * @pid: process ID whose effective UID should be read. * @account: destination buffer receiving the account label. * @account_len: size of @account in bytes. * * Returns no value. */ static void get_account_name(int pid, char *account, size_t account_len) { uid_t euid = read_euid(pid); proc_format_account_name_from_euid(euid, account, account_len); } #endif #ifdef PSCAP_TEST /* * wrap_to - test-visible wrapper for the shared plain text wrap helper. * @text: source string to wrap. * @max: maximum number of bytes to include before wrapping. * * Returns the byte offset where output should wrap. */ size_t wrap_to(const char *text, size_t max) { return proc_wrap_plain(text, max); } #endif #ifndef PSCAP_NO_MAIN /* * utility_logic_test only needs wrap_to(). Keep the rest of the pscap * implementation out of PSCAP_NO_MAIN builds so the test object does not * accumulate main-program helpers that trigger unused-function warnings. */ /* * compare_pid - order processes by pid for sorting/bsearch * @a: pointer to left struct proc_info * @b: pointer to right struct proc_info * * Returns -1, 0, or 1 for ordering. */ static int compare_pid(const void *a, const void *b) { const struct proc_info *left = a; const struct proc_info *right = b; if (left->pid < right->pid) return -1; if (left->pid > right->pid) return 1; return 0; } /* * find_proc - locate a process record by pid * @procs: process array sorted by pid * @count: number of entries in @procs * @pid: process id to locate * * Returns pointer to the matching entry or NULL if not found. */ static void *find_proc(struct proc_info *procs, size_t count, pid_t pid) { struct proc_info key; key.pid = pid; return bsearch(&key, procs, count, sizeof(*procs), compare_pid); } /* * append_marker - append a marker string to the capability text * @text: capability text buffer pointer to append to * @marker: marker string to append (e.g. " @" or " +") * * Returns 0 on success, -1 on allocation failure. */ static int append_marker(char **text, const char *marker) { size_t len = strlen(*text); size_t marker_len = strlen(marker); char *tmp = realloc(*text, len + marker_len + 1); if (!tmp) return -1; memcpy(tmp + len, marker, marker_len + 1); *text = tmp; return 0; } /* * format_caps - format capability text with optional markers * @caps: capability summary state from capng_have_capabilities() * @ambient: true if ambient capabilities are present * @bounds: true if bounding set differs from full * * Returns allocated capability string or NULL on allocation failure. */ static char *format_caps(int caps, bool ambient, bool bounds) { char *text; if (caps == CAPNG_PARTIAL) text = capng_print_caps_text(CAPNG_PRINT_BUFFER, CAPNG_PERMITTED); else if (caps == CAPNG_FULL) text = strdup("full"); else text = strdup("none"); if (!text) return NULL; if (ambient && append_marker(&text, " @") < 0) { free(text); return NULL; } if (bounds && append_marker(&text, " +") < 0) { free(text); return NULL; } return text; } /* * print_tree_node - render a node and its children in tree mode * @procs: process array * @count: number of entries in @procs * @index: index of current node in @procs * @prefix: current line prefix * @is_last: true if this node is the last child of its parent * @is_root: true if this node is a tree root * * Returns nothing. Recurses through children to emit full subtree. */ static void print_tree_node(struct proc_info *procs, size_t count, size_t index, const char *prefix, bool is_last, bool is_root, int width) { struct proc_info *proc = &procs[index]; size_t child_total = 0; size_t child_seen = 0; size_t i; char head[64]; const char *branch = ""; char *text; size_t text_len; char *line_prefix; char *cont_prefix; if (!is_root) branch = is_last ? " " : "│ "; line_prefix = malloc(strlen(prefix) + strlen(is_root ? "" : (is_last ? "└─ " : "├─ ")) + 1); if (!line_prefix) return; strcpy(line_prefix, prefix); if (!is_root) strcat(line_prefix, is_last ? "└─ " : "├─ "); cont_prefix = malloc(strlen(prefix) + strlen(branch) + 1); if (!cont_prefix) { free(line_prefix); return; } strcpy(cont_prefix, prefix); strcat(cont_prefix, branch); snprintf(head, sizeof(head), "%s(%d:%s) [", proc->cmd, proc->pid, proc->account); text_len = strlen(head) + strlen(proc->caps_text) + 2; text = malloc(text_len); if (!text) { free(line_prefix); free(cont_prefix); return; } snprintf(text, text_len, "%s%s]", head, proc->caps_text); proc_print_wrapped(line_prefix, cont_prefix, text, width); free(text); free(line_prefix); for (i = 0; i < count; i++) { if (procs[i].ppid == proc->pid) child_total++; } if (child_total == 0) { free(cont_prefix); return; } for (i = 0; i < count; i++) { if (procs[i].ppid != proc->pid) continue; child_seen++; print_tree_node(procs, count, i, cont_prefix, child_seen == child_total, false, width); } free(cont_prefix); } /* * print_tree - render all process trees in pid order * @procs: process array * @count: number of entries in @procs * * Returns nothing. Each tree starts at a pid whose parent isn't present. */ static void print_tree(struct proc_info *procs, size_t count) { size_t i; int width = proc_output_width(); if (count > 1) qsort(procs, count, sizeof(*procs), compare_pid); for (i = 0; i < count; i++) { if (!find_proc(procs, count, procs[i].ppid)) print_tree_node(procs, count, i, "", true, true, width); } } /* * Precise recursive checks for parent-child relation between namespaces * using ioctl() were avoided, because there didn't seem to be any case when * we may dereference the namespace symlink in /proc/PID/ns for processes in * user namespaces other than the current or child ones. Thus, the check just * tries to dereference the link and checks that it does not point to the * current NS. */ static bool in_child_userns(int pid) { char ns_file_path[32]; struct stat statbuf; ino_t own_ns_inode; dev_t own_ns_dev; if (stat("/proc/self/ns/user", &statbuf) < 0) return false; own_ns_inode = statbuf.st_ino; own_ns_dev = statbuf.st_dev; snprintf(ns_file_path, sizeof(ns_file_path), "/proc/%d/ns/user", pid); if (stat(ns_file_path, &statbuf) < 0) return false; return statbuf.st_ino != own_ns_inode || statbuf.st_dev != own_ns_dev; } #ifndef PSCAP_NO_MAIN int main(int argc, char *argv[]) { char *endptr = NULL; DIR *d; struct dirent *ent; int header = 0, show_all = 0, caps; pid_t our_pid = getpid(); pid_t target_pid = 0; uid_t last_uid = (uid_t)-1; const char *name = NULL; int tree_mode = 0; struct proc_info *procs = NULL; size_t proc_count = 0; size_t proc_capacity = 0; size_t i; for (i = 1; i < (size_t)argc; i++) { if (strcmp(argv[i], "-a") == 0) { show_all = 1; continue; } if (strcmp(argv[i], "--tree") == 0) { tree_mode = 1; continue; } if (strcmp(argv[i], "-p") == 0) { long parsed_pid; if (i + 1 >= (size_t)argc) usage(); errno = 0; parsed_pid = strtol(argv[++i], &endptr, 10); if (errno) { fprintf(stderr, "Can't read pid: %s\n", argv[i]); return 1; } if ((endptr == argv[i]) || (*endptr != '\0') || parsed_pid <= 0 || parsed_pid > INT_MAX) { fprintf(stderr, "Invalid pid argument: %s\n", argv[i]); return 1; } target_pid = (pid_t)parsed_pid; if (target_pid == 1) show_all = 1; continue; } usage(); } d = opendir("/proc"); if (d == NULL) { fprintf(stderr, "Can't open /proc: %s\n", strerror(errno)); return 1; } while (( ent = readdir(d) )) { int pid, ppid; char buf[100]; char *safe_cmd = NULL; char *tmp, cmd[CMD_LEN + USERNS_MARK_LEN], state; int fd, len; // Skip non-process dir entries if(*ent->d_name<'0' || *ent->d_name>'9') continue; errno = 0; pid = strtol(ent->d_name, NULL, 10); if (errno) continue; if (target_pid && (pid != target_pid)) continue; /* Skip our pid so we aren't listed */ if (pid == our_pid) continue; // Parse up the stat file for the proc snprintf(buf, sizeof(buf), "/proc/%d/stat", pid); fd = open(buf, O_RDONLY|O_CLOEXEC, 0); if (fd < 0) continue; len = read(fd, buf, sizeof(buf) - 1); close(fd); if (len < 40) continue; buf[len] = 0; tmp = strrchr(buf, ')'); if (tmp) *tmp = 0; else continue; memset(cmd, 0, sizeof(cmd)); sscanf(buf, "%d (%15c", &ppid, cmd); // ppid is throwaway sscanf(tmp+2, "%c %d", &state, &ppid); // Skip kthreads if (pid == 2 || ppid == 2) continue; // now get the capabilities capng_clear(CAPNG_SELECT_ALL); capng_setpid(pid); if (capng_get_caps_process()) continue; // And print out anything with capabilities caps = capng_have_capabilities(CAPNG_SELECT_CAPS); safe_cmd = sanitize_untrusted_field(cmd); if (!safe_cmd) continue; if (in_child_userns(pid)) { char *marked = malloc(strlen(safe_cmd) + 3); if (!marked) { free(safe_cmd); continue; } snprintf(marked, strlen(safe_cmd) + 3, "%s *", safe_cmd); free(safe_cmd); safe_cmd = marked; } if (tree_mode) { char *caps_text; bool has_ambient; bool has_bounds; if (!show_all && caps <= CAPNG_NONE) { free(safe_cmd); continue; } has_ambient = capng_have_capabilities( CAPNG_SELECT_AMBIENT) > CAPNG_NONE; has_bounds = capng_have_capabilities( CAPNG_SELECT_BOUNDS) > CAPNG_NONE; caps_text = format_caps(caps, has_ambient, has_bounds); if (!caps_text) { free(safe_cmd); continue; } if (proc_count == proc_capacity) { size_t new_capacity = proc_capacity ? proc_capacity * 2 : 256; struct proc_info *pi_tmp; pi_tmp = realloc(procs, new_capacity * sizeof(*procs)); if (!pi_tmp) { free(caps_text); free(safe_cmd); continue; } procs = pi_tmp; proc_capacity = new_capacity; } procs[proc_count].pid = pid; procs[proc_count].ppid = ppid; procs[proc_count].cmd = safe_cmd; get_account_name(pid, procs[proc_count].account, sizeof(procs[proc_count].account)); procs[proc_count].caps_text = caps_text; proc_count++; safe_cmd = NULL; } else if (caps > CAPNG_NONE) { char *caps_text; bool has_ambient; bool has_bounds; uid_t euid = read_euid(pid); if (header == 0) { printf("%-7s %-7s %-16s %-15s %s\n", "ppid", "pid", "uid", "command", "capabilities"); header = 1; } proc_update_account_cache(euid, &last_uid, &name); has_ambient = capng_have_capabilities( CAPNG_SELECT_AMBIENT) > CAPNG_NONE; has_bounds = capng_have_capabilities( CAPNG_SELECT_BOUNDS) > CAPNG_NONE; caps_text = format_caps(caps, has_ambient, has_bounds); if (!caps_text) { free(safe_cmd); continue; } if (name) { printf("%-7d %-7d %-16s %-15s ", ppid, pid, name, safe_cmd); } else printf("%-7d %-7d %-16u %-15s ", ppid, pid, (unsigned int)last_uid, safe_cmd); printf("%s\n", caps_text); free(caps_text); free(safe_cmd); safe_cmd = NULL; } if (safe_cmd) free(safe_cmd); } closedir(d); if (tree_mode) { print_tree(procs, proc_count); for (i = 0; i < proc_count; i++) { free(procs[i].cmd); free(procs[i].caps_text); } free(procs); } return 0; } #endif #endif stevegrubb-libcap-ng-9ae4440/utils/test/000077500000000000000000000000001524174023300201705ustar00rootroot00000000000000stevegrubb-libcap-ng-9ae4440/utils/test/Makefile.am000066400000000000000000000015741524174023300222330ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0-or-later # Makefile.am -- utility tests # # Copyright 2026 Red Hat Inc. # All Rights Reserved. AM_CPPFLAGS = -I${top_srcdir} -I${top_srcdir}/src -I${top_srcdir}/utils \ -I${top_srcdir}/utils/cap-audit check_PROGRAMS = utils_test utility_logic_test TESTS = $(check_PROGRAMS) utils_test_SOURCES = utils_test.c \ ${top_srcdir}/utils/filecap-path.c \ ${top_srcdir}/utils/proc-account.c \ ${top_srcdir}/utils/proc-status.c utility_logic_test_SOURCES = utility_logic_test.c \ ${top_srcdir}/utils/pscap.c \ ${top_srcdir}/utils/netcap.c \ ${top_srcdir}/utils/proc-account.c \ ${top_srcdir}/utils/proc-llist.c \ ${top_srcdir}/utils/proc-output.c \ ${top_srcdir}/utils/proc-sanitize.c utility_logic_test_CPPFLAGS = $(AM_CPPFLAGS) -DPSCAP_NO_MAIN -DPSCAP_TEST \ -DNETCAP_NO_MAIN -DNETCAP_TEST utility_logic_test_LDADD = ${top_builddir}/src/libcap-ng.la stevegrubb-libcap-ng-9ae4440/utils/test/utility_logic_test.c000066400000000000000000000067551524174023300242700ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* utility_logic_test.c -- direct tests against utility translation units * Copyright 2026 Red Hat Inc. * All Rights Reserved. * * This test links the real pscap.c and netcap.c objects with their main() * functions compiled out. That keeps make check close to production code * for utility-local helpers that are not worth moving into shared modules. */ #include "config.h" #include #include #include #include #include "proc-llist.h" size_t wrap_to(const char *text, size_t max); int parse_u32_hex_or_dec(const char *s, unsigned int *out); static void fail(const char *msg) { fprintf(stderr, "%s\n", msg); exit(EXIT_FAILURE); } static void test_wrap_to(void) { /* pscap tree output should wrap cleanly on separators when possible. */ if (wrap_to("cap_chown, cap_setuid", 12) != 11) fail("wrap_to should break after comma"); if (wrap_to("alpha beta", 7) != 6) fail("wrap_to should prefer spaces"); if (wrap_to("abcdefgh", 4) != 4) fail("wrap_to should hard-wrap when no separator exists"); } static void test_parse_u32_hex_or_dec(void) { unsigned int out; /* netcap accepts decimal, 0x-prefixed hex, and some procfs hex forms. */ if (parse_u32_hex_or_dec("123", &out) != 0 || out != 123) fail("decimal parse failed"); if (parse_u32_hex_or_dec("0x10", &out) != 0 || out != 16) fail("hex parse with prefix failed"); if (parse_u32_hex_or_dec("0010", &out) != 0 || out != 16) fail("hex parse with leading zero failed"); if (parse_u32_hex_or_dec("G1", &out) == 0) fail("invalid parse should fail"); if (parse_u32_hex_or_dec("4294967295", &out) != 0 || out != UINT_MAX) fail("maximum u32 parse failed"); if (parse_u32_hex_or_dec("4294967296", &out) == 0) fail("overflow parse should fail"); if (parse_u32_hex_or_dec("+1", &out) == 0) fail("positive sign parse should fail"); if (parse_u32_hex_or_dec("-1", &out) == 0) fail("negative parse should fail"); } static void test_list_inode_iteration(void) { llist list; lnode first = { 0 }; lnode second = { 0 }; lnode third = { 0 }; lnode *cur; list_create(&list); first.inode = 99; first.cmd = strdup("first"); first.capabilities = strdup("cap_net_bind_service"); first.bounds = strdup(""); first.ambient = strdup(""); second.inode = 99; second.cmd = strdup("second"); second.capabilities = strdup("cap_net_admin"); second.bounds = strdup(""); second.ambient = strdup(""); third.inode = 100; third.cmd = strdup("third"); third.capabilities = strdup("cap_sys_admin"); third.bounds = strdup(""); third.ambient = strdup(""); if (!first.cmd || !first.capabilities || !first.bounds || !first.ambient || !second.cmd || !second.capabilities || !second.bounds || !second.ambient || !third.cmd || !third.capabilities || !third.bounds || !third.ambient) fail("allocation failed in inode iteration test"); list_append(&list, &first); list_append(&list, &second); list_append(&list, &third); cur = list_find_inode(&list, 99); if (!cur || strcmp(cur->cmd, "first") != 0) fail("list_find_inode should return first matching inode"); cur = list_next_inode(&list, 99); if (!cur || strcmp(cur->cmd, "second") != 0) fail("list_next_inode should return later matching inode"); if (list_next_inode(&list, 99) != NULL) fail("list_next_inode should stop at the last match"); list_clear(&list); } int main(void) { test_wrap_to(); test_parse_u32_hex_or_dec(); test_list_inode_iteration(); puts("Direct utility logic tests passed"); return 0; } stevegrubb-libcap-ng-9ae4440/utils/test/utils_test.c000066400000000000000000000103541524174023300225360ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later /* utils_test.c -- utility helper regression tests * Copyright 2026 Red Hat Inc. * All Rights Reserved. * * These tests cover edge cases that were previously untested in make check: * PATH parsing for filecap, formatting of unknown proc owners, and netcap's * cached passwd lookup behavior. They are intentionally narrow and avoid * depending on live /proc traversal so the failure modes stay reproducible. */ #include "config.h" #include #include #include #include #include #include #include #include "filecap-path.h" #include "proc-account.h" #include "proc-status.h" struct path_list { char **items; size_t count; size_t cap; }; static void fail(const char *msg) { fprintf(stderr, "%s\n", msg); exit(EXIT_FAILURE); } static int append_path(const char *entry, void *data) { struct path_list *list = data; char **tmp; if (list->count == list->cap) { size_t new_cap = list->cap ? list->cap * 2 : 4; tmp = realloc(list->items, new_cap * sizeof(*tmp)); if (tmp == NULL) return -1; list->items = tmp; list->cap = new_cap; } list->items[list->count] = strdup(entry); if (list->items[list->count] == NULL) return -1; list->count++; return 0; } static void free_path_list(struct path_list *list) { size_t i; for (i = 0; i < list->count; i++) free(list->items[i]); free(list->items); } static uid_t find_missing_uid(void) { uid_t uid; /* * Exercise the range that a signed int cannot represent. UID_MAX is * reserved as the invalid UID sentinel, so stop before reaching it. */ for (uid = (uid_t)INT_MAX + 1U; uid != (uid_t)-1; uid++) { if (getpwuid(uid) == NULL) return uid; } fail("Failed to find an unmapped uid above INT_MAX for cache test"); return 0; } static void test_path_parser(void) { static const char *expected[] = { ".", "/usr/bin", ".", "/bin", "." }; struct path_list list = { 0 }; size_t i; if (filecap_foreach_path(":/usr/bin::/bin:", append_path, &list) != 0) fail("PATH parser failed"); if (list.count != sizeof(expected) / sizeof(expected[0])) fail("PATH parser returned unexpected entry count"); for (i = 0; i < list.count; i++) { if (strcmp(list.items[i], expected[i]) != 0) fail("PATH parser lost or reordered entries"); } free_path_list(&list); } static void test_account_formatting(void) { char account[32]; char expected[32]; uid_t missing_uid = find_missing_uid(); uid_t last_uid = 0; const char *cached_name = "root"; /* Unknown proc owners should stay explicit instead of becoming root. */ proc_format_account_name_from_euid((uid_t)-1, account, sizeof(account)); if (strcmp(account, "unknown") != 0) fail("Invalid euid should format as unknown"); proc_format_account_name_from_euid(0, account, sizeof(account)); if (strcmp(account, "root") != 0) fail("Root euid should format as root"); snprintf(expected, sizeof(expected), "%u", (unsigned int)missing_uid); proc_format_account_name_from_euid(missing_uid, account, sizeof(account)); if (strcmp(account, expected) != 0) fail("Large unmapped euid should retain its numeric value"); /* A failed lookup must not leave the previous cached name in place. */ netcap_update_account_cache(missing_uid, &last_uid, &cached_name); if (cached_name != NULL) fail("Missing passwd entry should clear stale cached name"); if (last_uid != missing_uid) fail("Missing passwd entry should update last_uid"); } /* * test_proc_status - verify shared /proc status parsing against this process. * @none: function takes no parameters. * * Returns no value. Calls fail() when expected status fields are missing. */ static void test_proc_status(void) { struct proc_status status; if (proc_read_status(getpid(), &status) != 0) fail("Failed to read own proc status"); if (!status.seen_name || status.name[0] == '\0') fail("Proc status should include a process name"); if (!status.seen_uid || status.uid != getuid()) fail("Proc status should include the real uid"); if (!status.seen_euid || status.euid != geteuid()) fail("Proc status should include the effective uid"); } int main(void) { test_path_parser(); test_account_formatting(); test_proc_status(); puts("Utility helper tests passed"); return 0; }