Version in base suite: 0.11.0-2+deb13u1 Base version: bubblewrap_0.11.0-2+deb13u1 Target version: bubblewrap_0.12.0-1~deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/b/bubblewrap/bubblewrap_0.11.0-2+deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/b/bubblewrap/bubblewrap_0.12.0-1~deb13u1.dsc .github/workflows/check.yml | 2 COPYING | 203 - COPYING.LIB | 502 ++++ LICENSE | 203 - NEWS.md | 80 README.md | 25 SECURITY.md | 17 bind-mount.c | 111 bind-mount.h | 12 bubblewrap.c | 1251 ++++------ bwrap.xml | 45 chroot_realpath.c | 195 + ci/enable-userns.sh | 6 debian/NEWS | 15 debian/README.Debian | 121 debian/changelog | 112 debian/control | 6 debian/copyright | 23 debian/patches/CVE-2026-41163/Don-t-run-the-privilege-separated-code-dumpable.patch | 89 debian/patches/CVE-2026-41163/fix-harden-privsep-parent-against-unexpected-operations.patch | 36 debian/patches/debian/Change-EPERM-error-message-to-show-Debian-specific-inform.patch | 12 debian/patches/series | 2 debian/rules | 4 debian/salsa-ci.yml | 7 meson.build | 16 meson_options.txt | 12 release-checklist.md | 6 safe_openat.c | 332 ++ tests/libtest-core.sh | 4 tests/libtest.sh | 4 tests/meson.build | 1 tests/sandbox_helper.py | 90 tests/test-helper.py | 270 ++ tests/test-run.sh | 13 tests/test-sandbox.py | 690 +++++ tests/test-seccomp.py | 2 tests/test-utils.c | 4 tests/try-syscall.c | 2 tests/use-as-subproject/assert-correct-rpath.py | 2 utils.c | 52 utils.h | 81 41 files changed, 3438 insertions(+), 1222 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpb9guugxj/bubblewrap_0.11.0-2+deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpb9guugxj/bubblewrap_0.12.0-1~deb13u1.dsc: no acceptable signature found diff -Nru bubblewrap-0.11.0/.github/workflows/check.yml bubblewrap-0.12.0/.github/workflows/check.yml --- bubblewrap-0.11.0/.github/workflows/check.yml 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/.github/workflows/check.yml 2026-08-26 10:08:29.000000000 +0000 @@ -17,6 +17,8 @@ uses: actions/checkout@v4 - name: Install build-dependencies run: sudo ./ci/builddeps.sh + - name: Enable user namespaces + run: sudo ./ci/enable-userns.sh - name: Create logs dir run: mkdir test-logs - name: setup diff -Nru bubblewrap-0.11.0/COPYING bubblewrap-0.12.0/COPYING --- bubblewrap-0.11.0/COPYING 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/COPYING 2026-08-26 10:08:29.000000000 +0000 @@ -1,13 +1,14 @@ - GNU LIBRARY GENERAL PUBLIC LICENSE - Version 2, June 1991 + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 - Copyright (C) 1991 Free Software Foundation, Inc. + 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 library GPL. It is - numbered 2 because it goes with version 2 of the ordinary GPL.] +[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 @@ -16,97 +17,109 @@ 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 Library General Public License, applies to some -specially designated Free Software Foundation software, and to any -other libraries whose authors decide to use it. You can use it for -your libraries, 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. + 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 -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 library, or if you modify it. +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 a program 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 +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. - Our method of protecting your rights has two steps: (1) copyright -the library, and (2) offer you this license which gives you legal + 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. - Also, for each distributor's protection, we want to make certain -that everyone understands that there is no warranty for this free -library. If the library is modified by someone else and passed on, we -want its recipients to know that what they have is not the original -version, so that any problems introduced by others will not reflect on -the original authors' reputations. + 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. - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that companies distributing free -software will individually obtain patent licenses, thus in effect -transforming the program into proprietary software. To prevent this, -we have made it clear that any patent must be licensed for everyone's -free use or not licensed at all. - - Most GNU software, including some libraries, is covered by the ordinary -GNU General Public License, which was designed for utility programs. This -license, the GNU Library General Public License, applies to certain -designated libraries. This license is quite different from the ordinary -one; be sure to read it in full, and don't assume that anything in it is -the same as in the ordinary license. - - The reason we have a separate public license for some libraries is that -they blur the distinction we usually make between modifying or adding to a -program and simply using it. Linking a program with a library, without -changing the library, is in some sense simply using the library, and is -analogous to running a utility program or application program. However, in -a textual and legal sense, the linked executable is a combined work, a -derivative of the original library, and the ordinary General Public License -treats it as such. - - Because of this blurred distinction, using the ordinary General -Public License for libraries did not effectively promote software -sharing, because most developers did not use the libraries. We -concluded that weaker conditions might promote sharing better. - - However, unrestricted linking of non-free programs would deprive the -users of those programs of all benefit from the free status of the -libraries themselves. This Library General Public License is intended to -permit developers of non-free programs to use free libraries, while -preserving your freedom as a user of such programs to change the free -libraries that are incorporated in them. (We have not seen how to achieve -this as regards changes in header files, but we have achieved it as regards -changes in the actual functions of the Library.) The hope is that this -will lead to faster development of free libraries. + 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, while the latter only -works together with the library. - - Note that it is possible for a library to be covered by the ordinary -General Public License rather than by this special one. +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. - GNU LIBRARY GENERAL PUBLIC LICENSE + GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - 0. This License Agreement applies to any software library which -contains a notice placed by the copyright holder or other authorized -party saying it may be distributed under the terms of this Library -General Public License (also called "this License"). Each licensee is -addressed as "you". + 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 @@ -133,7 +146,7 @@ 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 @@ -255,7 +268,7 @@ Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. - 6. As an exception to the Sections above, you may also compile or + 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 @@ -282,23 +295,31 @@ Library will not necessarily be able to recompile the application to use the modified definitions.) - b) Accompany the work with a written offer, valid for at + 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. - c) If distribution of the work is made by offering access to copy + 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. - d) Verify that the user has already received a copy of these + 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 source code distributed need not include anything that is normally -distributed (in either source or binary form) with the major +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. @@ -347,7 +368,7 @@ 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 to +You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent @@ -390,7 +411,7 @@ written in the body of this License. 13. The Free Software Foundation may publish revised and/or new -versions of the Library General Public License from time to time. +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. @@ -453,16 +474,16 @@ Copyright (C) This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public + 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. + 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 - Library General Public License for more details. + Lesser General Public License for more details. - You should have received a copy of the GNU Library General Public + 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 diff -Nru bubblewrap-0.11.0/COPYING.LIB bubblewrap-0.12.0/COPYING.LIB --- bubblewrap-0.11.0/COPYING.LIB 1970-01-01 00:00:00.000000000 +0000 +++ bubblewrap-0.12.0/COPYING.LIB 2026-08-26 10:08:29.000000000 +0000 @@ -0,0 +1,502 @@ + 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. + + 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. + + 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. + + 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. + + 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. + + 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. + + 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. + + 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 + + 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.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., 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! diff -Nru bubblewrap-0.11.0/LICENSE bubblewrap-0.12.0/LICENSE --- bubblewrap-0.11.0/LICENSE 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/LICENSE 2026-08-26 10:08:29.000000000 +0000 @@ -1,13 +1,14 @@ - GNU LIBRARY GENERAL PUBLIC LICENSE - Version 2, June 1991 + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 - Copyright (C) 1991 Free Software Foundation, Inc. + 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 library GPL. It is - numbered 2 because it goes with version 2 of the ordinary GPL.] +[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 @@ -16,97 +17,109 @@ 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 Library General Public License, applies to some -specially designated Free Software Foundation software, and to any -other libraries whose authors decide to use it. You can use it for -your libraries, 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. + 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 -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 library, or if you modify it. +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 a program 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 +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. - Our method of protecting your rights has two steps: (1) copyright -the library, and (2) offer you this license which gives you legal + 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. - Also, for each distributor's protection, we want to make certain -that everyone understands that there is no warranty for this free -library. If the library is modified by someone else and passed on, we -want its recipients to know that what they have is not the original -version, so that any problems introduced by others will not reflect on -the original authors' reputations. + 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. - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that companies distributing free -software will individually obtain patent licenses, thus in effect -transforming the program into proprietary software. To prevent this, -we have made it clear that any patent must be licensed for everyone's -free use or not licensed at all. - - Most GNU software, including some libraries, is covered by the ordinary -GNU General Public License, which was designed for utility programs. This -license, the GNU Library General Public License, applies to certain -designated libraries. This license is quite different from the ordinary -one; be sure to read it in full, and don't assume that anything in it is -the same as in the ordinary license. - - The reason we have a separate public license for some libraries is that -they blur the distinction we usually make between modifying or adding to a -program and simply using it. Linking a program with a library, without -changing the library, is in some sense simply using the library, and is -analogous to running a utility program or application program. However, in -a textual and legal sense, the linked executable is a combined work, a -derivative of the original library, and the ordinary General Public License -treats it as such. - - Because of this blurred distinction, using the ordinary General -Public License for libraries did not effectively promote software -sharing, because most developers did not use the libraries. We -concluded that weaker conditions might promote sharing better. - - However, unrestricted linking of non-free programs would deprive the -users of those programs of all benefit from the free status of the -libraries themselves. This Library General Public License is intended to -permit developers of non-free programs to use free libraries, while -preserving your freedom as a user of such programs to change the free -libraries that are incorporated in them. (We have not seen how to achieve -this as regards changes in header files, but we have achieved it as regards -changes in the actual functions of the Library.) The hope is that this -will lead to faster development of free libraries. + 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, while the latter only -works together with the library. - - Note that it is possible for a library to be covered by the ordinary -General Public License rather than by this special one. +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. - GNU LIBRARY GENERAL PUBLIC LICENSE + GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - 0. This License Agreement applies to any software library which -contains a notice placed by the copyright holder or other authorized -party saying it may be distributed under the terms of this Library -General Public License (also called "this License"). Each licensee is -addressed as "you". + 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 @@ -133,7 +146,7 @@ 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 @@ -255,7 +268,7 @@ Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. - 6. As an exception to the Sections above, you may also compile or + 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 @@ -282,23 +295,31 @@ Library will not necessarily be able to recompile the application to use the modified definitions.) - b) Accompany the work with a written offer, valid for at + 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. - c) If distribution of the work is made by offering access to copy + 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. - d) Verify that the user has already received a copy of these + 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 source code distributed need not include anything that is normally -distributed (in either source or binary form) with the major +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. @@ -347,7 +368,7 @@ 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 to +You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent @@ -390,7 +411,7 @@ written in the body of this License. 13. The Free Software Foundation may publish revised and/or new -versions of the Library General Public License from time to time. +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. @@ -453,16 +474,16 @@ Copyright (C) This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public + 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. + 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 - Library General Public License for more details. + Lesser General Public License for more details. - You should have received a copy of the GNU Library General Public + 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 diff -Nru bubblewrap-0.11.0/NEWS.md bubblewrap-0.12.0/NEWS.md --- bubblewrap-0.11.0/NEWS.md 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/NEWS.md 2026-08-26 10:08:29.000000000 +0000 @@ -1,3 +1,83 @@ +bubblewrap 0.12.0 +================= + +Released: 2026-08-26 + +Enhancements: + + * The flag --not-a-security-boundary was added. If this is enabled + then failure of some sandbox setup steps (like remounting a + submount) are not fatal. + + * The license has been updated from LGPL 2.0 (or later) to LGPL 2.1 + (or later). + + * This version removes the support for building a setuid + bubblewrap. Changes in this version made it difficult to support + and basically all modern linux distributions now support + unprivileged user namespaces to some extent. + + * The assume_kernel build option was added, if specified no backwards + compatiblity for kernels older than this is built in (and will result + in hard failures at runtime). Currently specifying 5.6.0 or + later will disable the fallback implementation of + openat2(RESOLVE_IN_ROOT). + +Bug fixes: + + * Bubblewrap now correctly resolves absolute symlinks during the + sandbox setup by using openat2 with RESOLVE_IN_ROOT (or a fallback + implementation). This fixes a security issue (GHSA-pxhw-h44j-8pfx) + where file or directories created during sandbox setup could + follow parent symlinks out of the sandbox. + +bubblewrap 0.11.2 +================= + +Released: 2026-04-23 + +Bug fixes: + + * In setuid mode, don't run the low-privileged parts parts of the setup + as dumpable, as that allows it to be ptraced which can lead to problems. + This is CVE-2026-41163, and was reported by François Diakhate. + +Enhancements: + + * New build option `-Dsupport_setuid`, which if set to false (which + is the default) disables the support for setuid. Binaries built + with this will refuse to run if made setuid. We recommend building + normal bubblewrap binaries like this, which allows you to safely + ignore any security issues that only affect setuid mode. + +bubblewrap 0.11.1 +================= + +Released: 2026-03-21 + +Bug fixes: + + * Reset disposition of `SIGCHLD`, restoring normal subprocess management + if bwrap was run from a process that was ignoring that signal, + such as Erlang or volumeicon (#705, Joel Pelaez Jorge) + + * Don't ignore `--userns 0`, `--userns2 0` or `--pidns 0` if used + (#731, Daniel Cazares). + Note that using a fd number ≥ 3 for these purposes is still + preferred, to avoid confusion with the stdin, stdout, stderr + that will be inherited by the command inside the container. + + * Fix grammar in an error message (#694, J. Neuschäfer) + + * Fix a broken link in the documentation (#729, Aaron Brooks) + +Internal changes: + + * Enable user namespaces in Github Actions configuration, fixing a CI + regression with newer Ubuntu (#728, Joel Pelaez Jorge) + + * Clarify comments (#737, Simon McVittie) + bubblewrap 0.11.0 ================= diff -Nru bubblewrap-0.11.0/README.md bubblewrap-0.12.0/README.md --- bubblewrap-0.11.0/README.md 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/README.md 2026-08-26 10:08:29.000000000 +0000 @@ -12,23 +12,14 @@ User namespaces --------------- -There is an effort in the Linux kernel called +There is a feature in the Linux kernel called [user namespaces](https://www.google.com/search?q=user+namespaces+site%3Ahttps%3A%2F%2Flwn.net) -which attempts to allow unprivileged users to use container features. -While significant progress has been made, there are -[still concerns](https://lwn.net/Articles/673597/) about it, and -it is not available to unprivileged users in several production distributions -such as CentOS/Red Hat Enterprise Linux 7, Debian Jessie, etc. - -See for example -[CVE-2016-3135](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-3135) -which is a local root vulnerability introduced by userns. -[This March 2016 post](https://lkml.org/lkml/2016/3/9/555) has some -more discussion. - -Bubblewrap could be viewed as setuid implementation of a *subset* of -user namespaces. Emphasis on subset - specifically relevant to the -above CVE, bubblewrap does not allow control over iptables. +which allows unprivileged users to use container features. Bubblewrap uses these to +build the sandbox, allowing any user to use the tool. + +Historically, bubblewrap also supported a setuid mode for systems where +unprivileged user namespaces were not supported. However, this has been +removed. The original bubblewrap code existed before user namespaces - it inherits code from [xdg-app helper](https://cgit.freedesktop.org/xdg-app/xdg-app/tree/common/xdg-app-helper.c?id=4c3bf179e2e4a2a298cd1db1d045adaf3f564532) @@ -151,7 +142,7 @@ IPC namespaces ([CLONE_NEWIPC](https://linux.die.net/man/2/clone)): The sandbox will get its own copy of all the different forms of IPCs, like SysV shared memory and semaphores. -PID namespaces ([CLONE_NEWPID](https://linux.die.net/man/2/clone)): The sandbox will not see any processes outside the sandbox. Additionally, bubblewrap will run a trivial pid1 inside your container to handle the requirements of reaping children in the sandbox. This avoids what is known now as the [Docker pid 1 problem](https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/). +PID namespaces ([CLONE_NEWPID](https://linux.die.net/man/2/clone)): The sandbox will not see any processes outside the sandbox. Additionally, bubblewrap will run a trivial pid1 inside your container to handle the requirements of reaping children in the sandbox. This avoids what is known now as the [Docker pid 1 problem](https://blog.phusion.nl/docker-and-the-pid-1-zombie-reaping-problem/). Network namespaces ([CLONE_NEWNET](https://linux.die.net/man/2/clone)): The sandbox will not see the network. Instead it will have its own network namespace with only a loopback device. diff -Nru bubblewrap-0.11.0/SECURITY.md bubblewrap-0.12.0/SECURITY.md --- bubblewrap-0.11.0/SECURITY.md 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/SECURITY.md 2026-08-26 10:08:29.000000000 +0000 @@ -4,16 +4,15 @@ ### System security -If bubblewrap is setuid root, then the goal is that it does not allow -a malicious local user to do anything that would not have been possible -on a kernel that allows unprivileged users to create new user namespaces. -For example, [CVE-2020-5291](https://github.com/containers/bubblewrap/security/advisories/GHSA-j2qp-rvxj-43vj) -was treated as a security vulnerability in bubblewrap. +bubblewrap is not a security boundary between the user and the OS, +because anything bubblewrap could do, a malicious user could equally +well do by writing their own tool equivalent to bubblewrap. -If bubblewrap is not setuid root, then it is not a security boundary -between the user and the OS, because anything bubblewrap could do, a -malicious user could equally well do by writing their own tool equivalent -to bubblewrap. +Older versions of bubblewrap were optionally setuid root. This is a +system security risk. See +https://github.com/containers/bubblewrap/blob/v0.11.2/SECURITY.md#system-security +for discussion of this historical configuration. Newer versions of +bubblewrap refuse to operate if the binary has been made setuid. ### Sandbox security diff -Nru bubblewrap-0.11.0/bind-mount.c bubblewrap-0.12.0/bind-mount.c --- bubblewrap-0.11.0/bind-mount.c 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/bind-mount.c 2026-08-26 10:08:29.000000000 +0000 @@ -227,8 +227,7 @@ } static MountTab -parse_mountinfo (int proc_fd, - const char *root_mount) +parse_mountinfo (const char *root_mount) { cleanup_free char *mountinfo = NULL; cleanup_free MountInfoLine *lines = NULL; @@ -375,12 +374,40 @@ } bind_mount_result -bind_mount (int proc_fd, - const char *src, +bind_mount (const char *src, const char *dest, bind_option_t options, char **failing_path) { + cleanup_fd int dest_fd = + TEMP_FAILURE_RETRY (open (dest, O_PATH | O_CLOEXEC)); + + if (dest_fd < 0) + { + if (failing_path != NULL) + *failing_path = xstrdup (dest); + return BIND_MOUNT_ERROR_OPEN_FD; + } + + cleanup_fd int src_fd = + TEMP_FAILURE_RETRY (open (src, O_PATH | O_CLOEXEC)); + + if (src_fd < 0) + { + if (failing_path != NULL) + *failing_path = xstrdup (src); + return BIND_MOUNT_ERROR_OPEN_FD; + } + + return bind_mount_fd (src_fd, dest_fd, options, failing_path); +} + +bind_mount_result +bind_mount_fd (int src_fd, + int dest_fd, + bind_option_t options, + char **failing_path) +{ bool readonly = (options & BIND_READONLY) != 0; bool devices = (options & BIND_DEVICES) != 0; bool recursive = (options & BIND_RECURSIVE) != 0; @@ -388,31 +415,9 @@ cleanup_mount_tab MountTab mount_tab = NULL; cleanup_free char *resolved_dest = NULL; cleanup_free char *dest_proc = NULL; - cleanup_free char *oldroot_dest_proc = NULL; - cleanup_free char *kernel_case_combination = NULL; - cleanup_fd int dest_fd = -1; int i; - if (src) - { - if (mount (src, dest, NULL, MS_SILENT | MS_BIND | (recursive ? MS_REC : 0), NULL) != 0) - return BIND_MOUNT_ERROR_MOUNT; - } - - /* The mount operation will resolve any symlinks in the destination - path, so to find it in the mount table we need to do that too. */ - resolved_dest = realpath (dest, NULL); - if (resolved_dest == NULL) - return BIND_MOUNT_ERROR_REALPATH_DEST; - - dest_fd = TEMP_FAILURE_RETRY (open (resolved_dest, O_PATH | O_CLOEXEC)); - if (dest_fd < 0) - { - if (failing_path != NULL) - *failing_path = steal_pointer (&resolved_dest); - - return BIND_MOUNT_ERROR_REOPEN_DEST; - } + dest_proc = fd_to_proc_path (dest_fd); /* If we are in a case-insensitive filesystem, mountinfo might contain a * different case combination of the path we requested to mount. @@ -424,28 +429,39 @@ * instead of the provided @root_mount, so that we can compare the mountinfo * entries with the same case combination that the kernel is expected to * use. */ - dest_proc = xasprintf ("/proc/self/fd/%d", dest_fd); - oldroot_dest_proc = get_oldroot_path (dest_proc); - kernel_case_combination = readlink_malloc (oldroot_dest_proc); - if (kernel_case_combination == NULL) + resolved_dest = readlink_malloc (dest_proc); + if (resolved_dest == NULL) { if (failing_path != NULL) - *failing_path = steal_pointer (&resolved_dest); + *failing_path = steal_pointer (&dest_proc); return BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD; } - mount_tab = parse_mountinfo (proc_fd, kernel_case_combination); + if (src_fd != -1) + { + cleanup_free char *src_proc = fd_to_proc_path (src_fd); + + if (mount (src_proc, dest_proc, NULL, MS_SILENT | MS_BIND | (recursive ? MS_REC : 0), NULL) != 0) + { + if (failing_path != NULL) + *failing_path = steal_pointer (&resolved_dest); + + return BIND_MOUNT_ERROR_MOUNT; + } + } + + mount_tab = parse_mountinfo (resolved_dest); if (mount_tab[0].mountpoint == NULL) { if (failing_path != NULL) - *failing_path = steal_pointer (&kernel_case_combination); + *failing_path = steal_pointer (&resolved_dest); errno = EINVAL; return BIND_MOUNT_ERROR_FIND_DEST_MOUNT; } - assert (path_equal (mount_tab[0].mountpoint, kernel_case_combination)); + assert (path_equal (mount_tab[0].mountpoint, resolved_dest)); current_flags = mount_tab[0].options; new_flags = current_flags | (devices ? 0 : MS_NODEV) | MS_NOSUID | (readonly ? MS_RDONLY : 0); if (new_flags != current_flags && @@ -476,6 +492,15 @@ be safe to ignore because its not something the user can access. */ if (errno != EACCES) { + /* And if we don't need a security boundary, we can also + * ignore other remount errors for submounts. */ + if (options & BIND_FAIL_OPEN) + { + warn ("Can't remount %s submount (%s), ignoring error", + mount_tab[i].mountpoint, strerror (errno)); + continue; + } + if (failing_path != NULL) *failing_path = xstrdup (mount_tab[i].mountpoint); @@ -507,14 +532,6 @@ string = xstrdup ("Unable to mount source on destination"); break; - case BIND_MOUNT_ERROR_REALPATH_DEST: - string = xstrdup ("realpath(destination)"); - break; - - case BIND_MOUNT_ERROR_REOPEN_DEST: - string = xasprintf ("open(\"%s\", O_PATH)", failing_path); - break; - case BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD: string = xasprintf ("readlink(/proc/self/fd/N) for \"%s\"", failing_path); break; @@ -534,6 +551,11 @@ failing_path); break; + case BIND_MOUNT_ERROR_OPEN_FD: + string = xasprintf ("Unable to open destination O_PATH fd \"%s\"", + failing_path); + break; + case BIND_MOUNT_SUCCESS: string = xstrdup ("Success"); break; @@ -583,10 +605,9 @@ fprintf (stderr, ": %s", mount_strerror (saved_errno)); break; - case BIND_MOUNT_ERROR_REALPATH_DEST: - case BIND_MOUNT_ERROR_REOPEN_DEST: case BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD: case BIND_MOUNT_ERROR_FIND_DEST_MOUNT: + case BIND_MOUNT_ERROR_OPEN_FD: case BIND_MOUNT_SUCCESS: default: fprintf (stderr, ": %s", strerror (saved_errno)); diff -Nru bubblewrap-0.11.0/bind-mount.h bubblewrap-0.12.0/bind-mount.h --- bubblewrap-0.11.0/bind-mount.h 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/bind-mount.h 2026-08-26 10:08:29.000000000 +0000 @@ -25,26 +25,30 @@ BIND_READONLY = (1 << 0), BIND_DEVICES = (1 << 2), BIND_RECURSIVE = (1 << 3), + BIND_FAIL_OPEN = (1 << 4), } bind_option_t; typedef enum { BIND_MOUNT_SUCCESS = 0, BIND_MOUNT_ERROR_MOUNT, - BIND_MOUNT_ERROR_REALPATH_DEST, - BIND_MOUNT_ERROR_REOPEN_DEST, BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD, BIND_MOUNT_ERROR_FIND_DEST_MOUNT, BIND_MOUNT_ERROR_REMOUNT_DEST, BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT, + BIND_MOUNT_ERROR_OPEN_FD, } bind_mount_result; -bind_mount_result bind_mount (int proc_fd, - const char *src, +bind_mount_result bind_mount (const char *src, const char *dest, bind_option_t options, char **failing_path); +bind_mount_result bind_mount_fd (int src_fd, + int dest_fd, + bind_option_t options, + char **failing_path); + void die_with_bind_result (bind_mount_result res, int saved_errno, const char *failing_path, diff -Nru bubblewrap-0.11.0/bubblewrap.c bubblewrap-0.12.0/bubblewrap.c --- bubblewrap-0.11.0/bubblewrap.c 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/bubblewrap.c 2026-08-26 10:08:29.000000000 +0000 @@ -55,10 +55,8 @@ static gid_t real_gid; static uid_t overflow_uid; static gid_t overflow_gid; -static bool is_privileged; /* See acquire_privs() */ static const char *argv0; static const char *host_tty_dev; -static int proc_fd = -1; static const char *opt_exec_label = NULL; static const char *opt_file_label = NULL; static bool opt_as_pid_1; @@ -95,6 +93,7 @@ static int next_perms = -1; static size_t next_size_arg = 0; static int next_overlay_src_count = 0; +static bool opt_not_a_security_boundary = false; #define CAP_TO_MASK_0(x) (1L << ((x) & 31)) #define CAP_TO_MASK_1(x) CAP_TO_MASK_0(x - 32) @@ -149,6 +148,13 @@ typedef struct _SetupOp SetupOp; +typedef enum { + NODE_TYPE_NONE, + NODE_TYPE_DIR, + NODE_TYPE_FILE, + NODE_TYPE_EXISTING, +} NodeType; + struct _SetupOp { SetupOpType type; @@ -170,28 +176,6 @@ LockFile *next; }; -enum { - PRIV_SEP_OP_DONE, - PRIV_SEP_OP_BIND_MOUNT, - PRIV_SEP_OP_OVERLAY_MOUNT, - PRIV_SEP_OP_PROC_MOUNT, - PRIV_SEP_OP_TMPFS_MOUNT, - PRIV_SEP_OP_DEVPTS_MOUNT, - PRIV_SEP_OP_MQUEUE_MOUNT, - PRIV_SEP_OP_REMOUNT_RO_NO_RECURSIVE, - PRIV_SEP_OP_SET_HOSTNAME, -}; - -typedef struct -{ - uint32_t op; - uint32_t flags; - uint32_t perms; - size_t size_arg; - uint32_t arg1_offset; - uint32_t arg2_offset; -} PrivSepOp; - /* * DEFINE_LINKED_LIST: * @Type: A struct with a `Type *next` member @@ -312,7 +296,7 @@ " --level-prefix Prepend e.g. <3> to diagnostic messages\n" " --unshare-all Unshare every namespace we support by default\n" " --share-net Retain the network namespace (can only combine with --unshare-all)\n" - " --unshare-user Create new user namespace (may be automatically implied if not setuid)\n" + " --unshare-user Create new user namespace (may be automatically implied if not root)\n" " --unshare-user-try Create new user namespace if possible else continue by skipping it\n" " --unshare-ipc Create new ipc namespace\n" " --unshare-pid Create new pid namespace\n" @@ -373,6 +357,8 @@ " --perms OCTAL Set permissions of next argument (--bind-data, --file, etc.)\n" " --size BYTES Set size of next argument (only for --tmpfs)\n" " --chmod OCTAL PATH Change permissions of PATH (must already exist)\n" + " --not-a-security-boundary Do not fail hard when some sandbox setup steps fail;\n" + " use only when the sandbox is not a security boundary\n" ); exit (ecode); } @@ -518,7 +504,7 @@ if (setup_finished_fd != -1) dont_close[j++] = setup_finished_fd; assert (j < sizeof(dont_close)/sizeof(*dont_close)); - fdwalk (proc_fd, close_extra_fds, dont_close); + fdwalk (close_extra_fds, dont_close); sigemptyset (&mask); sigaddset (&mask, SIGCHLD); @@ -602,7 +588,7 @@ for (lock = lock_files; lock != NULL; lock = lock->next) { - int fd = TEMP_FAILURE_RETRY (open (lock->path, O_RDONLY | O_CLOEXEC)); + int fd = TEMP_FAILURE_RETRY (open (lock->path, O_RDONLY | O_NOCTTY | O_CLOEXEC)); if (fd == -1) die_with_error ("Unable to open lock file %s", lock->path); @@ -683,23 +669,6 @@ #define REQUIRED_CAPS_1 0 static void -set_required_caps (void) -{ - struct __user_cap_header_struct hdr = { _LINUX_CAPABILITY_VERSION_3, 0 }; - struct __user_cap_data_struct data[2] = { { 0 } }; - - /* Drop all non-require capabilities */ - data[0].effective = REQUIRED_CAPS_0; - data[0].permitted = REQUIRED_CAPS_0; - data[0].inheritable = 0; - data[1].effective = REQUIRED_CAPS_1; - data[1].permitted = REQUIRED_CAPS_1; - data[1].inheritable = 0; - if (capset (&hdr, data) < 0) - die_with_error ("capset failed"); -} - -static void drop_all_caps (bool keep_requested_caps) { struct __user_cap_header_struct hdr = { _LINUX_CAPABILITY_VERSION_3, 0 }; @@ -713,10 +682,7 @@ * */ if (!opt_cap_add_or_drop_used && real_uid == 0) - { - assert (!is_privileged); - return; - } + return; data[0].effective = requested_caps[0]; data[0].permitted = requested_caps[0]; data[0].inheritable = requested_caps[0]; @@ -732,7 +698,7 @@ * the init process unconditionally. Since due to the systemd seccomp * filter that will fail, let's just ignore it. */ - if (errno == EPERM && real_uid == 0 && !is_privileged) + if (errno == EPERM && real_uid == 0) return; else die_with_error ("capset failed"); @@ -818,63 +784,21 @@ static void set_ambient_capabilities (void) { - if (is_privileged) - return; prctl_caps (requested_caps, false, true); } -/* This acquires the privileges that the bwrap will need it to work. - * If bwrap is not setuid, then this does nothing, and it relies on - * unprivileged user namespaces to be used. This case is - * "is_privileged = false". - * - * If bwrap is setuid, then we do things in phases. - * The first part is run as euid 0, but with fsuid as the real user. - * The second part, inside the child, is run as the real user but with - * capabilities. - * And finally we drop all capabilities. - * The reason for the above dance is to avoid having the setup phase - * being able to read files the user can't, while at the same time - * working around various kernel issues. See below for details. - */ static void acquire_privs (void) { - uid_t euid, new_fsuid; + uid_t euid; euid = geteuid (); /* Are we setuid ? */ if (real_uid != euid) { - if (euid != 0) - die ("Unexpected setuid user %d, should be 0", euid); - - is_privileged = true; - /* We want to keep running as euid=0 until at the clone() - * operation because doing so will make the user namespace be - * owned by root, which makes it not ptrace:able by the user as - * it otherwise would be. After that we will run fully as the - * user, which is necessary e.g. to be able to read from a fuse - * mount from the user. - * - * However, we don't want to accidentally mis-use euid=0 for - * escalated filesystem access before the clone(), so we set - * fsuid to the uid. - */ - if (setfsuid (real_uid) < 0) - die_with_error ("Unable to set fsuid"); - - /* setfsuid can't properly report errors, check that it worked (as per manpage) */ - new_fsuid = setfsuid (-1); - if (new_fsuid != real_uid) - die ("Unable to set fsuid (was %d)", (int)new_fsuid); - - /* We never need capabilities after execve(), so lets drop everything from the bounding set */ - drop_cap_bounding_set (true); - - /* Keep only the required capabilities for setup */ - set_required_caps (); + /* Historically we supported this, but now we only do user namespaces */ + die ("setuid use of bubblewrap is not supported"); } else if (real_uid != 0 && has_caps ()) { @@ -919,32 +843,12 @@ if (opt_sandbox_gid != real_gid && setgid (opt_sandbox_gid) < 0) die_with_error ("unable to switch to gid %d", opt_sandbox_gid); } - - if (!is_privileged) - return; - - /* Tell kernel not clear capabilities when later dropping root uid */ - if (prctl (PR_SET_KEEPCAPS, 1, 0, 0, 0) < 0) - die_with_error ("prctl(PR_SET_KEEPCAPS) failed"); - - if (setuid (opt_sandbox_uid) < 0) - die_with_error ("unable to drop root uid"); - - /* Regain effective required capabilities from permitted */ - set_required_caps (); } /* Call setuid() and use capset() to adjust capabilities */ static void -drop_privs (bool keep_requested_caps, - bool already_changed_uid) +drop_privs (bool keep_requested_caps) { - assert (!keep_requested_caps || !is_privileged); - /* Drop root uid */ - if (is_privileged && !already_changed_uid && - setuid (opt_sandbox_uid) < 0) - die_with_error ("unable to drop root uid"); - drop_all_caps (keep_requested_caps); /* We don't have any privs now, so mark us dumpable which makes /proc/self be owned by the user instead of root */ @@ -952,6 +856,16 @@ die_with_error ("can't set dumpable"); } +static int +openat_in_root (const char *root, const char *path, int flags) +{ + /* We have reopen the root dir, because we typically mount on top of + * /newroot (e.g. with --bind / /), which an old O_PATH fd will not + * pick up */ + cleanup_fd int root_fd = TEMP_FAILURE_RETRY (open (root, O_PATH | O_NOCTTY | O_CLOEXEC)); + return safe_openat (root_fd, root, path, flags, 0); +} + static void write_uid_gid_map (uid_t sandbox_uid, uid_t parent_uid, @@ -965,7 +879,6 @@ cleanup_free char *gid_map = NULL; cleanup_free char *dir = NULL; cleanup_fd int dir_fd = -1; - uid_t old_fsuid = (uid_t)-1; if (pid == -1) dir = xstrdup ("self"); @@ -988,11 +901,6 @@ else gid_map = xasprintf ("%d %d 1\n", sandbox_gid, parent_gid); - /* We have to be root to be allowed to write to the uid map - * for setuid apps, so temporary set fsuid to 0 */ - if (is_privileged) - old_fsuid = setfsuid (0); - if (write_file_at (dir_fd, "uid_map", uid_map) != 0) die_with_error ("setting up uid map"); @@ -1010,200 +918,254 @@ if (write_file_at (dir_fd, "gid_map", gid_map) != 0) die_with_error ("setting up gid map"); - - if (is_privileged) - { - setfsuid (old_fsuid); - if ((uid_t) setfsuid (-1) != real_uid) - die ("Unable to re-set fsuid"); - } } static void -privileged_op (int privileged_op_socket, - uint32_t op, - uint32_t flags, - uint32_t perms, - size_t size_arg, - const char *arg1, - const char *arg2) +setup_op_bind_mount_fd (bind_option_t options, + int src_fd, + const char *src_display, + int dest_fd, + const char *dest_display) { bind_mount_result bind_result; char *failing_path = NULL; - if (privileged_op_socket != -1) - { - uint32_t buffer[2048]; /* 8k, but is int32 to guarantee nice alignment */ - PrivSepOp *op_buffer = (PrivSepOp *) buffer; - size_t buffer_size = sizeof (PrivSepOp); - uint32_t arg1_offset = 0, arg2_offset = 0; + /* We always bind directories recursively, otherwise this would let us + access files that are otherwise covered on the host */ + bind_result = bind_mount_fd (src_fd, dest_fd, BIND_RECURSIVE | options, &failing_path); - /* We're unprivileged, send this request to the privileged part */ + if (bind_result != BIND_MOUNT_SUCCESS) + die_with_bind_result (bind_result, errno, failing_path, + "Can't bind mount %s on %s", src_display, dest_display); - if (arg1 != NULL) - { - arg1_offset = buffer_size; - buffer_size += strlen (arg1) + 1; - } - if (arg2 != NULL) + assert (failing_path == NULL); /* otherwise we would have died */ +} + +/* Bind mount from path in /oldroot */ +static void +setup_op_bind_mount (bind_option_t options, + const char *src, + int dest_fd, + const char *dest_display) +{ + cleanup_fd int src_fd = openat_in_root ("/oldroot", src, O_PATH); + if (src_fd < 0) + die_with_error("Can't open source %s", src); + + setup_op_bind_mount_fd (options, src_fd, src, dest_fd, dest_display); +} + +static void +setup_op_tmpfs_mount (uint32_t perms, + size_t size, + int dest_fd, + const char *dest_display) +{ + cleanup_free char *mode = NULL; + + /* This check should be unnecessary since we checked this when parsing + * the --size option as well. However, better be safe than sorry. */ + if (size > MAX_TMPFS_BYTES) + die_with_error ("Specified tmpfs size too large (%zu > %zu)", size, MAX_TMPFS_BYTES); + + if (size != 0) + mode = xasprintf ("mode=%#o,size=%zu", perms, size); + else + mode = xasprintf ("mode=%#o", perms); + + cleanup_free char *dest_path = fd_to_proc_path (dest_fd); + cleanup_free char *opt = label_mount (mode, opt_file_label); + if (mount ("tmpfs", dest_path, "tmpfs", MS_NOSUID | MS_NODEV, opt) != 0) + die_with_mount_error ("Can't mount tmpfs on %s", dest_display); +} + +/* This creates a file node as required for the new destination of an + * operation if one is not existing, and returns an O_NOFOLLOW|O_PATH fd + * to it (except if return_writable is passed, then we return a regular + * (writeable) O_NOFOLLOW fd to a new file (or what existing before).) + */ +static int +ensure_node_at (int parent_fd, const char *name, NodeType node_type, int perms, + bool return_writable) +{ + assert (node_type != NODE_TYPE_NONE); + + if (node_type == NODE_TYPE_DIR) + { + if (mkdirat (parent_fd, name, perms >= 0 ? perms : 0755) < 0 && errno != EEXIST) + return -1; + cleanup_fd int fd = TEMP_FAILURE_RETRY ( + openat (parent_fd, name, O_PATH | O_NOFOLLOW | O_CLOEXEC)); + return steal_fd (&fd); + } + else + { + if (node_type == NODE_TYPE_FILE) { - arg2_offset = buffer_size; - buffer_size += strlen (arg2) + 1; + /* Ensure there is a file node */ + cleanup_fd int fd = TEMP_FAILURE_RETRY ( + openat (parent_fd, name, + O_WRONLY | O_CREAT | (return_writable ? O_TRUNC : O_EXCL) | O_NOFOLLOW | O_NOCTTY | O_CLOEXEC, + perms >= 0 ? perms : 0444)); + if (return_writable) + { + if (fd < 0) + return -1; + return steal_fd (&fd); + } + if (fd < 0 && errno != EEXIST) + return -1; } - if (buffer_size >= sizeof (buffer)) - die ("privilege separation operation to large"); + /* Return no-follow O_PATH fd to the new or existing node */ + return TEMP_FAILURE_RETRY (openat (parent_fd, name, + O_PATH | O_NOFOLLOW | O_CLOEXEC)); + } +} - op_buffer->op = op; - op_buffer->flags = flags; - op_buffer->perms = perms; - op_buffer->size_arg = size_arg; - op_buffer->arg1_offset = arg1_offset; - op_buffer->arg2_offset = arg2_offset; - if (arg1 != NULL) - strcpy ((char *) buffer + arg1_offset, arg1); - if (arg2 != NULL) - strcpy ((char *) buffer + arg2_offset, arg2); +/* Sets errno on error (!= 0), ignores trailing slashes */ +static int +ensure_newroot_parents_at (const char *path, + mode_t mode, + char **last_component) +{ + cleanup_free char *fn = NULL; + char *p; - if (TEMP_FAILURE_RETRY (write (privileged_op_socket, buffer, buffer_size)) != (ssize_t)buffer_size) - die ("Can't write to privileged_op_socket"); + if (path == NULL) + { + errno = EINVAL; + return -1; + } - if (TEMP_FAILURE_RETRY (read (privileged_op_socket, buffer, 1)) != 1) - die ("Can't read from privileged_op_socket"); + fn = xstrdup (path); + p = fn; - return; - } + cleanup_fd int parent_fd = openat_in_root ("/newroot", "/", O_DIRECTORY); - /* - * This runs a privileged request for the unprivileged setup - * code. Note that since the setup code is unprivileged it is not as - * trusted, so we need to verify that all requests only affect the - * child namespace as set up by the privileged parts of the setup, - * and that all the code is very careful about handling input. - * - * This means: - * * Bind mounts are safe, since we always use filesystem namespace. They - * must be recursive though, as otherwise you can use a non-recursive bind - * mount to access an otherwise over-mounted mountpoint. - * * Mounting proc, tmpfs, mqueue, devpts in the child namespace is assumed to - * be safe. - * * Remounting RO (even non-recursive) is safe because it decreases privileges. - * * sethostname() is safe only if we set up a UTS namespace - */ - switch (op) + while (*p == '/') + p++; + + *last_component = NULL; + + while (*p) { - case PRIV_SEP_OP_DONE: - break; + char *start_component = p; + while (*p && *p != '/') + p++; + char *after_component = p; - case PRIV_SEP_OP_REMOUNT_RO_NO_RECURSIVE: - bind_result = bind_mount (proc_fd, NULL, arg2, BIND_READONLY, &failing_path); + while (*p == '/') + p++; + + cleanup_free char *component = xstrndup (start_component, after_component - start_component); + bool is_last = (*p == '\0'); + if (is_last) + { + *last_component = steal_pointer (&component); + break; + } + + if (mkdirat (parent_fd, component, mode) < 0 && errno != EEXIST) + return -1; + + char saved = *after_component; + *after_component = 0; + cleanup_fd int new_parent_fd = openat_in_root ("/newroot", fn, O_DIRECTORY); + *after_component = saved; + if (new_parent_fd < 0) + return -1; + close (parent_fd); + parent_fd = steal_fd(&new_parent_fd); + } + + if (*last_component == NULL) + *last_component = xstrdup("."); + + return steal_fd(&parent_fd); +} + +static bool +op_type_is_mount (SetupOpType type) +{ + switch (type) { + case SETUP_BIND_MOUNT: + case SETUP_RO_BIND_MOUNT: + case SETUP_DEV_BIND_MOUNT: + case SETUP_OVERLAY_MOUNT: + case SETUP_TMP_OVERLAY_MOUNT: + case SETUP_RO_OVERLAY_MOUNT: + case SETUP_OVERLAY_SRC: + case SETUP_MOUNT_PROC: + case SETUP_MOUNT_DEV: + case SETUP_MOUNT_TMPFS: + case SETUP_MOUNT_MQUEUE: + case SETUP_REMOUNT_RO_NO_RECURSIVE: + case SETUP_MAKE_BIND_FILE: + case SETUP_MAKE_RO_BIND_FILE: + return true; + case SETUP_MAKE_DIR: + case SETUP_MAKE_FILE: + case SETUP_MAKE_SYMLINK: + case SETUP_SET_HOSTNAME: + case SETUP_CHMOD: + default: + return false; + } +} + +/* After having covered dest_fd with a mount, we need + to re-open it to work on the new mount. */ +static int +reopen_newroot_fd (int dest_fd, const char *dest_path) +{ + close (dest_fd); + dest_fd = openat_in_root ("/newroot", dest_path, O_PATH | O_NOFOLLOW); + if (dest_fd < 0) + die_with_error ("Can't reopen %s", dest_path); + return dest_fd; +} - if (bind_result != BIND_MOUNT_SUCCESS) - die_with_bind_result (bind_result, errno, failing_path, - "Can't remount readonly on %s", arg2); - - assert (failing_path == NULL); /* otherwise we would have died */ - break; - - case PRIV_SEP_OP_BIND_MOUNT: - /* We always bind directories recursively, otherwise this would let us - access files that are otherwise covered on the host */ - bind_result = bind_mount (proc_fd, arg1, arg2, BIND_RECURSIVE | flags, &failing_path); - - if (bind_result != BIND_MOUNT_SUCCESS) - die_with_bind_result (bind_result, errno, failing_path, - "Can't bind mount %s on %s", arg1, arg2); - - assert (failing_path == NULL); /* otherwise we would have died */ - break; - - case PRIV_SEP_OP_PROC_MOUNT: - if (mount ("proc", arg1, "proc", MS_NOSUID | MS_NOEXEC | MS_NODEV, NULL) != 0) - die_with_mount_error ("Can't mount proc on %s", arg1); - break; - - case PRIV_SEP_OP_TMPFS_MOUNT: - { - cleanup_free char *mode = NULL; - - /* This check should be unnecessary since we checked this when parsing - * the --size option as well. However, better be safe than sorry. */ - if (size_arg > MAX_TMPFS_BYTES) - die_with_error ("Specified tmpfs size too large (%zu > %zu)", size_arg, MAX_TMPFS_BYTES); - - if (size_arg != 0) - mode = xasprintf ("mode=%#o,size=%zu", perms, size_arg); - else - mode = xasprintf ("mode=%#o", perms); - - cleanup_free char *opt = label_mount (mode, opt_file_label); - if (mount ("tmpfs", arg1, "tmpfs", MS_NOSUID | MS_NODEV, opt) != 0) - die_with_mount_error ("Can't mount tmpfs on %s", arg1); - break; - } - - case PRIV_SEP_OP_DEVPTS_MOUNT: - if (mount ("devpts", arg1, "devpts", MS_NOSUID | MS_NOEXEC, - "newinstance,ptmxmode=0666,mode=620") != 0) - die_with_mount_error ("Can't mount devpts on %s", arg1); - break; - - case PRIV_SEP_OP_MQUEUE_MOUNT: - if (mount ("mqueue", arg1, "mqueue", 0, NULL) != 0) - die_with_mount_error ("Can't mount mqueue on %s", arg1); - break; - - case PRIV_SEP_OP_OVERLAY_MOUNT: - if (mount ("overlay", arg2, "overlay", MS_MGC_VAL, arg1) != 0) - { - /* The standard message for ELOOP, "Too many levels of symbolic - * links", is not helpful here. */ - if (errno == ELOOP) - die ("Can't make overlay mount on %s with options %s: " - "Overlay directories may not overlap", - arg2, arg1); - die_with_mount_error ("Can't make overlay mount on %s with options %s", - arg2, arg1); - } - break; - - case PRIV_SEP_OP_SET_HOSTNAME: - /* This is checked at the start, but lets verify it here in case - something manages to send hacked priv-sep operation requests. */ - if (!opt_unshare_uts) - die ("Refusing to set hostname in original namespace"); - if (sethostname (arg1, strlen(arg1)) != 0) - die_with_error ("Can't set hostname to %s", arg1); - break; - - default: - die ("Unexpected privileged op %d", op); - } -} - -/* This is run unprivileged in the child namespace but can request - * some privileged operations (also in the child namespace) via the - * privileged_op_socket. - */ static void -setup_newroot (bool unshare_pid, - int privileged_op_socket) +setup_newroot (bool unshare_pid) { SetupOp *op; int tmp_overlay_idx = 0; for (op = ops; op != NULL; op = op->next) { - cleanup_free char *source = NULL; - cleanup_free char *dest = NULL; + cleanup_fd int source_fd = -1; + cleanup_fd int dest_fd = -1; + cleanup_fd int parent_fd = -1; + cleanup_free char *name = NULL; int source_mode = 0; + int dest_mode = 0; unsigned int i; if (op->source && op->type != SETUP_MAKE_SYMLINK) { - source = get_oldroot_path (op->source); - source_mode = get_file_mode (source); + if (has_path_prefix (op->source, "/proc")) + { + /* openat_in_root ("/oldroot") fails for some /proc magic links, such as + * /proc/self/ns/user which is used by flatpak, so use a regular open + * for these. This should be fine because /proc doesn't have any regular + * absolute symlinks, and the magic links should work fine. + */ + cleanup_free char *proc_oldroot_path = get_oldroot_path (op->source); + source_fd = TEMP_FAILURE_RETRY ( + open (proc_oldroot_path, O_PATH | O_CLOEXEC)); + } + else + source_fd = openat_in_root ("/oldroot", op->source, O_PATH); + if (source_fd < 0) + { + if (op->flags & ALLOW_NOTEXIST && errno == ENOENT) + continue; /* Ignore and move on */ + die_with_error("Can't open source %s", op->source); + } + + source_mode = get_file_mode (source_fd); if (source_mode < 0) { if (op->flags & ALLOW_NOTEXIST && errno == ENOENT) @@ -1216,6 +1178,42 @@ (op->flags & NO_CREATE_DEST) == 0) { unsigned parent_mode = 0755; + NodeType dest_type = NODE_TYPE_NONE; + + switch (op->type) + { + default: + case SETUP_MAKE_SYMLINK: + case SETUP_SET_HOSTNAME: + /* No need to create a base file for these */ + dest_type = NODE_TYPE_NONE; + break; + case SETUP_REMOUNT_RO_NO_RECURSIVE: + case SETUP_CHMOD: + dest_type = NODE_TYPE_EXISTING; + break; + case SETUP_MAKE_FILE: + case SETUP_MAKE_BIND_FILE: + case SETUP_MAKE_RO_BIND_FILE: + dest_type = NODE_TYPE_FILE; + break; + case SETUP_BIND_MOUNT: + case SETUP_RO_BIND_MOUNT: + case SETUP_DEV_BIND_MOUNT: + dest_type = (source_mode == S_IFDIR) ? NODE_TYPE_DIR : NODE_TYPE_FILE; + break; + case SETUP_OVERLAY_MOUNT: + case SETUP_RO_OVERLAY_MOUNT: + case SETUP_TMP_OVERLAY_MOUNT: + case SETUP_OVERLAY_SRC: + case SETUP_MOUNT_PROC: + case SETUP_MOUNT_DEV: + case SETUP_MOUNT_TMPFS: + case SETUP_MOUNT_MQUEUE: + case SETUP_MAKE_DIR: + dest_type = NODE_TYPE_DIR; + break; + } /* If we're creating a file that is inaccessible by the owning group, * try to achieve least-astonishment by creating parents also @@ -1229,9 +1227,38 @@ (op->perms & 0007) == 0) parent_mode &= ~0005U; - dest = get_newroot_path (op->dest); - if (mkdir_with_parents (dest, parent_mode, false) != 0) + parent_fd = ensure_newroot_parents_at (op->dest, parent_mode, &name); + + if (parent_fd < 0) die_with_error ("Can't mkdir parents for %s", op->dest); + + if (dest_type != NODE_TYPE_NONE) + { + bool writable_fd = (op->type == SETUP_MAKE_FILE); + int node_perms = (op->type == SETUP_MAKE_DIR || op->type == SETUP_MAKE_FILE) ? op->perms : -1; + + /* Note: dest_fd is O_NOFOLLOW, and O_PATH unless writable_fd */ + dest_fd = ensure_node_at (parent_fd, name, dest_type, node_perms, writable_fd); + if (dest_fd < 0) + die_with_error ("Can't create file %s", op->dest); + + dest_mode = get_file_mode (dest_fd); + if (dest_mode < 0) + die_with_error("Can't get type of dest %s", op->dest); + + /* For security reasons, we don't mount on symlink targets, because mount will + follow the symlink and create a mountpoint in the target location. This is + generally not expected and might allow you to use symlinks to control + the mount layout in potentially dangerous ways. */ + if (op_type_is_mount (op->type) && dest_mode == S_IFLNK) + die ("Can't mount on symlink destination %s", op->dest); + + if (dest_type == NODE_TYPE_DIR && dest_mode != S_IFDIR) + die ("Destination is not a directory %s", op->dest); + + if (dest_type == NODE_TYPE_FILE && dest_mode == S_IFDIR) + die ("Destination is not a file %s", op->dest); + } } switch (op->type) @@ -1239,44 +1266,47 @@ case SETUP_RO_BIND_MOUNT: case SETUP_DEV_BIND_MOUNT: case SETUP_BIND_MOUNT: - if (source_mode == S_IFDIR) - { - if (ensure_dir (dest, 0755) != 0) - die_with_error ("Can't mkdir %s", op->dest); - } - else if (ensure_file (dest, 0444) != 0) - die_with_error ("Can't create file at %s", op->dest); + { + bind_option_t bind_flags = 0; - privileged_op (privileged_op_socket, - PRIV_SEP_OP_BIND_MOUNT, - (op->type == SETUP_RO_BIND_MOUNT ? BIND_READONLY : 0) | - (op->type == SETUP_DEV_BIND_MOUNT ? BIND_DEVICES : 0), - 0, 0, source, dest); + if (opt_not_a_security_boundary) + bind_flags |= BIND_FAIL_OPEN; - if (op->fd >= 0) - { - struct stat fd_st, mount_st; + if (op->type == SETUP_RO_BIND_MOUNT) + bind_flags |= BIND_READONLY; - /* When using bind-fd, there is a race condition between resolving the fd as a magic symlink - * and mounting it, where someone could replace what is at the symlink target. Ideally - * we would not even resolve the symlink and directly bind-mount from the fd, but unfortunately - * we can't do that, because its not permitted to bind mount a fd from another user namespace. - * So, we resolve, mount and then compare fstat+stat to detect the race. */ - - if (fstat(op->fd, &fd_st) != 0) - die_with_error("Can't stat fd %d", op->fd); - if (lstat(dest, &mount_st) != 0) - die_with_error("Can't stat mount at %s", dest); - - if (fd_st.st_ino != mount_st.st_ino || - fd_st.st_dev != mount_st.st_dev) - die_with_error("Race condition binding dirfd"); + if (op->type == SETUP_DEV_BIND_MOUNT) + bind_flags |= BIND_DEVICES; - close(op->fd); - op->fd = -1; - } + setup_op_bind_mount_fd (bind_flags, source_fd, op->source, dest_fd, op->dest); - break; + /* When using bind-fd, there is a race condition between resolving the fd as a magic symlink + * and mounting it, where someone could replace what is at the symlink target. Ideally + * we would not even resolve the symlink and directly bind-mount from the fd, but unfortunately + * we can't do that, because its not permitted to bind mount a fd from another user namespace. + * So, we verify after the mount that the target is what we expect. */ + if (op->fd >= 0) + { + struct stat fd_st, mount_st; + cleanup_fd int mounted_fd = openat_in_root ("/newroot", op->dest, O_PATH); + if (mounted_fd < 0) + die_with_error ("Can't reopen %s", op->dest); + + if (fstat (op->fd, &fd_st) != 0) + die_with_error ("Can't stat fd %d", op->fd); + if (fstat (mounted_fd, &mount_st) != 0) + die_with_error ("Can't stat mount at %s", op->dest); + + if (fd_st.st_ino != mount_st.st_ino || + fd_st.st_dev != mount_st.st_dev) + die_with_error ("Race condition binding dirfd"); + + close (op->fd); + op->fd = -1; + } + + break; + } case SETUP_OVERLAY_MOUNT: case SETUP_RO_OVERLAY_MOUNT: @@ -1284,265 +1314,263 @@ { StringBuilder sb = {0}; bool multi_src = false; - - if (ensure_dir (dest, 0755) != 0) - die_with_error ("Can't mkdir %s", op->dest); + cleanup_fdset FdSet fds = {0}; + cleanup_free char *dest_path = fd_to_proc_path (dest_fd); if (op->source != NULL) { - strappend (&sb, "upperdir=/oldroot"); - strappend_escape_for_mount_options (&sb, op->source); - strappend (&sb, ",workdir=/oldroot"); + cleanup_free char *upper_path = fdset_add_to_proc_path (&fds, steal_fd (&source_fd)); + strappendf (&sb, "upperdir=%s,", upper_path); + op = op->next; - strappend_escape_for_mount_options (&sb, op->source); - strappend (&sb, ","); + int work_fd = openat_in_root ("/oldroot", op->source, O_PATH); + if (work_fd < 0) + die_with_error ("Can't open overlay workdir %s", op->source); + cleanup_free char *work_path = fdset_add_to_proc_path (&fds, work_fd); + strappendf (&sb, "workdir=%s,", work_path); } else if (op->type == SETUP_TMP_OVERLAY_MOUNT) strappendf (&sb, "upperdir=/tmp-overlay-upper-%1$d,workdir=/tmp-overlay-work-%1$d,", tmp_overlay_idx++); - strappend (&sb, "lowerdir=/oldroot"); + strappend (&sb, "lowerdir="); while (op->next != NULL && op->next->type == SETUP_OVERLAY_SRC) { op = op->next; + int lower_fd = openat_in_root ("/oldroot", op->source, O_PATH); + if (lower_fd < 0) + die_with_error ("Can't open overlay source %s", op->source); + cleanup_free char *lower_path = fdset_add_to_proc_path (&fds, lower_fd); if (multi_src) - strappend (&sb, ":/oldroot"); - strappend_escape_for_mount_options (&sb, op->source); + strappend (&sb, ":"); + strappend (&sb, lower_path); multi_src = true; } strappend (&sb, ",userxattr"); - privileged_op (privileged_op_socket, - PRIV_SEP_OP_OVERLAY_MOUNT, 0, 0, 0, sb.str, dest); + if (mount ("overlay", dest_path, "overlay", MS_MGC_VAL | MS_NOSUID | MS_NODEV, sb.str) != 0) + { + /* The standard message for ELOOP, "Too many levels of symbolic + * links", is not helpful here. */ + if (errno == ELOOP) + die ("Can't make overlay mount on %s with options %s: " + "Overlay directories may not overlap", + op->dest, sb.str); + die_with_mount_error ("Can't make overlay mount on %s with options %s", + op->dest, sb.str); + } + free (sb.str); } break; case SETUP_REMOUNT_RO_NO_RECURSIVE: - privileged_op (privileged_op_socket, - PRIV_SEP_OP_REMOUNT_RO_NO_RECURSIVE, 0, 0, 0, NULL, dest); + { + char *failing_path = NULL; + bind_mount_result bind_result; + + bind_result = bind_mount_fd (-1, dest_fd, BIND_READONLY, &failing_path); + + if (bind_result != BIND_MOUNT_SUCCESS) + die_with_bind_result (bind_result, errno, failing_path, + "Can't remount readonly on %s", op->dest); + + assert (failing_path == NULL); /* otherwise we would have died */ + } break; case SETUP_MOUNT_PROC: - if (ensure_dir (dest, 0755) != 0) - die_with_error ("Can't mkdir %s", op->dest); - - if (unshare_pid || opt_pidns_fd != -1) - { - /* Our own procfs */ - privileged_op (privileged_op_socket, - PRIV_SEP_OP_PROC_MOUNT, 0, 0, 0, - dest, NULL); - } - else - { - /* Use system procfs, as we share pid namespace anyway */ - privileged_op (privileged_op_socket, - PRIV_SEP_OP_BIND_MOUNT, 0, 0, 0, - "oldroot/proc", dest); - } + { + cleanup_free char *dest_path = fd_to_proc_path (dest_fd); - /* There are a bunch of weird old subdirs of /proc that could potentially be - problematic (for instance /proc/sysrq-trigger lets you shut down the machine - if you have write access). We should not have access to these as a non-privileged - user, but lets cover them anyway just to make sure */ - static const char * const cover_proc_dirs[] = { "sys", "sysrq-trigger", "irq", "bus" }; - for (i = 0; i < N_ELEMENTS (cover_proc_dirs); i++) - { - cleanup_free char *subdir = strconcat3 (dest, "/", cover_proc_dirs[i]); - if (access (subdir, W_OK) < 0) - { - /* The file is already read-only or doesn't exist. */ - if (errno == EACCES || errno == ENOENT || errno == EROFS) - continue; + if (unshare_pid || opt_pidns_fd != -1) + { + /* Our own procfs */ + if (mount ("proc", dest_path, "proc", MS_NOSUID | MS_NOEXEC | MS_NODEV, NULL) != 0) + die_with_mount_error ("Can't mount proc on %s", op->dest); + } + else + { + /* Use system procfs, as we share pid namespace anyway */ + setup_op_bind_mount (0, "/proc", dest_fd, op->dest); + } - die_with_error ("Can't access %s", subdir); - } + dest_fd = reopen_newroot_fd (dest_fd, op->dest); - privileged_op (privileged_op_socket, - PRIV_SEP_OP_BIND_MOUNT, BIND_READONLY, 0, 0, - subdir, subdir); - } + /* There are a bunch of weird old subdirs of /proc that could potentially be + problematic (for instance /proc/sysrq-trigger lets you shut down the machine + if you have write access). We should not have access to these as a non-privileged + user, but lets cover them anyway just to make sure */ + static const char * const cover_proc_dirs[] = { "sys", "sysrq-trigger", "irq", "bus" }; + for (i = 0; i < N_ELEMENTS (cover_proc_dirs); i++) + { + if (faccessat (dest_fd, cover_proc_dirs[i], W_OK, 0) < 0) + { + /* The file is already read-only or doesn't exist. */ + if (errno == EACCES || errno == ENOENT || errno == EROFS) + continue; + + die_with_error ("Can't access %s/%s", op->dest, cover_proc_dirs[i]); + } + + cleanup_fd int subdir_fd = TEMP_FAILURE_RETRY ( + openat (dest_fd, cover_proc_dirs[i], O_PATH | O_NOFOLLOW | O_CLOEXEC)); + if (subdir_fd < 0) + die_with_error ("Can't open %s/%s", op->dest, cover_proc_dirs[i]); + + cleanup_free char *subdir_path = fd_to_proc_path (subdir_fd); + setup_op_bind_mount_fd (BIND_READONLY, + subdir_fd, cover_proc_dirs[i], + subdir_fd, cover_proc_dirs[i]); + } - break; + break; + } case SETUP_MOUNT_DEV: - if (ensure_dir (dest, 0755) != 0) - die_with_error ("Can't mkdir %s", op->dest); + { + setup_op_tmpfs_mount (0755, 0, dest_fd, op->dest); - privileged_op (privileged_op_socket, - PRIV_SEP_OP_TMPFS_MOUNT, 0, 0755, 0, - dest, NULL); + dest_fd = reopen_newroot_fd (dest_fd, op->dest); - static const char *const devnodes[] = { "null", "zero", "full", "random", "urandom", "tty" }; - for (i = 0; i < N_ELEMENTS (devnodes); i++) - { - cleanup_free char *node_dest = strconcat3 (dest, "/", devnodes[i]); - cleanup_free char *node_src = strconcat ("/oldroot/dev/", devnodes[i]); - if (create_file (node_dest, 0444, NULL) != 0) - die_with_error ("Can't create file %s/%s", op->dest, devnodes[i]); - privileged_op (privileged_op_socket, - PRIV_SEP_OP_BIND_MOUNT, BIND_DEVICES, 0, 0, - node_src, node_dest); - } + static const char *const devnodes[] = { "null", "zero", "full", "random", "urandom", "tty" }; + for (i = 0; i < N_ELEMENTS (devnodes); i++) + { + cleanup_free char *node_src = strconcat ("/dev/", devnodes[i]); + cleanup_fd int node_fd = -1; - static const char *const stdionodes[] = { "stdin", "stdout", "stderr" }; - for (i = 0; i < N_ELEMENTS (stdionodes); i++) - { - cleanup_free char *target = xasprintf ("/proc/self/fd/%d", i); - cleanup_free char *node_dest = strconcat3 (dest, "/", stdionodes[i]); - if (symlink (target, node_dest) < 0) - die_with_error ("Can't create symlink %s/%s", op->dest, stdionodes[i]); - } + node_fd = ensure_node_at (dest_fd, devnodes[i], NODE_TYPE_FILE, 0444, false); + if (node_fd < 0) + die_with_error ("Can't create file %s/%s", op->dest, devnodes[i]); + setup_op_bind_mount (BIND_DEVICES, node_src, node_fd, devnodes[i]); + } - /* /dev/fd and /dev/core - legacy, but both nspawn and docker do these */ - { cleanup_free char *dev_fd = strconcat (dest, "/fd"); - if (symlink ("/proc/self/fd", dev_fd) < 0) - die_with_error ("Can't create symlink %s", dev_fd); - } - { cleanup_free char *dev_core = strconcat (dest, "/core"); - if (symlink ("/proc/kcore", dev_core) < 0) - die_with_error ("Can't create symlink %s", dev_core); - } + static const char *const stdionodes[] = { "stdin", "stdout", "stderr" }; + for (i = 0; i < N_ELEMENTS (stdionodes); i++) + { + cleanup_free char *target = xasprintf ("/proc/self/fd/%d", i); + if (symlinkat (target, dest_fd, stdionodes[i]) < 0) + die_with_error ("Can't create symlink %s/%s", op->dest, stdionodes[i]); + } - { - cleanup_free char *pts = strconcat (dest, "/pts"); - cleanup_free char *ptmx = strconcat (dest, "/ptmx"); - cleanup_free char *shm = strconcat (dest, "/shm"); + /* /dev/fd and /dev/core - legacy, but both nspawn and docker do these */ + if (symlinkat ("/proc/self/fd", dest_fd, "fd") < 0) + die_with_error ("Can't create symlink %s/fd", op->dest); + if (symlinkat ("/proc/kcore", dest_fd, "core") < 0) + die_with_error ("Can't create symlink %s/core", op->dest); - if (mkdir (shm, 0755) == -1) + if (mkdirat (dest_fd, "shm", 0755) == -1) die_with_error ("Can't create %s/shm", op->dest); - if (mkdir (pts, 0755) == -1) - die_with_error ("Can't create %s/devpts", op->dest); - privileged_op (privileged_op_socket, - PRIV_SEP_OP_DEVPTS_MOUNT, 0, 0, 0, pts, NULL); + if (mkdirat (dest_fd, "pts", 0755) == -1) + die_with_error ("Can't create %s/pts", op->dest); - if (symlink ("pts/ptmx", ptmx) != 0) - die_with_error ("Can't make symlink at %s/ptmx", op->dest); - } - - /* If stdout is a tty, that means the sandbox can write to the - outside-sandbox tty. In that case we also create a /dev/console - that points to this tty device. This should not cause any more - access than we already have, and it makes ttyname() work in the - sandbox. */ - if (host_tty_dev != NULL && *host_tty_dev != 0) { - cleanup_free char *src_tty_dev = strconcat ("/oldroot", host_tty_dev); - cleanup_free char *dest_console = strconcat (dest, "/console"); - - if (create_file (dest_console, 0444, NULL) != 0) - die_with_error ("creating %s/console", op->dest); + cleanup_fd int pts_fd = TEMP_FAILURE_RETRY ( + openat (dest_fd, "pts", O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC)); + if (pts_fd < 0) + die_with_error ("Can't open %s/pts", op->dest); + cleanup_free char *pts_path = fd_to_proc_path (pts_fd); - privileged_op (privileged_op_socket, - PRIV_SEP_OP_BIND_MOUNT, BIND_DEVICES, 0, 0, - src_tty_dev, dest_console); + if (mount ("devpts", pts_path, "devpts", MS_NOSUID | MS_NOEXEC, + "newinstance,ptmxmode=0666,mode=620") != 0) + die_with_mount_error ("Can't mount devpts on %s/pts", op->dest); } - break; + if (symlinkat ("pts/ptmx", dest_fd, "ptmx") != 0) + die_with_error ("Can't make symlink at %s/ptmx", op->dest); + + /* If stdout is a tty, that means the sandbox can write to the + outside-sandbox tty. In that case we also create a /dev/console + that points to this tty device. This should not cause any more + access than we already have, and it makes ttyname() work in the + sandbox. */ + if (host_tty_dev != NULL && *host_tty_dev != 0) + { + cleanup_fd int console_fd = ensure_node_at (dest_fd, "console", NODE_TYPE_FILE, 0444, false); + if (console_fd < 0) + die_with_error ("creating %s/console", op->dest); + setup_op_bind_mount (BIND_DEVICES, host_tty_dev, console_fd, "console"); + } + + break; + } case SETUP_MOUNT_TMPFS: - assert (dest != NULL); assert (op->perms >= 0); assert (op->perms <= 07777); - if (ensure_dir (dest, 0755) != 0) - die_with_error ("Can't mkdir %s", op->dest); - - privileged_op (privileged_op_socket, - PRIV_SEP_OP_TMPFS_MOUNT, 0, op->perms, op->size, - dest, NULL); + setup_op_tmpfs_mount (op->perms, op->size, dest_fd, op->dest); break; case SETUP_MOUNT_MQUEUE: - if (ensure_dir (dest, 0755) != 0) - die_with_error ("Can't mkdir %s", op->dest); - - privileged_op (privileged_op_socket, - PRIV_SEP_OP_MQUEUE_MOUNT, 0, 0, 0, - dest, NULL); - break; + { + cleanup_free char *dest_path = fd_to_proc_path (dest_fd); + if (mount ("mqueue", dest_path, "mqueue", 0, NULL) != 0) + die_with_mount_error ("Can't mount mqueue on %s", op->dest); + break; + } case SETUP_MAKE_DIR: - assert (dest != NULL); assert (op->perms >= 0); assert (op->perms <= 07777); - - if (ensure_dir (dest, op->perms) != 0) - die_with_error ("Can't mkdir %s", op->dest); - break; case SETUP_CHMOD: - assert (op->dest != NULL); - /* We used NO_CREATE_DEST so we have to use get_newroot_path() - * explicitly */ - assert (dest == NULL); - dest = get_newroot_path (op->dest); - assert (dest != NULL); - assert (op->perms >= 0); - assert (op->perms <= 07777); - - if (chmod (dest, op->perms) != 0) - die_with_error ("Can't chmod %#o %s", op->perms, op->dest); - - break; - - case SETUP_MAKE_FILE: { - cleanup_fd int dest_fd = -1; + { + dest_fd = openat_in_root ("/newroot", op->dest, O_PATH | O_NOFOLLOW); + if (dest_fd < 0) + die_with_error ("Can't open %s", op->dest); + } - assert (dest != NULL); + cleanup_free char *dest_path = fd_to_proc_path (dest_fd); assert (op->perms >= 0); assert (op->perms <= 07777); - dest_fd = creat (dest, op->perms); - if (dest_fd == -1) - die_with_error ("Can't create file %s", op->dest); - - if (copy_file_data (op->fd, dest_fd) != 0) - die_with_error ("Can't write data to file %s", op->dest); + if (chmod (dest_path, op->perms) != 0) + die_with_error ("Can't chmod %#o %s", op->perms, op->dest); - close (op->fd); - op->fd = -1; + break; } + + case SETUP_MAKE_FILE: + assert (op->perms >= 0); + assert (op->perms <= 07777); + + if (copy_file_data (op->fd, dest_fd) != 0) + die_with_error ("Can't write data to file %s", op->dest); + + close (op->fd); + op->fd = -1; break; case SETUP_MAKE_BIND_FILE: case SETUP_MAKE_RO_BIND_FILE: { - cleanup_fd int dest_fd = -1; + cleanup_fd int tmp_fd = -1; char tempfile[] = "/bindfileXXXXXX"; - assert (dest != NULL); assert (op->perms >= 0); assert (op->perms <= 07777); - dest_fd = mkstemp (tempfile); - if (dest_fd == -1) + tmp_fd = mkstemp (tempfile); + if (tmp_fd == -1) die_with_error ("Can't create tmpfile for %s", op->dest); - if (fchmod (dest_fd, op->perms) != 0) + if (fchmod (tmp_fd, op->perms) != 0) die_with_error ("Can't set mode %#o on file to be used for %s", op->perms, op->dest); - if (copy_file_data (op->fd, dest_fd) != 0) + if (copy_file_data (op->fd, tmp_fd) != 0) die_with_error ("Can't write data to file %s", op->dest); close (op->fd); op->fd = -1; - assert (dest != NULL); - - if (ensure_file (dest, 0444) != 0) - die_with_error ("Can't create file at %s", op->dest); - - privileged_op (privileged_op_socket, - PRIV_SEP_OP_BIND_MOUNT, - (op->type == SETUP_MAKE_RO_BIND_FILE ? BIND_READONLY : 0), - 0, 0, tempfile, dest); + setup_op_bind_mount_fd ((op->type == SETUP_MAKE_RO_BIND_FILE ? BIND_READONLY : 0), + tmp_fd, "tmpfile", dest_fd, op->dest); /* Remove the file so we're sure the app can't get to it in any other way. Its outside the container chroot, so it shouldn't be possible, but lets @@ -1553,23 +1581,25 @@ case SETUP_MAKE_SYMLINK: assert (op->source != NULL); /* guaranteed by the constructor */ - if (symlink (op->source, dest) != 0) + if (symlinkat (op->source, parent_fd, name) != 0) { if (errno == EEXIST) { - cleanup_free char *existing = readlink_malloc (dest); - if (existing == NULL) + char buf[PATH_MAX + 1]; + ssize_t len = readlinkat (parent_fd, name, buf, sizeof (buf) - 1); + if (len < 0) { if (errno == EINVAL) die ("Can't make symlink at %s: destination exists and is not a symlink", op->dest); else die_with_error ("Can't make symlink at %s: destination exists, and cannot read symlink target", op->dest); } + buf[len] = '\0'; - if (strcmp (existing, op->source) == 0) + if (strcmp (buf, op->source) == 0) break; - die ("Can't make symlink at %s: existing destination is %s", op->dest, existing); + die ("Can't make symlink at %s: existing destination is %s", op->dest, buf); } die_with_error ("Can't make symlink at %s", op->dest); } @@ -1577,9 +1607,10 @@ case SETUP_SET_HOSTNAME: assert (op->dest != NULL); /* guaranteed by the constructor */ - privileged_op (privileged_op_socket, - PRIV_SEP_OP_SET_HOSTNAME, 0, 0, 0, - op->dest, NULL); + if (op->dest == NULL) + die ("Hostname argument is NULL"); + if (sethostname (op->dest, strlen(op->dest)) != 0) + die_with_error ("Can't set hostname to %s", op->dest); break; case SETUP_OVERLAY_SRC: /* handled by SETUP_OVERLAY_MOUNT */ @@ -1587,8 +1618,6 @@ die ("Unexpected type %d", op->type); } } - privileged_op (privileged_op_socket, - PRIV_SEP_OP_DONE, 0, 0, 0, NULL, NULL); } /* Do not leak file descriptors already used by setup_newroot () */ @@ -1658,59 +1687,6 @@ } } - -static const char * -resolve_string_offset (void *buffer, - size_t buffer_size, - uint32_t offset) -{ - if (offset == 0) - return NULL; - - if (offset > buffer_size) - die ("Invalid string offset %d (buffer size %zd)", offset, buffer_size); - - return (const char *) buffer + offset; -} - -static uint32_t -read_priv_sec_op (int read_socket, - void *buffer, - size_t buffer_size, - uint32_t *flags, - uint32_t *perms, - size_t *size_arg, - const char **arg1, - const char **arg2) -{ - const PrivSepOp *op = (const PrivSepOp *) buffer; - ssize_t rec_len; - - do - rec_len = read (read_socket, buffer, buffer_size - 1); - while (rec_len == -1 && errno == EINTR); - - if (rec_len < 0) - die_with_error ("Can't read from unprivileged helper"); - - if (rec_len == 0) - exit (1); /* Privileged helper died and printed error, so exit silently */ - - if ((size_t)rec_len < sizeof (PrivSepOp)) - die ("Invalid size %zd from unprivileged helper", rec_len); - - /* Guarantee zero termination of any strings */ - ((char *) buffer)[rec_len] = 0; - - *flags = op->flags; - *perms = op->perms; - *size_arg = op->size_arg; - *arg1 = resolve_string_offset (buffer, rec_len, op->arg1_offset); - *arg2 = resolve_string_offset (buffer, rec_len, op->arg2_offset); - - return op->op; -} - static void __attribute__ ((noreturn)) print_version_and_exit (void) { @@ -2024,9 +2000,6 @@ } else if (strcmp (arg, "--overlay-src") == 0) { - if (is_privileged) - die ("The --overlay-src option is not permitted in setuid mode"); - next_overlay_src_count++; argv += 1; @@ -2036,9 +2009,6 @@ { SetupOp *workdir_op; - if (is_privileged) - die ("The --overlay option is not permitted in setuid mode"); - if (argc < 4) die ("--overlay takes three arguments"); @@ -2057,9 +2027,6 @@ } else if (strcmp (arg, "--tmp-overlay") == 0) { - if (is_privileged) - die ("The --tmp-overlay option is not permitted in setuid mode"); - if (argc < 2) die ("--tmp-overlay takes an argument"); @@ -2076,9 +2043,6 @@ } else if (strcmp (arg, "--ro-overlay") == 0) { - if (is_privileged) - die ("The --ro-overlay option is not permitted in setuid mode"); - if (argc < 2) die ("--ro-overlay takes an argument"); @@ -2681,9 +2645,6 @@ unsigned long long size; char *endptr = NULL; - if (is_privileged) - die ("The --size option is not permitted in setuid mode"); - if (argc < 2) die ("--size takes an argument"); @@ -2736,6 +2697,18 @@ argv += 2; argc -= 2; } + else if (strcmp (arg, "--not-a-security-boundary") == 0) + { + opt_not_a_security_boundary = true; + } + else if (has_prefix (arg, "--debug-opt=")) + { + const char *val = arg + strlen ("--debug-opt="); + if (strcmp (val, "force-openat-fallback") == 0) + opt_force_openat_fallback = true; + else + die ("Unknown --debug-opt value: %s", val); + } else if (strcmp (arg, "--") == 0) { argv += 1; @@ -2889,6 +2862,7 @@ int intermediate_pids_sockets[2] = {-1, -1}; const char *exec_path = NULL; int i; + struct sigaction sa = {}; /* Handle --version early on before we try to acquire/drop * any capabilities so it works in a build environment; @@ -2898,6 +2872,12 @@ if (argc == 2 && (strcmp (argv[1], "--version") == 0)) print_version_and_exit (); + /* Reset SIGCHILD to SIG_DFL allowing signalfd working propertly + * if the parent process had set SIGCHLD to SIG_IGN. */ + sigemptyset (&sa.sa_mask); + sa.sa_handler = SIG_DFL; + sigaction (SIGCHLD, &sa, NULL); + real_uid = getuid (); real_gid = getgid (); @@ -2930,9 +2910,6 @@ args_data = opt_args_data; opt_args_data = NULL; - if ((requested_caps[0] || requested_caps[1]) && is_privileged) - die ("--cap-add in setuid mode can be used only by root"); - if (opt_userns_block_fd != -1 && !opt_unshare_user) die ("--userns-block-fd requires --unshare-user"); @@ -2940,46 +2917,21 @@ die ("--userns-block-fd requires --info-fd"); if (opt_userns_fd != -1 && opt_unshare_user) - die ("--userns not compatible --unshare-user"); + die ("--userns is not compatible with --unshare-user"); if (opt_userns_fd != -1 && opt_unshare_user_try) - die ("--userns not compatible --unshare-user-try"); + die ("--userns is not compatible with --unshare-user-try"); if (opt_disable_userns && !opt_unshare_user) die ("--disable-userns requires --unshare-user"); if (opt_disable_userns && opt_userns_block_fd != -1) - die ("--disable-userns is not compatible with --userns-block-fd"); - - /* Technically using setns() is probably safe even in the privileged - * case, because we got passed in a file descriptor to the - * namespace, and that can only be gotten if you have ptrace - * permissions against the target, and then you could do whatever to - * the namespace anyway. - * - * However, for practical reasons this isn't possible to use, - * because (as described in acquire_privs()) setuid bwrap causes - * root to own the namespaces that it creates, so you will not be - * able to access these namespaces anyway. So, best just not support - * it anyway. - */ - if (opt_userns_fd != -1 && is_privileged) - die ("--userns doesn't work in setuid mode"); - - if (opt_userns2_fd != -1 && is_privileged) - die ("--userns2 doesn't work in setuid mode"); + die ("--disable-userns is not compatible with --userns-block-fd"); - /* We have to do this if we weren't installed setuid (and we're not - * root), so let's just DWIM */ - if (!is_privileged && getuid () != 0 && opt_userns_fd == -1) + /* We have to do this if we we're not root, so let's just DWIM */ + if (getuid () != 0 && opt_userns_fd == -1) opt_unshare_user = true; -#ifdef ENABLE_REQUIRE_USERNS - /* In this build option, we require userns. */ - if (is_privileged && getuid () != 0 && opt_userns_fd == -1) - opt_unshare_user = true; -#endif - if (opt_unshare_user_try && stat ("/proc/self/ns/user", &sbuf) == 0) { @@ -3004,8 +2956,8 @@ } /* Debian lets you disable *unprivileged* user namespaces. However this is not - a problem if we're privileged, and if we're not opt_unshare_user is true - already, and there is not much we can do, its just a non-working setup. */ + a problem if we're privileged, and if we're not then opt_unshare_user is true + already, and there is not much we can do, it's just a non-working setup. */ if (!disabled) opt_unshare_user = true; @@ -3105,7 +3057,7 @@ } /* Switch to the custom user ns before the clone, gets us privs in that ns (assuming its a child of the current and thus allowed) */ - if (opt_userns_fd > 0 && setns (opt_userns_fd, CLONE_NEWUSER) != 0) + if (opt_userns_fd != -1 && setns (opt_userns_fd, CLONE_NEWUSER) != 0) { if (errno == EINVAL) die ("Joining the specified user namespace failed, it might not be a descendant of the current user namespace."); @@ -3126,9 +3078,9 @@ if (opt_unshare_user) { if (errno == EINVAL) - die ("Creating new namespace failed, likely because the kernel does not support user namespaces. bwrap must be installed setuid on such systems."); - else if (errno == EPERM && !is_privileged) - die ("No permissions to creating new namespace, likely because the kernel does not allow non-privileged user namespaces. On e.g. debian this can be enabled with 'sysctl kernel.unprivileged_userns_clone=1'."); + die ("Creating new namespace failed, likely because the kernel does not support user namespaces."); + else if (errno == EPERM) + die ("No permissions to create a new namespace, likely because the kernel does not allow non-privileged user namespaces. On e.g. debian this can be enabled with 'sysctl kernel.unprivileged_userns_clone=1'."); } if (errno == ENOSPC) @@ -3154,28 +3106,13 @@ /* Discover namespace ids before we drop privileges */ namespace_ids_read (pid); - if (is_privileged && opt_unshare_user && opt_userns_block_fd == -1) - { - /* We're running as euid 0, but the uid we want to map is - * not 0. This means we're not allowed to write this from - * the child user namespace, so we do it from the parent. - * - * Also, we map uid/gid 0 in the namespace (to overflowuid) - * if opt_needs_devpts is true, because otherwise the mount - * of devpts fails due to root not being mapped. - */ - write_uid_gid_map (ns_uid, real_uid, - ns_gid, real_gid, - pid, true, opt_needs_devpts); - } - /* Initial launched process, wait for pid 1 or exec:ed command to exit */ - if (opt_userns2_fd > 0 && setns (opt_userns2_fd, CLONE_NEWUSER) != 0) + if (opt_userns2_fd != -1 && setns (opt_userns2_fd, CLONE_NEWUSER) != 0) die_with_error ("Setting userns2 failed"); /* We don't need any privileges in the launcher, drop them immediately. */ - drop_privs (false, false); + drop_privs (false); /* Optionally bind our lifecycle to that of the parent */ handle_die_with_parent (); @@ -3212,7 +3149,7 @@ return monitor_child (event_fd, pid, setup_finished_pipe[0]); } - if (opt_pidns_fd > 0) + if (opt_pidns_fd != -1) { if (setns (opt_pidns_fd, CLONE_NEWPID) != 0) die_with_error ("Setting pidns failed"); @@ -3272,7 +3209,7 @@ ns_uid = opt_sandbox_uid; ns_gid = opt_sandbox_gid; - if (!is_privileged && opt_unshare_user && opt_userns_block_fd == -1) + if (opt_unshare_user && opt_userns_block_fd == -1) { /* In the unprivileged case we have to write the uid/gid maps in * the child, because we have no caps in the parent */ @@ -3348,56 +3285,13 @@ if (chdir ("/") != 0) die_with_error ("chdir / (base path)"); - if (is_privileged) - { - pid_t child; - int privsep_sockets[2]; - - if (socketpair (AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, privsep_sockets) != 0) - die_with_error ("Can't create privsep socket"); - - child = fork (); - if (child == -1) - die_with_error ("Can't fork unprivileged helper"); - - if (child == 0) - { - /* Unprivileged setup process */ - drop_privs (false, true); - close (privsep_sockets[0]); - setup_newroot (opt_unshare_pid, privsep_sockets[1]); - exit (0); - } - else - { - int status; - uint32_t buffer[2048]; /* 8k, but is int32 to guarantee nice alignment */ - uint32_t op, flags, perms; - size_t size_arg; - const char *arg1, *arg2; - cleanup_fd int unpriv_socket = -1; - - unpriv_socket = privsep_sockets[0]; - close (privsep_sockets[1]); + /* Bind-mount proc so /proc/self/fd/N paths work for fd-based mount() calls */ + if (mkdir ("proc", 0755)) + die_with_error ("Creating proc failed"); + if (mount ("oldroot/proc", "proc", NULL, MS_SILENT | MS_BIND | MS_REC, NULL) != 0) + die_with_mount_error ("mounting proc"); - do - { - op = read_priv_sec_op (unpriv_socket, buffer, sizeof (buffer), - &flags, &perms, &size_arg, &arg1, &arg2); - privileged_op (-1, op, flags, perms, size_arg, arg1, arg2); - if (TEMP_FAILURE_RETRY (write (unpriv_socket, buffer, 1)) != 1) - die ("Can't write to op_socket"); - } - while (op != PRIV_SEP_OP_DONE); - - TEMP_FAILURE_RETRY (waitpid (child, &status, 0)); - /* Continue post setup */ - } - } - else - { - setup_newroot (opt_unshare_pid, -1); - } + setup_newroot (opt_unshare_pid); close_ops_fd (); @@ -3439,7 +3333,7 @@ die_with_error ("chdir /"); } - if (opt_userns2_fd > 0 && setns (opt_userns2_fd, CLONE_NEWUSER) != 0) + if (opt_userns2_fd != -1 && setns (opt_userns2_fd, CLONE_NEWUSER) != 0) die_with_error ("Setting userns2 failed"); if (opt_unshare_user && opt_userns_block_fd == -1 && @@ -3492,7 +3386,7 @@ } /* All privileged ops are done now, so drop caps we don't need */ - drop_privs (!is_privileged, true); + drop_privs (true); if (opt_block_fd != -1) { @@ -3571,7 +3465,7 @@ if (opt_sync_fd != -1) dont_close[j++] = opt_sync_fd; dont_close[j++] = -1; - fdwalk (proc_fd, close_extra_fds, dont_close); + fdwalk (close_extra_fds, dont_close); } return do_init (event_fd, pid); @@ -3597,8 +3491,7 @@ /* Optionally bind our lifecycle */ handle_die_with_parent (); - if (!is_privileged) - set_ambient_capabilities (); + set_ambient_capabilities (); /* Should be the last thing before execve() so that filters don't * need to handle anything above */ diff -Nru bubblewrap-0.11.0/bwrap.xml bubblewrap-0.12.0/bwrap.xml --- bubblewrap-0.11.0/bwrap.xml 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/bwrap.xml 2026-08-26 10:08:29.000000000 +0000 @@ -42,8 +42,7 @@ Description - bwrap is a unprivileged low-level sandboxing tool - (optionally setuid on older distributions). You + bwrap is an unprivileged low-level sandboxing tool. You are unlikely to use it directly from the commandline, although that is possible. @@ -56,7 +55,7 @@ By default, bwrap creates a new mount namespace for the sandbox. Optionally it also sets up new user, ipc, pid, network and uts namespaces (but note the - user namespace is required if bwrap is not installed setuid root). + user namespace is required if bwrap is not run as root). The application in the sandbox can be made to run with a different UID and GID. @@ -162,7 +161,7 @@ Use an existing user namespace instead of creating a new one. The namespace must fulfil the permission requirements for setns(), which generally means that it must be a descendant of the currently active user namespace, owned by the same user. - This is incompatible with --unshare-user, and doesn't work in the setuid version of bubblewrap. + @@ -179,8 +178,7 @@ user.max_user_namespaces sysctl to 1, and then entering a nested user namespace which is unable to raise that limit in the outer namespace. - This option requires , and doesn't work - in the setuid version of bubblewrap. + This option requires . @@ -377,8 +375,7 @@ to precede it. - None of these options are available in the setuid version of - bubblewrap. Using --ro-overlay or providing + Using --ro-overlay or providing more than one requires a Linux kernel version of 4.0 or later. @@ -617,6 +614,38 @@ command line. Please be careful to the order they are specified. + + + + + Declare that this invocation of bwrap + is not intended to create a security boundary + between the sandbox and the host system. + When this option is given, + certain non-fatal sandbox setup failures + (such as a subdirectory remount failing + because an automounter did not respond in time) + will produce a warning and will be skipped, + rather than causing bwrap to exit with an error. + In future releases of bubblewrap + the effect of this option might be extended + to make other sandbox setup operations non-fatal + + + This option is intended for callers + such as xdg-dbus-proxy or Steam + that use bwrap + to adjust the filesystem layout for a process, + but do not rely on it to create a security boundary. + + + Other operations that are fundamental to establishing the sandbox + (creating namespaces, pivot_root, + dropping capabilities) + will still cause a hard failure regardless of this option. + + + diff -Nru bubblewrap-0.11.0/chroot_realpath.c bubblewrap-0.12.0/chroot_realpath.c --- bubblewrap-0.11.0/chroot_realpath.c 1970-01-01 00:00:00.000000000 +0000 +++ bubblewrap-0.12.0/chroot_realpath.c 2026-08-26 10:08:29.000000000 +0000 @@ -0,0 +1,195 @@ +/* + * chroot_realpath.c -- resolve pathname as if inside chroot + * Based on realpath.c Copyright (C) 1993 Rick Sladkey + * + * This program 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; see the file COPYING.LIB. If not, + * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * 2005/09/12: Dan Howell (modified from realpath.c to emulate chroot) + * 2019/04/19: Giuseppe Scrivano (on ENOENT return the part that could be resolved) + * 2019/09/30: Giuseppe Scrivano (follow symlinks for the last component) + * 2020/02/02: Giuseppe Scrivano (don't lose terminal '/' if an absolute symlink is found) + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "utils.h" + +#include +#include +#include +#include +#include +#include /* for PATH_MAX */ +#include /* for MAXPATHLEN */ +#include +#ifndef __set_errno +#define __set_errno(val) ((errno) = (val)) +#endif + +#include /* for S_IFLNK */ + +#ifndef PATH_MAX +#define PATH_MAX _POSIX_PATH_MAX +#endif + +#define MAX_READLINKS 32 + +char *chroot_realpath(const char *chroot, const char *path, char resolved_path[]) +{ + char copy_path[PATH_MAX]; + char link_path[PATH_MAX]; + char got_path[PATH_MAX]; + char *got_path_root = got_path; + char *new_path = got_path; + char *max_path; + int readlinks = 0; + int n; + int chroot_len; + int last_component; + + /* Trivial case. */ + if (chroot == NULL || *chroot == '\0' || + (*chroot == '/' && chroot[1] == '\0')) { + if (strlen(path) >= PATH_MAX) { + __set_errno(ENAMETOOLONG); + return NULL; + } + strcpy(resolved_path, path); + return resolved_path; + } + + chroot_len = strlen(chroot); + + if (chroot_len + strlen(path) >= PATH_MAX - 3) { + __set_errno(ENAMETOOLONG); + return NULL; + } + + /* Make a copy of the source path since we may need to modify it. */ + strcpy(copy_path, path); + path = copy_path; + max_path = copy_path + PATH_MAX - chroot_len - 3; + + /* Start with the chroot path. */ + strcpy(new_path, chroot); + new_path += chroot_len; + while (*new_path == '/' && new_path > got_path) + new_path--; + got_path_root = new_path; + *new_path++ = '/'; + + /* Expand each slash-separated pathname component. */ + while (*path != '\0') { + /* Ignore stray "/". */ + if (*path == '/') { + path++; + continue; + } + if (*path == '.') { + /* Ignore ".". */ + if (path[1] == '\0' || path[1] == '/') { + path++; + continue; + } + if (path[1] == '.') { + if (path[2] == '\0' || path[2] == '/') { + path += 2; + /* Ignore ".." at root. */ + if (new_path == got_path_root + 1) + continue; + /* Handle ".." by backing up. */ + while ((--new_path)[-1] != '/'); + continue; + } + } + } + /* Safely copy the next pathname component. */ + while (*path != '\0' && *path != '/') { + if (path > max_path || new_path >= got_path + PATH_MAX - 1) { + __set_errno(ENAMETOOLONG); + return NULL; + } + *new_path++ = *path++; + } + + last_component = (*path == '\0'); + +#ifdef S_IFLNK + /* Protect against infinite loops. */ + if (readlinks++ > MAX_READLINKS) { + __set_errno(ELOOP); + return NULL; + } + /* See if latest pathname component is a symlink. */ + *new_path = '\0'; + n = readlink(got_path, link_path, PATH_MAX - 1); + if (n < 0) { + /* If a component doesn't exist, then return what we could translate. */ + if (errno == ENOENT) { + int ret = snprintf (resolved_path, PATH_MAX, "%s%s%s", got_path, path[0] == '/' || path[0] == '\0' ? "" : "/", path); + if (ret >= PATH_MAX) { + __set_errno(ENAMETOOLONG); + return NULL; + } + return resolved_path; + } + /* EINVAL means the file exists but isn't a symlink. */ + if (errno != EINVAL) + return NULL; + } else { + /* Note: readlink doesn't add the null byte. */ + link_path[n] = '\0'; + if (*link_path == '/') { + /* Start over for an absolute symlink. */ + new_path = got_path_root; + *new_path++ = '/'; + } + else { + /* Otherwise back up over this component, keeping the + separator, so that the expanded symlink is not + concatenated to the parent directory name. */ + while (*(--new_path) != '/'); + new_path++; + } + /* Safe sex check. */ + if (strlen(path) + n >= PATH_MAX - 2) { + __set_errno(ENAMETOOLONG); + return NULL; + } + /* Insert symlink contents into path. */ + strcat(link_path, path); + strcpy(copy_path, link_path); + path = copy_path; + } +#endif /* S_IFLNK */ + if (!last_component) { + if (new_path >= got_path + PATH_MAX - 1) { + __set_errno(ENAMETOOLONG); + return NULL; + } + *new_path++ = '/'; + } + } + /* Delete trailing slash but don't whomp a lone slash. */ + if (new_path != got_path + 1 && new_path[-1] == '/') + new_path--; + /* Make sure it's null terminated. */ + *new_path = '\0'; + strcpy(resolved_path, got_path); + return resolved_path; +} diff -Nru bubblewrap-0.11.0/ci/enable-userns.sh bubblewrap-0.12.0/ci/enable-userns.sh --- bubblewrap-0.11.0/ci/enable-userns.sh 1970-01-01 00:00:00.000000000 +0000 +++ bubblewrap-0.12.0/ci/enable-userns.sh 2026-08-26 10:08:29.000000000 +0000 @@ -0,0 +1,6 @@ +#!/bin/bash + +set -e + +echo "kernel.apparmor_restrict_unprivileged_userns = 0" > /etc/sysctl.d/99-userns.conf +sysctl --system diff -Nru bubblewrap-0.11.0/debian/NEWS bubblewrap-0.12.0/debian/NEWS --- bubblewrap-0.11.0/debian/NEWS 1970-01-01 00:00:00.000000000 +0000 +++ bubblewrap-0.12.0/debian/NEWS 2026-08-26 11:04:21.000000000 +0000 @@ -0,0 +1,15 @@ +bubblewrap (0.11.2-2) unstable; urgency=medium + + Historically, bubblewrap was sometimes installed setuid root. + This is unnecessary on Debian kernels since Debian 11, and no longer + supported by upstream. + + As a safety mechanism against possible root privilege escalation + vulnerabilities, this version of bubblewrap will no longer run if it + is detected to be setuid root. + + If bubblewrap has been made setuid root via dpkg-statoverride, please remove + that setting, and instead leave it unprivileged. + See file:///usr/share/doc/bubblewrap/README.Debian.gz for more details. + + -- Simon McVittie Tue, 12 May 2026 18:57:32 +0100 diff -Nru bubblewrap-0.11.0/debian/README.Debian bubblewrap-0.12.0/debian/README.Debian --- bubblewrap-0.11.0/debian/README.Debian 2026-04-26 13:05:43.000000000 +0000 +++ bubblewrap-0.12.0/debian/README.Debian 2026-08-26 11:04:21.000000000 +0000 @@ -8,7 +8,7 @@ - Flatpak uses bubblewrap to run partially-trusted, user-installable apps in a sandboxed environment. -- libgnome-desktop uses bubblewrap to run thumbnailers in a sandboxed +- The glycin library uses bubblewrap to run image loaders in a sandboxed environment, so that if there are security flaws in an image decoder used by a thumbnailer, the process of generating thumbnails for a maliciously crafted image cannot be used to attack the rest of the @@ -24,47 +24,14 @@ - Steam uses bubblewrap to run some games with a predictable library stack that does not match the rest of the system. -The necessary capabilities to do this can be obtained in one of two -ways: - -- On kernels where unprivileged users can create new user namespaces, - bubblewrap's bwrap executable can be an ordinary unprivileged program. - -- On kernels where this is not possible, bubblewrap will not work unless - the /usr/bin/bwrap executable is setuid root. Some bubblewrap and Flatpak - features will not work in this configuration for security reasons. - -Newer Debian kernels (Linux 5.10 and newer; Debian 11 and newer) ----------------------------------------------------------------- - -Debian kernels since 5.10 allow unprivileged users to create new user -namespaces. The bwrap executable can be made non-setuid on these kernels. - -By default, the bubblewrap package in Debian no longer installs a setuid -root /usr/bin/bwrap executable. - -Ubuntu kernels (Ubuntu 18.04 and newer) ---------------------------------------- - -Ubuntu kernels also allow unprivileged users to create new user -namespaces. The bwrap executable can be made non-setuid on these kernels, -and the Ubuntu bubblewrap package does not install a setuid executable. - -Older Debian kernels (Linux 5.9 and older; Debian 10 and older) ---------------------------------------------------------------- - -Debian kernels older than 5.10 have support for user namespaces, but -that feature is disabled by default to reduce the kernel's attack -surface. - -The bubblewrap package contains configuration in -/usr/lib/sysctl.d/50-bubblewrap.conf to enable user namespaces -(see "Enabling kernel.unprivileged_userns_clone", below). - -If this is not desired, system administrators can copy that file -to /etc/sysctl.d/50-bubblewrap.conf and modify it to disable unprivileged -creation of user namespaces, then make bubblewrap setuid root so that it -still works as intended (see "Making bubblewrap setuid root", below). +To achieve this, the kernel must support user namespaces, and allow +ordinary unprivileged processes to create a new user namespace. +This is the case by default on all modern versions of Debian and Ubuntu. + +Historically, the bubblewrap executable was sometimes setuid root, +which allowed it to create user namespaces even if unprivileged processes +are not able to do so. This was deprecated because it is a security risk, +and is no longer possible. Custom and third-party kernels ------------------------------ @@ -82,35 +49,30 @@ This Debian-specific sysctl parameter controls whether unprivileged users are allowed to create new user namespaces. +The default is 1 for all modern Debian and Ubuntu kernels. + If it is set to 0, some attacks against the kernel are made more difficult, -which can increase security. However, some user-space software will not -be able to create a sandboxed environment or will have to rely on a -setuid version of bubblewrap to create a sandboxed environment, which -reduces security. The value of this sysctl parameter is a trade-off -between different security risks. - -If this parameter is set to 0, bubblewrap and Flatpak will not work unless -bwrap is made setuid root (see "Making bubblewrap setuid root" below). - -The default is 1 for Debian kernels that are version 5.10 or newer, -1 for Ubuntu kernels, or 0 for older Debian kernels. The bubblewrap -package contains configuration in /usr/lib/sysctl.d/50-bubblewrap.conf -to set this parameter to 1 during system startup. - -If this is not desired, system administrators can copy -/usr/lib/sysctl.d/50-bubblewrap.conf to /etc/sysctl.d/50-bubblewrap.conf -and modify it to disable unprivileged creation of user namespaces, then -make bubblewrap setuid root so that it still works as intended (see -"Making bubblewrap setuid root", below). +which can increase security. However, user-space software like bubblewrap +and web browsers will not be able to create a sandboxed environment. +The value of this sysctl parameter is a trade-off between different +security risks. + +If this parameter is set to 0, bubblewrap and Flatpak will not work. You can view the current setting with: cat /proc/sys/kernel/unprivileged_userns_clone -and temporarily set it to 1 (until the next reboot) with: +and temporarily set it to 0 or 1 (until the next reboot) with a command +like: sudo sysctl -w kernel.unprivileged_userns_clone=1 +It can be set permanently by using a file /etc/sysctl.d/*.conf: +this requires either systemd as pid 1, or the procps package. +For details please see sysctl.d(5) on systemd systems, or sysctl.conf(5) +on systems using a non-default init. + Configuring the maximum number of namespaces per user ----------------------------------------------------- @@ -118,9 +80,8 @@ depends on the amount of RAM available. Setting this limit to 0 is the recommended way to disable user namespace -creation if this is required as a security hardening measure. bubblewrap -will not work with this limit set to 0, unless it is setuid root (see -"Configuring whether bubblewrap is setuid root" below). +creation if this is required as a security hardening measure. However, +bubblewrap will not work with this limit set to 0. The limit is given by the user.max_user_namespaces sysctl parameter. @@ -141,32 +102,22 @@ Configuring whether bubblewrap is setuid root --------------------------------------------- -To use bubblewrap with kernel.unprivileged_userns_clone set to 0 -or user.max_user_namespaces set to 0, it is necessary to make the bwrap -executable setuid root. This gives it the necessary capabilities to set -up containers even when run by an otherwise unprivileged user, and is the -configuration normally used in Debian 10. - -This can be a security risk: if there are bugs in bubblewrap, it might be -possible for an unprivileged user to get root privileges by running a -setuid version of the bwrap executable. CVE-2020-5291 and CVE-2016-8659 -are examples of bugs that had this effect in the past. However, it allows -the kernel to be configured to disallow creation of user namespaces by -unprivileged users, which prevents attacks like CVE-2016-3135 from being -carried out against the kernel. This is a trade-off between different -security risks. +To use bubblewrap with kernel.unprivileged_userns_clone set to 0 or +user.max_user_namespaces set to 0, historically some systems made the bwrap +executable setuid root. This led to a risk of root privilege escalation +security vulnerabilities such as CVE-2026-41163, CVE-2020-5291 +and CVE-2016-8659, and as a result this configuration has been disabled. + +If it is setuid root, bubblewrap will now fail with this error message: -To avoid other attacks, some Flatpak and bubblewrap features are not -available when bwrap is setuid root, and the absence of those features -is known to break some Flatpak apps. For example, the Flatpak app for -the Chromium web browser will not work with a setuid bwrap executable. + bwrap: setuid use of bubblewrap is not supported To check whether the bwrap executable will be made setuid root after the next upgrade, use this command: dpkg-statoverride --list /usr/bin/bwrap -To force the bwrap executable to be setuid root, use these commands: +and to remove any permissions overrides that it might have: sudo dpkg-statoverride --quiet --remove /usr/bin/bwrap - sudo dpkg-statoverride --update --add root root 4755 /usr/bin/bwrap + sudo chmod 0755 /usr/bin/bwrap diff -Nru bubblewrap-0.11.0/debian/changelog bubblewrap-0.12.0/debian/changelog --- bubblewrap-0.11.0/debian/changelog 2026-04-26 13:05:43.000000000 +0000 +++ bubblewrap-0.12.0/debian/changelog 2026-08-26 11:04:21.000000000 +0000 @@ -1,3 +1,70 @@ +bubblewrap (0.12.0-1~deb13u1) trixie-security; urgency=high + + * Merge new upstream release from unstable + - Prevent sandbox escape via symlink traversal. + If an app framework such as Flatpak mounts subdirectories into a + directory controlled by the sandboxed app, a malicious or compromised + sandboxed app could create symlinks in that directory to arrange for + files/directories to be created on the host system. + (GHSA-pxhw-h44j-8pfx, no known CVE ID; Closes: #1145655) + - bubblewrap no longer supports running when setuid, matching the + upstream default. This ensures that vulnerabilities similar to + CVE-2026-41163 can't happen in future. + * Debian 13 backport changes: + - d/control, d/gbp.conf: Branch for Debian 13 stable updates + - Revert packaging changes that are not appropriate for a stable release + * Packaging changes since 0.11.0-2+deb13u1: + - d/rules: Stop passing -Dsupport_setuid=false. + The option no longer exists, and the new version of bubblewrap always + behaves as though its value was false. + - d/rules: Don't compile fallback code paths for kernel older than 5.10. + This ensures that we're using the safest available mechanisms, + using the openat2() syscall rather than emulating it in user-space. + As a result, this version will not work on kernels older than the + one found in Debian 11. + - d/rules: Install NEWS.md as the upstream changelog + - d/p/CVE-2026-41163/: + Drop patches, no longer needed/applicable with the new upstream release + - d/p/debian/Change-EPERM-error-message-to-show-Debian-specific-inform.patch: + Adjust patch to apply to the new upstream release + - d/README.Debian: Rewrite to reflect that setuid is no longer supported + - d/copyright: Update license from LGPL-2+ to LGPL-2.1+, matching upstream + + -- Simon McVittie Wed, 26 Aug 2026 12:04:21 +0100 + +bubblewrap (0.12.0-1) unstable; urgency=high + + * New upstream release + - Prevent sandbox escape via symlink traversal. + If an app framework such as Flatpak mounts subdirectories into a + directory controlled by the sandboxed app, a malicious or compromised + sandboxed app could create symlinks in that directory to arrange for + files/directories to be created on the host system. + (GHSA-pxhw-h44j-8pfx, no known CVE ID; Closes: #1145655) + - d/rules: Stop passing -Dsupport_setuid=false. + The option no longer exists, and the new version of bubblewrap always + behaves as though its value was false. + - d/copyright: Update license from LGPL-2+ to LGPL-2.1+, matching upstream + - d/p/debian/Change-EPERM-error-message-to-show-Debian-specific-inform.patch: + Adjust patch to apply to the new upstream release + * d/rules: Don't compile fallback code paths for kernel older than 5.10. + This ensures that we're using the safest available mechanisms, + using the openat2() syscall rather than emulating it in user-space. + As a result, this version will not work on kernels older than the + one found in Debian 11. + + -- Simon McVittie Wed, 26 Aug 2026 11:32:16 +0100 + +bubblewrap (0.11.2-2) unstable; urgency=medium + + * d/rules: Stop allowing bubblewrap to run when setuid, matching + the upstream default. This ensures that vulnerabilities similar to + CVE-2026-41163 can't happen in future. + * d/control, d/NEWS, d/README.Debian: Update documentation accordingly + * Standards-Version: 4.7.4 (no changes required) + + -- Simon McVittie Tue, 12 May 2026 18:57:32 +0100 + bubblewrap (0.11.0-2+deb13u1) trixie; urgency=medium * d/control, d/gbp.conf: Branch for Debian 13 stable updates @@ -14,6 +81,51 @@ -- Simon McVittie Sun, 26 Apr 2026 14:05:43 +0100 +bubblewrap (0.11.2-1) unstable; urgency=medium + + * New upstream release + - Fixes a root privilege escalation vulnerability if bwrap has been + made setuid root locally (CVE-2026-41163, Closes: #1134704). + Most Debian systems have a non-setuid bubblewrap and therefore + are unaffected by this. + * d/rules: Temporarily allow bubblewrap to be setuid root. + This configuration is a security risk and rarely necessary, so the + option is deprecated, and a future upstream version will + unconditionally refuse to run if it detects that it has been run + setuid root. + * d/README.Debian: Update to reflect deprecation of setuid-root bwrap + * d/NEWS: Mention deprecation of setuid-root mode + + -- Simon McVittie Thu, 23 Apr 2026 12:25:34 +0100 + +bubblewrap (0.11.1-1) unstable; urgency=medium + + * New upstream release + * Stop overriding kernel.unprivileged_userns_clone sysctl. + The setting we use has been the default for several years. + * d/control: Remove Recommends on procps. + This was only needed as a way to force the required value of + kernel.unprivileged_userns_clone during the transition from Debian 10 + to 11, and even then, only on non-systemd systems. + * d/p/debian/Change-EPERM-error-message-to-show-Debian-specific-inform.patch: + Refresh patch to apply after a grammatical fix upstream + * d/control: Replace transitional libselinux1-dev with libselinux-dev + * d/control: Bump Standards-Version to 4.7.3. + Remove Priority: optional, no longer required with current dpkg-dev. + * d/copyright: Stop quoting the FSF's former postal address + * d/rules: Stop overriding build system. + This was only necessary while bubblewrap supported both Meson and + Autotools builds, which it hasn't since 0.11.0. + * d/clean: Remove another Autotools remnant + * d/rules: Install NEWS.md as the upstream changelog + * Stop using debhelper's historical special case for a single binary + package. Explicitly list what we intend to install, instead. + * d/watch: Update to v5 format + * d/README.Debian: Mention glycin as a prominent use of bubblewrap + * d/README.Debian: Reduce focus on pre-Debian-10 kernels + + -- Simon McVittie Sat, 21 Mar 2026 18:52:56 +0000 + bubblewrap (0.11.0-2) unstable; urgency=medium [ Helmut Grohne ] diff -Nru bubblewrap-0.11.0/debian/control bubblewrap-0.12.0/debian/control --- bubblewrap-0.11.0/debian/control 2026-04-26 13:05:43.000000000 +0000 +++ bubblewrap-0.12.0/debian/control 2026-08-26 11:04:21.000000000 +0000 @@ -41,9 +41,5 @@ or to run programs in a different library stack such as a Flatpak runtime or a different Debian release. . - By default, this package relies on a kernel with user namespaces enabled. + This package relies on a kernel with user namespaces enabled. Official Debian and Ubuntu kernels are suitable. - . - On kernels without user namespaces, system administrators can make the - bwrap executable setuid root, allowing it to create unprivileged - containers even though ordinary user processes cannot. diff -Nru bubblewrap-0.11.0/debian/copyright bubblewrap-0.12.0/debian/copyright --- bubblewrap-0.11.0/debian/copyright 2026-04-26 13:05:43.000000000 +0000 +++ bubblewrap-0.12.0/debian/copyright 2026-08-26 11:04:21.000000000 +0000 @@ -7,7 +7,7 @@ 2016 Alexander Larsson 2021 Simon McVittie 2022 Collabora Ltd. -License: LGPL-2+ +License: LGPL-2.1+ Files: bubblewrap.jpg Copyright: none @@ -19,7 +19,7 @@ 2016 Laszlo Boszormenyi (GCS) 2016-2024 Simon McVittie 2016-2024 Collabora Ltd. -License: LGPL-2+ +License: LGPL-2+ and LGPL-2.1+ License: LGPL-2+ This library is free software; you can redistribute it and/or @@ -32,10 +32,19 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. . - You should have received a copy of the GNU Library 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. - . On Debian systems, the full text of the GNU Library General Public License version 2 can be found in the file `/usr/share/common-licenses/LGPL-2'. + +License: LGPL-2.1+ + 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. + . + On Debian systems, the full text of the GNU Lesser General Public License + version 2.1 can be found in the file `/usr/share/common-licenses/LGPL-2.1'. diff -Nru bubblewrap-0.11.0/debian/patches/CVE-2026-41163/Don-t-run-the-privilege-separated-code-dumpable.patch bubblewrap-0.12.0/debian/patches/CVE-2026-41163/Don-t-run-the-privilege-separated-code-dumpable.patch --- bubblewrap-0.11.0/debian/patches/CVE-2026-41163/Don-t-run-the-privilege-separated-code-dumpable.patch 2026-04-26 13:05:43.000000000 +0000 +++ bubblewrap-0.12.0/debian/patches/CVE-2026-41163/Don-t-run-the-privilege-separated-code-dumpable.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,89 +0,0 @@ -From: Alexander Larsson -Date: Tue, 14 Apr 2026 11:46:12 +0200 -Subject: Don't run the privilege separated code dumpable -MIME-Version: 1.0 -Content-Type: text/plain; charset="utf-8" -Content-Transfer-Encoding: 8bit - -If the unprivileged code is dumpable, then it can be attached to with -ptrace and execute arbitrary requests to the privileged part of the -setup. In some cases this allows privilege escalation, such as using -overlay mounts which would otherwise not be allowed in setuid mode. - -Note: We still make the monitor process and the main child process -dumpable, because otherwise the owner of /proc/self is root which -makes these processed not able to use features that are needed for -e.g. detarmining portal access. - -Initially reported by François Diakhate - -Signed-off-by: Alexander Larsson -Origin: upstream, 0.11.2, commit:10ed62f34a4565188887b06df1438ef8002c3c80 -Bug: https://github.com/containers/bubblewrap/security/advisories/GHSA-xq78-7hw4-5jvp -Bug-CVE: CVE-2026-41163 -Bug-Debian: https://bugs.debian.org/1134704 ---- - bubblewrap.c | 22 ++++++++++++++-------- - 1 file changed, 14 insertions(+), 8 deletions(-) - -diff --git a/bubblewrap.c b/bubblewrap.c -index f8728c7..0d8d54d 100644 ---- a/bubblewrap.c -+++ b/bubblewrap.c -@@ -937,7 +937,8 @@ switch_to_user_with_privs (void) - /* Call setuid() and use capset() to adjust capabilities */ - static void - drop_privs (bool keep_requested_caps, -- bool already_changed_uid) -+ bool already_changed_uid, -+ bool set_dumpable) - { - assert (!keep_requested_caps || !is_privileged); - /* Drop root uid */ -@@ -947,9 +948,12 @@ drop_privs (bool keep_requested_caps, - - drop_all_caps (keep_requested_caps); - -- /* We don't have any privs now, so mark us dumpable which makes /proc/self be owned by the user instead of root */ -- if (prctl (PR_SET_DUMPABLE, 1, 0, 0, 0) != 0) -- die_with_error ("can't set dumpable"); -+ if (set_dumpable) -+ { -+ /* We don't have any privs now, so mark us dumpable which makes /proc/self be owned by the user instead of root */ -+ if (prctl (PR_SET_DUMPABLE, 1, 0, 0, 0) != 0) -+ die_with_error ("can't set dumpable"); -+ } - } - - static void -@@ -3175,7 +3179,7 @@ main (int argc, - die_with_error ("Setting userns2 failed"); - - /* We don't need any privileges in the launcher, drop them immediately. */ -- drop_privs (false, false); -+ drop_privs (false, false, true); - - /* Optionally bind our lifecycle to that of the parent */ - handle_die_with_parent (); -@@ -3362,8 +3366,10 @@ main (int argc, - - if (child == 0) - { -- /* Unprivileged setup process */ -- drop_privs (false, true); -+ /* Unprivileged setup process. -+ * Note: Don't set dumpable, because we can still perform privileged -+ * operations via privileged_op(). */ -+ drop_privs (false, true, false); - close (privsep_sockets[0]); - setup_newroot (opt_unshare_pid, privsep_sockets[1]); - exit (0); -@@ -3492,7 +3498,7 @@ main (int argc, - } - - /* All privileged ops are done now, so drop caps we don't need */ -- drop_privs (!is_privileged, true); -+ drop_privs (!is_privileged, true, true); - - if (opt_block_fd != -1) - { diff -Nru bubblewrap-0.11.0/debian/patches/CVE-2026-41163/fix-harden-privsep-parent-against-unexpected-operations.patch bubblewrap-0.12.0/debian/patches/CVE-2026-41163/fix-harden-privsep-parent-against-unexpected-operations.patch --- bubblewrap-0.11.0/debian/patches/CVE-2026-41163/fix-harden-privsep-parent-against-unexpected-operations.patch 2026-04-26 13:05:43.000000000 +0000 +++ bubblewrap-0.12.0/debian/patches/CVE-2026-41163/fix-harden-privsep-parent-against-unexpected-operations.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,36 +0,0 @@ -From: =?utf-8?q?Fran=C3=A7ois_Diakhat=C3=A9?= -Date: Thu, 9 Apr 2026 18:17:33 +0000 -Subject: fix: harden privsep parent against unexpected operations - -Origin: upstream, 0.11.2, commit:9e16c35582af7704958dd2b2ec3cba524c9605b6 -Bug: https://github.com/containers/bubblewrap/security/advisories/GHSA-xq78-7hw4-5jvp -Bug-CVE: CVE-2026-41163 -Bug-Debian: https://bugs.debian.org/1134704 ---- - bubblewrap.c | 6 +++++- - 1 file changed, 5 insertions(+), 1 deletion(-) - -diff --git a/bubblewrap.c b/bubblewrap.c -index 0d8d54d..42efe2a 100644 ---- a/bubblewrap.c -+++ b/bubblewrap.c -@@ -1158,7 +1158,9 @@ privileged_op (int privileged_op_socket, - break; - - case PRIV_SEP_OP_OVERLAY_MOUNT: -- if (mount ("overlay", arg2, "overlay", MS_MGC_VAL, arg1) != 0) -+ if (is_privileged) -+ die ("Overlay mounts are not supported in setuid mode"); -+ if (mount ("overlay", arg2, "overlay", MS_MGC_VAL | MS_NOSUID | MS_NODEV, arg1) != 0) - { - /* The standard message for ELOOP, "Too many levels of symbolic - * links", is not helpful here. */ -@@ -1176,6 +1178,8 @@ privileged_op (int privileged_op_socket, - something manages to send hacked priv-sep operation requests. */ - if (!opt_unshare_uts) - die ("Refusing to set hostname in original namespace"); -+ if (arg1 == NULL) -+ die ("Hostname argument is NULL"); - if (sethostname (arg1, strlen(arg1)) != 0) - die_with_error ("Can't set hostname to %s", arg1); - break; diff -Nru bubblewrap-0.11.0/debian/patches/debian/Change-EPERM-error-message-to-show-Debian-specific-inform.patch bubblewrap-0.12.0/debian/patches/debian/Change-EPERM-error-message-to-show-Debian-specific-inform.patch --- bubblewrap-0.11.0/debian/patches/debian/Change-EPERM-error-message-to-show-Debian-specific-inform.patch 2026-04-26 13:05:43.000000000 +0000 +++ bubblewrap-0.12.0/debian/patches/debian/Change-EPERM-error-message-to-show-Debian-specific-inform.patch 2026-08-26 11:04:21.000000000 +0000 @@ -8,15 +8,15 @@ 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bubblewrap.c b/bubblewrap.c -index 42efe2a..3e48d26 100644 +index 9192550..d3f17af 100644 --- a/bubblewrap.c +++ b/bubblewrap.c -@@ -3136,7 +3136,7 @@ main (int argc, +@@ -3080,7 +3080,7 @@ main (int argc, if (errno == EINVAL) - die ("Creating new namespace failed, likely because the kernel does not support user namespaces. bwrap must be installed setuid on such systems."); - else if (errno == EPERM && !is_privileged) -- die ("No permissions to creating new namespace, likely because the kernel does not allow non-privileged user namespaces. On e.g. debian this can be enabled with 'sysctl kernel.unprivileged_userns_clone=1'."); -+ die ("No permissions to create new namespace, likely because the kernel does not allow non-privileged user namespaces. See or ."); + die ("Creating new namespace failed, likely because the kernel does not support user namespaces."); + else if (errno == EPERM) +- die ("No permissions to create a new namespace, likely because the kernel does not allow non-privileged user namespaces. On e.g. debian this can be enabled with 'sysctl kernel.unprivileged_userns_clone=1'."); ++ die ("No permissions to create a new namespace, likely because the kernel does not allow non-privileged user namespaces. See or ."); } if (errno == ENOSPC) diff -Nru bubblewrap-0.11.0/debian/patches/series bubblewrap-0.12.0/debian/patches/series --- bubblewrap-0.11.0/debian/patches/series 2026-04-26 13:05:43.000000000 +0000 +++ bubblewrap-0.12.0/debian/patches/series 2026-08-26 11:04:21.000000000 +0000 @@ -1,3 +1 @@ -CVE-2026-41163/Don-t-run-the-privilege-separated-code-dumpable.patch -CVE-2026-41163/fix-harden-privsep-parent-against-unexpected-operations.patch debian/Change-EPERM-error-message-to-show-Debian-specific-inform.patch diff -Nru bubblewrap-0.11.0/debian/rules bubblewrap-0.12.0/debian/rules --- bubblewrap-0.11.0/debian/rules 2026-04-26 13:05:43.000000000 +0000 +++ bubblewrap-0.12.0/debian/rules 2026-08-26 11:04:21.000000000 +0000 @@ -14,6 +14,7 @@ override_dh_auto_configure: dh_auto_configure \ -- \ + -Dassume_kernel=5.10.0 \ -Dzsh_completion_dir=/usr/share/zsh/vendor-completions \ ${NULL} .PHONY: override_dh_auto_configure @@ -25,3 +26,6 @@ env -u LD_PRELOAD dh_auto_test .PHONY: override_dh_auto_test + +override_dh_installchangelogs: + dh_installchangelogs NEWS.md diff -Nru bubblewrap-0.11.0/debian/salsa-ci.yml bubblewrap-0.12.0/debian/salsa-ci.yml --- bubblewrap-0.11.0/debian/salsa-ci.yml 2026-04-26 13:05:43.000000000 +0000 +++ bubblewrap-0.12.0/debian/salsa-ci.yml 2026-08-26 11:04:21.000000000 +0000 @@ -1,5 +1,12 @@ +--- include: - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml variables: + # This package is Architecture: linux-any so the default build job is enough + SALSA_CI_DISABLE_BUILD_PACKAGE_ALL: 1 + SALSA_CI_DISABLE_BUILD_PACKAGE_ANY: 1 + + SALSA_CI_DISABLE_CROSSBUILD_ARM64: 0 + SALSA_CI_IGNORED_BRANCHES: '/^(ppa|ubuntu)\//' diff -Nru bubblewrap-0.11.0/meson.build bubblewrap-0.12.0/meson.build --- bubblewrap-0.11.0/meson.build 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/meson.build 2026-08-26 10:08:29.000000000 +0000 @@ -1,7 +1,7 @@ project( 'bubblewrap', 'c', - version : '0.11.0', + version : '0.12.0', meson_version : '>=0.49.0', default_options : [ 'warning_level=2', @@ -87,8 +87,16 @@ endif endif -if get_option('require_userns') - cdata.set('ENABLE_REQUIRE_USERNS', 1) +assume_kernel = get_option('assume_kernel') +if assume_kernel != '' + kernel_parts = assume_kernel.split('.') + if kernel_parts.length() != 3 + error('assume_kernel must be three dot-separated integers, e.g. 6.1.0') + endif + cdata.set('ASSUMED_KERNEL_MAJOR', kernel_parts[0].to_int()) + cdata.set('ASSUMED_KERNEL_MINOR', kernel_parts[1].to_int()) + cdata.set('ASSUMED_KERNEL_PATCH', kernel_parts[2].to_int()) + cdata.set('HAVE_ASSUMED_KERNEL', 1) endif configure_file( @@ -115,6 +123,8 @@ 'bind-mount.c', 'network.c', 'utils.c', + 'chroot_realpath.c', + 'safe_openat.c', ], build_rpath : get_option('build_rpath'), install : true, diff -Nru bubblewrap-0.11.0/meson_options.txt bubblewrap-0.12.0/meson_options.txt --- bubblewrap-0.11.0/meson_options.txt 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/meson_options.txt 2026-08-26 10:08:29.000000000 +0000 @@ -42,12 +42,6 @@ description : 'Path to Python 3, or empty to use python3', ) option( - 'require_userns', - type : 'boolean', - description : 'require user namespaces by default when installed setuid', - value : false, -) -option( 'selinux', type : 'feature', description : 'enable optional SELINUX support', @@ -60,6 +54,12 @@ value : true, ) option( + 'assume_kernel', + type : 'string', + description : 'Assume at least this kernel version (e.g. 6.1.0). Compile out fallback code for older kernels.', + value : '', +) +option( 'zsh_completion', type : 'feature', description : 'install zsh completion script', diff -Nru bubblewrap-0.11.0/release-checklist.md bubblewrap-0.12.0/release-checklist.md --- bubblewrap-0.11.0/release-checklist.md 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/release-checklist.md 2026-08-26 10:08:29.000000000 +0000 @@ -1,13 +1,13 @@ bubblewrap release checklist ============================ -* Collect release notes in `NEWS` -* Update version number in `meson.build` and release date in `NEWS` +* Collect release notes in `NEWS.md` +* Update version number in `meson.build` and release date in `NEWS.md` * Commit the changes * `meson dist -C ${builddir}` * Do any final smoke-testing, e.g. update a package, install and test it * `git evtag sign v$VERSION` - * Include the release notes from `NEWS` in the tag message + * Include the release notes from `NEWS.md` in the tag message * `git push --atomic origin main v$VERSION` * https://github.com/containers/bubblewrap/releases/new * Fill in the new version's tag in the "Tag version" box diff -Nru bubblewrap-0.11.0/safe_openat.c bubblewrap-0.12.0/safe_openat.c --- bubblewrap-0.11.0/safe_openat.c 1970-01-01 00:00:00.000000000 +0000 +++ bubblewrap-0.12.0/safe_openat.c 2026-08-26 10:08:29.000000000 +0000 @@ -0,0 +1,332 @@ +/* + * This code was copied from: + * + * crun - OCI runtime written in C + * + * Copyright (C) 2017, 2018, 2019 Giuseppe Scrivano + * crun 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. + * + * crun 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 crun. If not, see . + */ +#include "config.h" + +#include "utils.h" +#include +#include +#include +#include +#ifndef __set_errno +#define __set_errno(val) ((errno) = (val)) +#endif + +bool opt_force_openat_fallback = false; + +#if ASSUMED_KERNEL < BWRAP_KERNEL_VERSION (5, 6, 0) +# define USE_OPENAT_FALLBACK 1 +#else +# define USE_OPENAT_FALLBACK 0 +#endif + +#define cleanup_close cleanup_fd + +#define LIKELY(x) __builtin_expect ((x), 1) +#define UNLIKELY(x) __builtin_expect ((x), 0) + +/* Adapted from systemd. Include space for the NUL byte. */ +#define DECIMAL_STR_MAX(type) \ + ((size_t) 2U + (sizeof (type) <= 1 ? 3U : sizeof (type) <= 2 ? 5U \ + : sizeof (type) <= 4 ? 10U \ + : sizeof (type) <= 8 ? 20U \ + : sizeof (int[-2 * (sizeof (type) > 8)]))) + +#define _STRLEN(s) (sizeof (s) - 1) + +/* openat2 resolve flags */ +#ifndef RESOLVE_IN_ROOT +# define RESOLVE_IN_ROOT 0x10 +#endif +#ifndef RESOLVE_NO_MAGICLINKS +# define RESOLVE_NO_MAGICLINKS 0x02 +#endif + +/* Structures for syscalls */ +struct openat2_open_how +{ + uint64_t flags; + uint64_t mode; + uint64_t resolve; +}; + +static inline int +syscall_openat2 (int dirfd, const char *path, uint64_t flags, uint64_t mode, uint64_t resolve) +{ +#ifdef __NR_openat2 + struct openat2_open_how how = { + .flags = flags, + .mode = mode, + .resolve = resolve, + }; + return (int) syscall (__NR_openat2, dirfd, path, &how, sizeof (how), 0); +#else + (void) dirfd; + (void) path; + (void) flags; + (void) mode; + (void) resolve; + errno = ENOSYS; + return -1; +#endif +} + +#define consume_slashes(t) \ + ({ \ + typeof (t) _s = (t); \ + while (*_s == '/') \ + _s++; \ + _s; \ + }) + + +/* _STRLEN("self") < DECIMAL_STR_MAX (pid_t), so we don't need to calculate the length of both. */ +#define PROC_PID_FD_STRLEN (_STRLEN ("/proc/") + DECIMAL_STR_MAX (pid_t) \ + + _STRLEN ("/fd/") + DECIMAL_STR_MAX (int)) + +/* A buffer long enough to hold either /proc/self/fd/$FD or a /proc/$PID/fd/$FD path. */ +typedef char proc_fd_path_t[PROC_PID_FD_STRLEN]; + +static inline void +get_proc_fd_path (proc_fd_path_t path, pid_t pid, int fd) +{ + const size_t max_len = sizeof (proc_fd_path_t); + size_t n; + + if (pid) + n = snprintf (path, max_len, "/proc/%d/fd/%d", pid, fd); + else + n = snprintf (path, max_len, "/proc/self/fd/%d", fd); + + if (UNLIKELY (n >= max_len)) + abort (); +} + +static inline void +get_proc_self_fd_path (proc_fd_path_t path, int fd) +{ + get_proc_fd_path (path, 0, fd); +} + + +static int +check_fd_is_path (const char *path, int fd, UNUSED const char *fdname) +{ + proc_fd_path_t fdpath; + size_t path_len = strlen (path); + char link[PATH_MAX]; + int ret; + + get_proc_self_fd_path (fdpath, fd); + ret = TEMP_FAILURE_RETRY (readlink (fdpath, link, sizeof (link))); + if (UNLIKELY (ret < 0)) + return -1; + + if (((size_t) ret) != path_len || memcmp (link, path, path_len)) + { + // crun_make_error (err, 0, "target `%s` does not point to the directory `%s`", fdname, path); + __set_errno(ENOTDIR); + return -1; + } + + return 0; +} + +#if USE_OPENAT_FALLBACK +static int +check_fd_under_path (const char *rootfs, size_t rootfslen, int fd, UNUSED const char *fdname) +{ + proc_fd_path_t fdpath; + char link[PATH_MAX]; + int ret; + + /* Every path is under "/", there is nothing to verify. The check below + would reject any path since it expects a '/' right after the rootfs. */ + if (rootfslen == 0 || (rootfslen == 1 && rootfs[0] == '/')) + return 0; + + get_proc_self_fd_path (fdpath, fd); + ret = TEMP_FAILURE_RETRY (readlink (fdpath, link, sizeof (link))); + if (UNLIKELY (ret < 0)) + return -1; + + if (((size_t) ret) <= rootfslen || memcmp (link, rootfs, rootfslen) != 0 || link[rootfslen] != '/') + { + // crun_make_error (err, 0, "target `%s` not under the directory `%s`", fdname, rootfs); + __set_errno(ENOTDIR); + return -1; + } + + return 0; +} +#endif + +/* DIRFD must be a file descriptor for ROOTFS itself: PATH is resolved + against ROOTFS and the result is then opened relatively to DIRFD. */ +static int +safe_openat_fallback (int dirfd, const char *rootfs, const char *path, int flags, + int mode) +{ +#if !USE_OPENAT_FALLBACK + (void) dirfd; + (void) rootfs; + (void) path; + (void) flags; + (void) mode; + errno = ENOSYS; + return -1; +#else + cleanup_free char *parent_path = NULL; + const char *last_component = NULL; + const char *orig_path = path; + const char *path_in_chroot; + cleanup_close int fd = -1; + char resolved[PATH_MAX]; + char buffer[PATH_MAX]; + size_t rootfs_len = is_empty_string (rootfs) ? 0 : strlen (rootfs); + int ret; + + /* chroot_realpath resolves the last component as well, so when O_NOFOLLOW + is requested resolve only the parent directory and let openat(2) deal + with the last component, otherwise a symlink would be followed even + though the caller asked not to. */ + if (flags & O_NOFOLLOW) + { + char *sep; + + parent_path = xstrdup (path); + sep = strrchr (parent_path, '/'); + if (sep == NULL) + { + /* No parent directory, the entire path is the last component. */ + last_component = path; + parent_path[0] = '\0'; + } + else if (sep[1] != '\0') + { + *sep = '\0'; + last_component = path + (sep - parent_path) + 1; + } + /* A trailing '/' forces the symlink to be resolved anyway, so in that + case keep resolving the entire path. */ + + if (last_component) + path = parent_path; + } + + path_in_chroot = chroot_realpath (rootfs, path, buffer); + if (path_in_chroot == NULL) + return -1; + + /* When rootfs is "/" or not set, chroot_realpath returns the path + unchanged, so drop the prefix only when it is really there. */ + if (rootfs_len > 0 && strncmp (path_in_chroot, rootfs, rootfs_len) == 0) + path_in_chroot += rootfs_len; + path_in_chroot = consume_slashes (path_in_chroot); + + if (last_component) + { + ret = snprintf (resolved, sizeof (resolved), "%s%s%s", path_in_chroot, + path_in_chroot[0] == '\0' ? "" : "/", last_component); + if (UNLIKELY (ret >= (int) sizeof (resolved))) + { + __set_errno (ENAMETOOLONG); + return -1; + } + + path_in_chroot = resolved; + } + + /* If the path is empty we are at the root, dup the dirfd itself. */ + if (path_in_chroot[0] == '\0') + { + ret = dup (dirfd); + if (UNLIKELY (ret < 0)) + return -1; + return ret; + } + + ret = openat (dirfd, path_in_chroot, flags, mode); + if (UNLIKELY (ret < 0)) + return -1; + + fd = ret; + + ret = check_fd_under_path (rootfs, rootfs_len, fd, orig_path); + if (UNLIKELY (ret < 0)) + return ret; + + ret = fd; + fd = -1; + return ret; +#endif /* USE_OPENAT_FALLBACK */ +} + +int +safe_openat (int dirfd, const char *rootfs, const char *path, int flags, int mode) +{ + static bool openat2_supported = true; + int ret; + + if (is_empty_string (path)) + { + cleanup_close int fd = -1; + + fd = open (rootfs, flags, mode); + if (UNLIKELY (fd < 0)) + return -1; + + /* Skip the readlink-based check when opening the root + directory itself (rootfs="/", path=""). After pivot_root, + "/" can only refer to the container root so the readlink + verification is redundant, and after setns the /proc-based + readlink may not be reachable by path yet. */ + if (rootfs[0] != '/' || rootfs[1] != '\0') + { + ret = check_fd_is_path (rootfs, fd, path); + if (UNLIKELY (ret < 0)) + return ret; + } + + ret = fd; + fd = -1; + return ret; + } + + if (openat2_supported && !opt_force_openat_fallback) + { + repeat: + ret = syscall_openat2 (dirfd, path, flags, mode, RESOLVE_IN_ROOT|RESOLVE_NO_MAGICLINKS); + if (UNLIKELY (ret < 0)) + { + if (errno == EINTR || errno == EAGAIN) + goto repeat; + if (errno == ENOSYS) + openat2_supported = false; + if (errno == ENOSYS || errno == EINVAL || errno == EPERM) + return safe_openat_fallback (dirfd, rootfs, path, flags, mode); + + return -1; + } + + return ret; + } + + return safe_openat_fallback (dirfd, rootfs, path, flags, mode); +} diff -Nru bubblewrap-0.11.0/tests/libtest-core.sh bubblewrap-0.12.0/tests/libtest-core.sh --- bubblewrap-0.11.0/tests/libtest-core.sh 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/tests/libtest-core.sh 2026-08-26 10:08:29.000000000 +0000 @@ -10,12 +10,12 @@ # # Copyright (C) 2017 Colin Walters # -# SPDX-License-Identifier: LGPL-2.0-or-later +# SPDX-License-Identifier: LGPL-2.1-or-later # # 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. +# 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 diff -Nru bubblewrap-0.11.0/tests/libtest.sh bubblewrap-0.12.0/tests/libtest.sh --- bubblewrap-0.11.0/tests/libtest.sh 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/tests/libtest.sh 2026-08-26 10:08:29.000000000 +0000 @@ -4,12 +4,12 @@ # Add non-bubblewrap-specific code to libtest-core.sh instead. # # Copyright (C) 2017 Colin Walters -# SPDX-License-Identifier: LGPL-2.0-or-later +# SPDX-License-Identifier: LGPL-2.1-or-later # # 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. +# 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 diff -Nru bubblewrap-0.11.0/tests/meson.build bubblewrap-0.12.0/tests/meson.build --- bubblewrap-0.11.0/tests/meson.build 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/tests/meson.build 2026-08-26 10:08:29.000000000 +0000 @@ -17,6 +17,7 @@ test_scripts = [ 'test-run.sh', + 'test-sandbox.py', 'test-seccomp.py', 'test-specifying-pidns.sh', 'test-specifying-userns.sh', diff -Nru bubblewrap-0.11.0/tests/sandbox_helper.py bubblewrap-0.12.0/tests/sandbox_helper.py --- bubblewrap-0.11.0/tests/sandbox_helper.py 1970-01-01 00:00:00.000000000 +0000 +++ bubblewrap-0.12.0/tests/sandbox_helper.py 2026-08-26 10:08:29.000000000 +0000 @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# Copyright 2024 Alexander Larsson +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Helper script that runs inside a bwrap sandbox. Receives commands +# over a socket fd and sends back JSON results. +# stdout/stderr remain free for debug logging. + +import importlib.util +import json +import os +import socket +import sys +import traceback +import unittest + + +def load_module(module_path): + spec = importlib.util.spec_from_file_location('_sandbox_test', module_path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def handle_run_test(cmd): + info = cmd['run_test'] + mod = load_module(info['module']) + cls = getattr(mod, info['class']) + method_name = info['method'] + + tc = cls(method_name) + try: + getattr(tc, method_name)() + return {'ok': True} + except Exception: + return {'ok': False, 'error': traceback.format_exc()} + + +def handle_eval(cmd): + code = cmd.get('eval', '') + try: + result = eval(code) # noqa: S307 + return {'ok': True, 'result': result} + except Exception as e: + return {'ok': False, 'error': str(e)} + + +def main(): + if len(sys.argv) != 2: + print(f'Usage: {sys.argv[0]} SOCKET_FD', file=sys.stderr) + sys.exit(1) + + fd = int(sys.argv[1]) + sock = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM) + os.close(fd) + + buf = b'' + while True: + chunk = sock.recv(4096) + if not chunk: + break + buf += chunk + + while b'\n' in buf: + line, buf = buf.split(b'\n', 1) + try: + cmd = json.loads(line) + except json.JSONDecodeError as e: + resp = {'ok': False, 'error': f'bad json: {e}'} + sock.sendall(json.dumps(resp).encode() + b'\n') + continue + + if cmd.get('exit'): + sock.close() + return + + if 'run_test' in cmd: + resp = handle_run_test(cmd) + elif 'eval' in cmd: + resp = handle_eval(cmd) + else: + resp = {'ok': False, 'error': 'unknown command'} + + sock.sendall(json.dumps(resp).encode() + b'\n') + + sock.close() + + +if __name__ == '__main__': + main() diff -Nru bubblewrap-0.11.0/tests/test-helper.py bubblewrap-0.12.0/tests/test-helper.py --- bubblewrap-0.11.0/tests/test-helper.py 1970-01-01 00:00:00.000000000 +0000 +++ bubblewrap-0.12.0/tests/test-helper.py 2026-08-26 10:08:29.000000000 +0000 @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +# Copyright 2024 Alexander Larsson +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Test infrastructure for bwrap sandbox integration tests. +# Provides BwrapSandbox context manager, SourceTree fixture, +# @in_sandbox / @sandbox_test_class decorators, and TAP runner. + +import json +import os +import socket +import subprocess +import sys +import unittest + + +BWRAP = os.getenv('BWRAP', 'bwrap') +HELPER = os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'sandbox_helper.py') + + +def in_sandbox(*sandbox_args): + """Decorator: marks a method to run inside a bwrap sandbox. + + Args are bwrap options. Use callables (taking self) for dynamic + values like fixture paths, e.g.: + @in_sandbox('--ro-bind', lambda self: self.src.file, '/tmp/f') + """ + def decorator(fn): + fn._sandbox_args = sandbox_args + return fn + return decorator + + +def sandbox_test_class(cls): + """Class decorator: for each @in_sandbox method, installs a + sandboxed_* copy and replaces the original with a host-side + wrapper that launches bwrap and runs it remotely.""" + for name in list(vars(cls)): + fn = vars(cls)[name] + if not hasattr(fn, '_sandbox_args'): + continue + + sandbox_args = fn._sandbox_args + sandboxed_name = 'sandboxed_' + name + + setattr(cls, sandboxed_name, fn) + + def make_wrapper(sname, sargs): + def wrapper(self): + resolved = [a(self) if callable(a) else a for a in sargs] + with BwrapSandbox(*resolved) as sb: + sb.run_test(self, sname) + return wrapper + + wrapper = make_wrapper(sandboxed_name, sandbox_args) + wrapper.__name__ = name + wrapper.__qualname__ = fn.__qualname__ + setattr(cls, name, wrapper) + + return cls + + +class OpenFd: + """Placeholder in @in_sandbox args: opens path with O_PATH at launch time.""" + def __init__(self, path): + self.path = path + + +class DataFd: + """Placeholder in @in_sandbox/run_bwrap args: creates a pipe with content.""" + def __init__(self, content): + self.content = content + + +class BwrapSandbox: + """Context manager that runs a bwrap sandbox with a Python helper. + + Base setup is --ro-bind / / --dev /dev --proc /proc --tmpfs /tmp, + so test destinations that need to be created should go under /tmp. + """ + + def __init__(self, *extra_args): + self.extra_args = list(extra_args) + self.proc = None + self._parent_sock = None + self._opened_fds = [] + + def __enter__(self): + parent_sock, child_sock = socket.socketpair( + socket.AF_UNIX, socket.SOCK_STREAM, + ) + self._parent_sock = parent_sock + child_fd = child_sock.fileno() + + resolved_args = [] + for arg in self.extra_args: + if isinstance(arg, OpenFd): + fd = os.open(arg.path, os.O_PATH) + self._opened_fds.append(fd) + resolved_args.append(str(fd)) + elif isinstance(arg, DataFd): + r, w = os.pipe() + os.write(w, arg.content) + os.close(w) + self._opened_fds.append(r) + resolved_args.append(str(r)) + else: + resolved_args.append(arg) + + cmd = [ + BWRAP, *BASE_BWRAP_ARGS, + *resolved_args, + 'python3', HELPER, str(child_fd), + ] + + self.proc = subprocess.Popen( + cmd, + pass_fds=(child_fd, *self._opened_fds), + ) + child_sock.close() + for fd in self._opened_fds: + os.close(fd) + self._opened_fds = [] + + self._buf = b'' + return self + + def __exit__(self, *exc): + try: + self._send({'exit': True}) + except OSError: + pass + + self._parent_sock.close() + + if self.proc: + self.proc.wait(timeout=5) + return False + + def _send(self, msg): + self._parent_sock.sendall(json.dumps(msg).encode() + b'\n') + + def _recv(self): + while b'\n' not in self._buf: + chunk = self._parent_sock.recv(4096) + if not chunk: + raise ConnectionError('sandbox helper closed connection') + self._buf += chunk + line, self._buf = self._buf.split(b'\n', 1) + return json.loads(line) + + def run_test(self, test_case, method_name): + """Run a test method from test_case's class inside the sandbox.""" + self._send({ + 'run_test': { + 'module': self._test_module(test_case), + 'class': type(test_case).__name__, + 'method': method_name, + }, + }) + resp = self._recv() + if not resp['ok']: + raise AssertionError( + f'sandboxed test failed:\n{resp["error"]}', + ) + + def _test_module(self, test_case): + """Get the file path of the module that defines the test case.""" + import inspect + return os.path.abspath(inspect.getfile(type(test_case))) + + +BASE_BWRAP_ARGS = ['--ro-bind', '/', '/', '--dev', '/dev', + '--proc', '/proc', '--tmpfs', '/tmp'] + + +def run_bwrap(*extra_args, pass_fds=()): + """Run bwrap with base sandbox args plus extra_args, return CompletedProcess. + + Args may include DataFd instances, which are resolved to pipe fds. + """ + opened_fds = [] + resolved_args = [] + for arg in extra_args: + if isinstance(arg, DataFd): + r, w = os.pipe() + os.write(w, arg.content) + os.close(w) + opened_fds.append(r) + resolved_args.append(str(r)) + else: + resolved_args.append(arg) + cmd = [BWRAP, *BASE_BWRAP_ARGS, *resolved_args] + try: + return subprocess.run( + cmd, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + pass_fds=(*pass_fds, *opened_fds), + ) + finally: + for fd in opened_fds: + os.close(fd) + + +def list_mounts(): + """Return list of mountpoints from /proc/self/mounts.""" + with open('/proc/self/mounts') as f: + return [line.split()[1] for line in f] + + +def can_run_bwrap(): + """Check if bwrap can run at all.""" + try: + r = subprocess.run( + [BWRAP, '--ro-bind', '/', '/', 'true'], + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return r.returncode == 0 + except (FileNotFoundError, OSError): + return False + + +class TAPResult(unittest.TestResult): + def __init__(self): + super().__init__() + self.index = 0 + + def _test_name(self, test): + return test._testMethodName + + def _print_diagnostics(self, test, err): + for line in self._exc_info_to_string(err, test).splitlines(): + print(f'# {line}') + + def addSuccess(self, test): + super().addSuccess(test) + self.index += 1 + print(f'ok {self.index} {self._test_name(test)}') + + def addFailure(self, test, err): + super().addFailure(test, err) + self.index += 1 + print(f'not ok {self.index} {self._test_name(test)}') + self._print_diagnostics(test, err) + + def addError(self, test, err): + super().addError(test, err) + self.index += 1 + print(f'not ok {self.index} {self._test_name(test)}') + self._print_diagnostics(test, err) + + def addSkip(self, test, reason): + super().addSkip(test, reason) + self.index += 1 + print(f'ok {self.index} {self._test_name(test)} # SKIP {reason}') + + +def run_tap_tests(module): + """Run all tests in module with TAP output.""" + loader = unittest.TestLoader() + suite = loader.loadTestsFromModule(module) + result = TAPResult() + suite.run(result) + print(f'1..{result.index}') + sys.exit(0 if result.wasSuccessful() else 1) diff -Nru bubblewrap-0.11.0/tests/test-run.sh bubblewrap-0.12.0/tests/test-run.sh --- bubblewrap-0.11.0/tests/test-run.sh 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/tests/test-run.sh 2026-08-26 10:08:29.000000000 +0000 @@ -70,10 +70,6 @@ else ok_skip "not sure what unreadable file to use" fi - - # bind dest in symlink (https://github.com/projectatomic/bubblewrap/pull/119) - $RUN $ALT --dir /tmp/dir --symlink dir /tmp/link --bind /etc /tmp/link true - ok "can bind a destination over a symlink" done # Test symlink behaviour @@ -689,4 +685,13 @@ fi fi +# Smoke-test --not-a-security-boundary +# +# Setting up an unavailable automount and triggering the right conditions is +# complicated to do here, but we can at least check that the option is there, +# and that it stays there. + +$RUN --not-a-security-boundary true +ok "Accepts --not-a-security-boundary" + done_testing diff -Nru bubblewrap-0.11.0/tests/test-sandbox.py bubblewrap-0.12.0/tests/test-sandbox.py --- bubblewrap-0.11.0/tests/test-sandbox.py 1970-01-01 00:00:00.000000000 +0000 +++ bubblewrap-0.12.0/tests/test-sandbox.py 2026-08-26 10:08:29.000000000 +0000 @@ -0,0 +1,690 @@ +#!/usr/bin/env python3 +# Copyright 2024 Alexander Larsson +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Integration tests for bwrap sandbox setup. Methods decorated with +# @in_sandbox(...) define assertions that run inside the sandbox; +# the @sandbox_test_class decorator auto-generates the host-side +# test_* methods that launch bwrap and run them. + +import ctypes +import ctypes.util +import importlib.util +import os +import stat +import subprocess +import sys +import tempfile +import unittest + +_spec = importlib.util.spec_from_file_location( + 'test_helper', + os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test-helper.py'), +) +_helper = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_helper) + +BWRAP = _helper.BWRAP +can_run_bwrap = _helper.can_run_bwrap +in_sandbox = _helper.in_sandbox +list_mounts = _helper.list_mounts +DataFd = _helper.DataFd +OpenFd = _helper.OpenFd +run_bwrap = _helper.run_bwrap +run_tap_tests = _helper.run_tap_tests +sandbox_test_class = _helper.sandbox_test_class + + +class SourceTree: + """Creates a temporary tree with various file types for testing.""" + + def __init__(self, tmp_dir): + self.root = os.path.join(tmp_dir, 'src') + os.makedirs(self.root) + + self.file = os.path.join(self.root, 'regular_file') + with open(self.file, 'w') as f: + f.write('hello') + + self.other_file = os.path.join(self.root, 'other_file') + with open(self.other_file, 'w') as f: + f.write('other') + + self.dir = os.path.join(self.root, 'subdir') + os.makedirs(self.dir) + with open(os.path.join(self.dir, 'child'), 'w') as f: + f.write('child content') + + self.dir2 = os.path.join(self.root, 'subdir2') + os.makedirs(self.dir2) + with open(os.path.join(self.dir2, 'child2'), 'w') as f: + f.write('child2 content') + + self.symlink_to_file = os.path.join(self.root, 'link_to_file') + os.symlink(self.file, self.symlink_to_file) + + self.symlink_to_dir = os.path.join(self.root, 'link_to_dir') + os.symlink(self.dir, self.symlink_to_dir) + + self.symlink_relative = os.path.join(self.root, 'link_relative') + os.symlink('regular_file', self.symlink_relative) + + +@sandbox_test_class +@unittest.skipUnless(can_run_bwrap(), 'bwrap not available or not functional') +class TestSandbox(unittest.TestCase): + """Integration tests for bwrap sandbox setup options.""" + + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.tmp = self._tmp.name + self.src = SourceTree(self.tmp) + + def tearDown(self): + self._tmp.cleanup() + + def assertIsRegFile(self, path): + self.assertTrue(stat.S_ISREG(os.lstat(path).st_mode), + f'{path} is not a regular file') + + def assertIsChrDev(self, path): + self.assertTrue(stat.S_ISCHR(os.lstat(path).st_mode), + f'{path} is not a character device') + + def assertIsDir(self, path): + self.assertTrue(stat.S_ISDIR(os.lstat(path).st_mode), + f'{path} is not a directory') + + def assertIsSymlink(self, path): + self.assertTrue(stat.S_ISLNK(os.lstat(path).st_mode), + f'{path} is not a symlink') + + def assertSymlinkContent(self, path, expected): + self.assertIsSymlink(path) + self.assertEqual(os.readlink(path), expected, + f'{path} points to {os.readlink(path)!r}, expected {expected!r}') + + def assertInStderr(self, needle, result): + self.assertIn(needle, result.stderr, + f'expected {needle!r} in stderr, got: {result.stderr!r}') + + def assertFileContent(self, path, expected): + with open(path) as f: + self.assertEqual(f.read(), expected) + + _libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True) + TMPFS_MAGIC = 0x01021994 + + def _statfs_type(self, path): + class statfs_t(ctypes.Structure): + _fields_ = [('f_type', ctypes.c_long), ('f_bsize', ctypes.c_long)] + buf = statfs_t() + rc = self._libc.statfs(path.encode(), ctypes.byref(buf)) + self.assertEqual(rc, 0, f'statfs({path}) failed') + return buf.f_type + + def assertIsTmpfs(self, path): + fs_type = self._statfs_type(path) + self.assertEqual(fs_type, self.TMPFS_MAGIC, + f'{path} is not tmpfs (f_type=0x{fs_type:x})') + + def assertIsMountpoint(self, path): + self.assertIn(path, list_mounts(), + f'{path} is not a mountpoint') + + def assertBwrapFailed(self, result): + self.assertNotEqual(result.returncode, 0, + f'expected bwrap to fail, stderr: {result.stderr!r}') + + def assertMountFlags(self, path, expected, unexpected): + flags = os.statvfs(path).f_flag + for f in expected: + self.assertTrue(flags & f, f'{path}: expected flag {f:#x} not set') + for f in unexpected: + self.assertFalse(flags & f, f'{path}: unexpected flag {f:#x} set') + + # ------ Source is a regular file ------ + + @in_sandbox('--ro-bind', lambda self: self.src.file, '/tmp/f') + def test_ro_bind_file(self): + self.assertIsRegFile('/tmp/f') + self.assertFileContent('/tmp/f', 'hello') + self.assertIsMountpoint('/tmp/f') + self.assertMountFlags('/tmp/f', + [os.ST_RDONLY, os.ST_NOSUID, os.ST_NODEV], []) + + @in_sandbox('--bind', lambda self: self.src.file, '/tmp/f') + def test_bind_file(self): + self.assertIsRegFile('/tmp/f') + self.assertFileContent('/tmp/f', 'hello') + self.assertIsMountpoint('/tmp/f') + self.assertMountFlags('/tmp/f', + [os.ST_NOSUID, os.ST_NODEV], [os.ST_RDONLY]) + + @in_sandbox('--dev-bind', '/dev/null', '/tmp/devnull') + def test_dev_bind_file(self): + self.assertIsChrDev('/tmp/devnull') + self.assertIsMountpoint('/tmp/devnull') + self.assertMountFlags('/tmp/devnull', + [os.ST_NOSUID], [os.ST_RDONLY, os.ST_NODEV]) + + # ------ Source is a directory ------ + + @in_sandbox('--ro-bind', lambda self: self.src.dir, '/tmp/d') + def test_ro_bind_dir(self): + self.assertIsDir('/tmp/d') + self.assertFileContent('/tmp/d/child', 'child content') + self.assertIsMountpoint('/tmp/d') + self.assertMountFlags('/tmp/d', + [os.ST_RDONLY, os.ST_NOSUID, os.ST_NODEV], []) + + @in_sandbox('--bind', lambda self: self.src.dir, '/tmp/d') + def test_bind_dir(self): + self.assertIsDir('/tmp/d') + self.assertFileContent('/tmp/d/child', 'child content') + self.assertIsMountpoint('/tmp/d') + self.assertMountFlags('/tmp/d', + [os.ST_NOSUID, os.ST_NODEV], [os.ST_RDONLY]) + + @in_sandbox('--dev-bind', '/dev', '/tmp/d') + def test_dev_bind_dir(self): + self.assertIsDir('/tmp/d') + self.assertIsMountpoint('/tmp/d') + self.assertMountFlags('/tmp/d', + [os.ST_NOSUID], [os.ST_RDONLY, os.ST_NODEV]) + + # ------ Source is a symlink (should resolve to target) ------ + + @in_sandbox('--ro-bind', lambda self: self.src.symlink_to_file, '/tmp/f') + def test_ro_bind_symlink_to_file(self): + self.assertIsRegFile('/tmp/f') + self.assertFileContent('/tmp/f', 'hello') + self.assertMountFlags('/tmp/f', + [os.ST_RDONLY, os.ST_NOSUID, os.ST_NODEV], []) + + @in_sandbox('--ro-bind', lambda self: self.src.symlink_to_dir, '/tmp/d') + def test_ro_bind_symlink_to_dir(self): + self.assertIsDir('/tmp/d') + self.assertFileContent('/tmp/d/child', 'child content') + self.assertMountFlags('/tmp/d', + [os.ST_RDONLY, os.ST_NOSUID, os.ST_NODEV], []) + + @in_sandbox('--ro-bind', lambda self: self.src.symlink_relative, '/tmp/f') + def test_ro_bind_relative_symlink(self): + self.assertIsRegFile('/tmp/f') + self.assertFileContent('/tmp/f', 'hello') + self.assertMountFlags('/tmp/f', + [os.ST_RDONLY, os.ST_NOSUID, os.ST_NODEV], []) + + # ------ Destination parent creation ------ + + @in_sandbox('--ro-bind', lambda self: self.src.file, + '/tmp/deep/nested/path/file') + def test_bind_creates_parent_dirs(self): + self.assertIsDir('/tmp/deep/nested/path') + self.assertFileContent('/tmp/deep/nested/path/file', 'hello') + + @in_sandbox('--ro-bind', lambda self: self.src.dir, + '/tmp/deep/nested/dir') + def test_bind_dir_creates_parent_dirs(self): + self.assertIsDir('/tmp/deep/nested/dir') + self.assertFileContent('/tmp/deep/nested/dir/child', 'child content') + + # ------ Destination already exists ------ + + @in_sandbox('--dir', '/tmp/existing', + '--ro-bind', lambda self: self.src.dir, '/tmp/existing') + def test_bind_over_existing_dir(self): + self.assertFileContent('/tmp/existing/child', 'child content') + + @in_sandbox('--ro-bind', lambda self: self.src.other_file, '/tmp/f', + '--ro-bind', lambda self: self.src.file, '/tmp/f') + def test_bind_over_existing_file(self): + self.assertFileContent('/tmp/f', 'hello') + + # ------ Destination is a symlink (should fail) ------ + + def test_bind_to_symlink_dest_fails(self): + result = run_bwrap('--dir', '/tmp/dir', + '--symlink', 'dir', '/tmp/link', + '--ro-bind', self.src.dir, '/tmp/link', + 'true') + self.assertBwrapFailed(result) + self.assertInStderr(b"Can't mount on symlink destination", result) + + def test_bind_to_symlink_dest_fails_file(self): + result = run_bwrap('--symlink', 'dir', '/tmp/link', + '--ro-bind', self.src.file, '/tmp/link', + 'true') + self.assertBwrapFailed(result) + self.assertInStderr(b"Can't mount on symlink destination", result) + + # ------ bind-fd / ro-bind-fd ------ + + @in_sandbox('--ro-bind-fd', lambda self: OpenFd(self.src.file), '/tmp/f') + def test_ro_bind_fd_file(self): + self.assertIsRegFile('/tmp/f') + self.assertFileContent('/tmp/f', 'hello') + self.assertMountFlags('/tmp/f', + [os.ST_RDONLY, os.ST_NOSUID, os.ST_NODEV], []) + + @in_sandbox('--ro-bind-fd', lambda self: OpenFd(self.src.dir), '/tmp/d') + def test_ro_bind_fd_dir(self): + self.assertIsDir('/tmp/d') + self.assertFileContent('/tmp/d/child', 'child content') + self.assertMountFlags('/tmp/d', + [os.ST_RDONLY, os.ST_NOSUID, os.ST_NODEV], []) + + @in_sandbox('--bind-fd', lambda self: OpenFd(self.src.file), '/tmp/f') + def test_bind_fd_file(self): + self.assertIsRegFile('/tmp/f') + self.assertFileContent('/tmp/f', 'hello') + self.assertMountFlags('/tmp/f', + [os.ST_NOSUID, os.ST_NODEV], [os.ST_RDONLY]) + + @in_sandbox('--bind-fd', lambda self: OpenFd(self.src.dir), '/tmp/d') + def test_bind_fd_dir(self): + self.assertIsDir('/tmp/d') + self.assertFileContent('/tmp/d/child', 'child content') + self.assertMountFlags('/tmp/d', + [os.ST_NOSUID, os.ST_NODEV], [os.ST_RDONLY]) + + # ------ Multiple binds ------ + + @in_sandbox('--ro-bind', lambda self: self.src.file, '/tmp/a', + '--ro-bind', lambda self: self.src.dir, '/tmp/b') + def test_multiple_binds(self): + self.assertFileContent('/tmp/a', 'hello') + self.assertIsDir('/tmp/b') + self.assertFileContent('/tmp/b/child', 'child content') + + @in_sandbox('--ro-bind', lambda self: self.src.file, '/tmp/parent/a', + '--ro-bind', lambda self: self.src.dir, '/tmp/parent/b') + def test_bind_file_then_dir_same_parent(self): + self.assertFileContent('/tmp/parent/a', 'hello') + self.assertIsDir('/tmp/parent/b') + + # ------ Recursive mount flags ------ + + @in_sandbox() + def test_ro_bind_root_is_recursive(self): + """The base --ro-bind / / should apply read-only to sub-mounts.""" + skip_prefixes = ('/dev', '/proc', '/sys', '/tmp') + for mountpoint in list_mounts(): + if any(mountpoint == p or mountpoint.startswith(p + '/') + for p in skip_prefixes): + continue + flags = os.statvfs(mountpoint).f_flag + self.assertTrue( + flags & os.ST_RDONLY, + f'{mountpoint} is not read-only', + ) + + # ------ tmpfs ------ + + @in_sandbox('--tmpfs', '/tmp/t') + def test_tmpfs(self): + self.assertIsDir('/tmp/t') + self.assertIsMountpoint('/tmp/t') + self.assertIsTmpfs('/tmp/t') + self.assertMountFlags('/tmp/t', + [os.ST_NOSUID, os.ST_NODEV], [os.ST_RDONLY]) + with open('/tmp/t/testfile', 'w') as f: + f.write('writable') + self.assertFileContent('/tmp/t/testfile', 'writable') + + @in_sandbox('--perms', '0700', '--tmpfs', '/tmp/t') + def test_tmpfs_perms(self): + self.assertIsMountpoint('/tmp/t') + self.assertIsTmpfs('/tmp/t') + mode = os.lstat('/tmp/t').st_mode & 0o7777 + self.assertEqual(mode, 0o700) + + @in_sandbox('--size', str(64 * 1024), '--tmpfs', '/tmp/t') + def test_tmpfs_size(self): + self.assertIsMountpoint('/tmp/t') + self.assertIsTmpfs('/tmp/t') + st = os.statvfs('/tmp/t') + total = st.f_blocks * st.f_frsize + self.assertLessEqual(total, 64 * 1024 + 4096) + + # ------ proc ------ + + @in_sandbox() + def test_proc(self): + self.assertIsSymlink('/proc/self') + self.assertIsDir('/proc/self/') + self.assertIsRegFile('/proc/self/status') + for subdir in ['sys', 'sysrq-trigger', 'irq', 'bus']: + path = f'/proc/{subdir}' + if os.path.exists(path): + self.assertFalse(os.access(path, os.W_OK), + f'/proc/{subdir} should not be writable') + + # ------ dev ------ + + @in_sandbox() + def test_dev_nodes(self): + for node in ['null', 'zero', 'full', 'random', 'urandom', 'tty']: + self.assertIsChrDev(f'/dev/{node}') + for link in ['stdin', 'stdout', 'stderr', 'fd', 'ptmx']: + self.assertIsSymlink(f'/dev/{link}') + self.assertIsDir('/dev/pts') + self.assertIsDir('/dev/shm') + + # ------ dir ------ + + @in_sandbox('--dir', '/tmp/d') + def test_dir(self): + self.assertIsDir('/tmp/d') + + @in_sandbox('--perms', '0700', '--dir', '/tmp/d') + def test_dir_perms(self): + self.assertIsDir('/tmp/d') + self.assertEqual(os.lstat('/tmp/d').st_mode & 0o7777, 0o700) + + @in_sandbox('--dir', '/tmp/a/b/c') + def test_dir_creates_parents(self): + self.assertIsDir('/tmp/a') + self.assertIsDir('/tmp/a/b') + self.assertIsDir('/tmp/a/b/c') + + # ------ file ------ + + @in_sandbox('--file', DataFd(b'file content'), '/tmp/f') + def test_file(self): + self.assertIsRegFile('/tmp/f') + self.assertFileContent('/tmp/f', 'file content') + + @in_sandbox('--perms', '0600', '--file', DataFd(b'x'), '/tmp/f') + def test_file_perms(self): + self.assertIsRegFile('/tmp/f') + self.assertEqual(os.lstat('/tmp/f').st_mode & 0o7777, 0o600) + + # ------ bind-data / ro-bind-data ------ + + @in_sandbox('--bind-data', DataFd(b'bind data'), '/tmp/f') + def test_bind_data(self): + self.assertIsRegFile('/tmp/f') + self.assertFileContent('/tmp/f', 'bind data') + self.assertIsMountpoint('/tmp/f') + self.assertMountFlags('/tmp/f', + [os.ST_NOSUID, os.ST_NODEV], [os.ST_RDONLY]) + + @in_sandbox('--ro-bind-data', DataFd(b'ro data'), '/tmp/f') + def test_ro_bind_data(self): + self.assertIsRegFile('/tmp/f') + self.assertFileContent('/tmp/f', 'ro data') + self.assertIsMountpoint('/tmp/f') + self.assertMountFlags('/tmp/f', + [os.ST_RDONLY, os.ST_NOSUID, os.ST_NODEV], []) + + # ------ symlink ------ + + @in_sandbox('--symlink', '/usr', '/tmp/link') + def test_symlink(self): + self.assertSymlinkContent('/tmp/link', '/usr') + + @in_sandbox('--symlink', '/usr', '/tmp/link', + '--symlink', '/usr', '/tmp/link') + def test_symlink_idempotent(self): + self.assertSymlinkContent('/tmp/link', '/usr') + + def test_symlink_conflict(self): + result = run_bwrap('--symlink', '/usr', '/tmp/link', + '--symlink', '/etc', '/tmp/link', + 'true') + self.assertBwrapFailed(result) + self.assertInStderr(b'existing destination is /usr', result) + + def test_symlink_over_file(self): + result = run_bwrap('--file', DataFd(b''), '/tmp/f', + '--symlink', '/usr', '/tmp/f', + 'true') + self.assertBwrapFailed(result) + self.assertInStderr(b'not a symlink', result) + + # ------ chmod ------ + + @in_sandbox('--dir', '/tmp/d', '--chmod', '0700', '/tmp/d') + def test_chmod(self): + self.assertEqual(os.lstat('/tmp/d').st_mode & 0o7777, 0o700) + + @in_sandbox('--dir', '/tmp/d', '--chmod', '0755', '/tmp/d') + def test_chmod_755(self): + self.assertEqual(os.lstat('/tmp/d').st_mode & 0o7777, 0o755) + + @in_sandbox('--file', DataFd(b'x'), '/tmp/f', '--chmod', '0600', '/tmp/f') + def test_chmod_file(self): + self.assertEqual(os.lstat('/tmp/f').st_mode & 0o7777, 0o600) + + def test_chmod_nonexistent(self): + result = run_bwrap('--chmod', '0700', '/tmp/noexist', 'true') + self.assertBwrapFailed(result) + self.assertInStderr(b"No such file or directory", result) + + # ------ remount-ro ------ + + @in_sandbox('--tmpfs', '/tmp/rw', '--remount-ro', '/tmp/rw') + def test_remount_ro(self): + self.assertMountFlags('/tmp/rw', [os.ST_RDONLY], []) + + # ------ mqueue ------ + + def test_mqueue(self): + result = run_bwrap('--mqueue', '/tmp/mq', 'true') + if result.returncode != 0 and b'Operation not permitted' in result.stderr: + self.skipTest('mqueue not available in user namespace') + self.assertEqual(result.returncode, 0) + + # ------ hostname ------ + + def test_hostname(self): + result = run_bwrap('--unshare-uts', '--hostname', 'testhost', + 'hostname') + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout.strip(), b'testhost') + + # ------ bind-try / ro-bind-try (nonexistent source skipped) ------ + + @in_sandbox('--ro-bind-try', '/nonexistent/path', '/tmp/f') + def test_ro_bind_try_nonexistent(self): + self.assertFalse(os.path.exists('/tmp/f')) + + @in_sandbox('--bind-try', '/nonexistent/path', '/tmp/f') + def test_bind_try_nonexistent(self): + self.assertFalse(os.path.exists('/tmp/f')) + + @in_sandbox('--dev-bind-try', '/nonexistent/path', '/tmp/f') + def test_dev_bind_try_nonexistent(self): + self.assertFalse(os.path.exists('/tmp/f')) + + @in_sandbox('--ro-bind-try', lambda self: self.src.file, '/tmp/f') + def test_ro_bind_try_existing(self): + self.assertFileContent('/tmp/f', 'hello') + + # ------ overlay ------ + + def test_tmp_overlay(self): + result = run_bwrap('--overlay-src', self.src.dir, + '--tmp-overlay', '/tmp/ov', + 'cat', '/tmp/ov/child') + if result.returncode != 0 and b'overlay' in result.stderr.lower(): + self.skipTest('overlayfs not available') + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, b'child content') + + def test_ro_overlay(self): + result = run_bwrap('--overlay-src', self.src.dir, + '--overlay-src', self.src.dir2, + '--ro-overlay', '/tmp/ov', + 'cat', '/tmp/ov/child') + if result.returncode != 0 and b'overlay' in result.stderr.lower(): + self.skipTest('overlayfs not available') + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, b'child content') + + # ------ Edge cases: source path with symlinks ------ + + def test_bind_source_absolute_symlink_in_path(self): + """Source path contains a symlink with absolute target.""" + link = os.path.join(self.tmp, 'abs_link') + os.symlink(self.src.root, link) + result = run_bwrap('--ro-bind', link + '/regular_file', '/tmp/f', + 'cat', '/tmp/f') + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, b'hello') + + def test_bind_source_dotdot_in_path(self): + """Source path with .. components.""" + path = self.src.dir + '/../regular_file' + result = run_bwrap('--ro-bind', path, '/tmp/f', + 'cat', '/tmp/f') + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, b'hello') + + def test_bind_source_symlink_chain(self): + """Source is a chain of symlinks.""" + link1 = os.path.join(self.tmp, 'link1') + link2 = os.path.join(self.tmp, 'link2') + os.symlink(self.src.file, link1) + os.symlink(link1, link2) + result = run_bwrap('--ro-bind', link2, '/tmp/f', + 'cat', '/tmp/f') + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, b'hello') + + # ------ Edge cases: dest parent issues ------ + + def test_bind_dest_parent_is_file(self): + """Bind where a parent component of dest is an existing file.""" + result = run_bwrap('--file', DataFd(b''), '/tmp/notadir', + '--ro-bind', self.src.file, '/tmp/notadir/child', + 'true') + self.assertBwrapFailed(result) + self.assertInStderr(b"Not a directory", result) + + @in_sandbox('--dir', '/tmp/target', + '--symlink', 'target', '/tmp/link', + '--ro-bind', lambda self: self.src.file, '/tmp/link/child') + def test_bind_dest_parent_is_symlink(self): + """Bind where a parent component of dest is a symlink to a dir.""" + self.assertFileContent('/tmp/link/child', 'hello') + + def test_dir_dest_parent_is_file(self): + """--dir where a parent component is an existing file.""" + result = run_bwrap('--file', DataFd(b''), '/tmp/notadir', + '--dir', '/tmp/notadir/sub', + 'true') + self.assertBwrapFailed(result) + self.assertInStderr(b"Not a directory", result) + + # ------ Edge cases: dest type mismatches ------ + + def test_bind_dir_over_existing_file(self): + """Bind a directory source over an existing file dest.""" + result = run_bwrap('--file', DataFd(b''), '/tmp/f', + '--ro-bind', self.src.dir, '/tmp/f', + 'true') + self.assertBwrapFailed(result) + self.assertInStderr(b'not a directory', result) + + def test_bind_file_over_existing_dir(self): + """Bind a file source over an existing directory dest.""" + result = run_bwrap('--dir', '/tmp/d', + '--ro-bind', self.src.file, '/tmp/d', + 'true') + self.assertBwrapFailed(result) + self.assertInStderr(b"not a file", result) + + # ------ Edge cases: nonexistent source ------ + + def test_bind_nonexistent_source(self): + result = run_bwrap('--ro-bind', '/nonexistent/path', '/tmp/f', + 'true') + self.assertBwrapFailed(result) + self.assertInStderr(b"No such file or directory", result) + + # ------ Edge cases: multiple overlapping mounts ------ + + @in_sandbox('--tmpfs', '/tmp/a', + '--dir', '/tmp/a/sub', + '--ro-bind', lambda self: self.src.file, '/tmp/a/sub/f') + def test_mount_chain(self): + """tmpfs, then dir inside it, then bind inside that.""" + self.assertIsDir('/tmp/a/sub') + self.assertFileContent('/tmp/a/sub/f', 'hello') + + # ------ Edge cases: bind-fd dest is symlink ------ + + def test_bind_fd_to_symlink_dest_fails(self): + fd = os.open(self.src.dir, os.O_PATH) + result = run_bwrap('--dir', '/tmp/dir', + '--symlink', 'dir', '/tmp/link', + '--bind-fd', str(fd), '/tmp/link', + 'true', pass_fds=(fd,)) + os.close(fd) + self.assertBwrapFailed(result) + self.assertInStderr(b"Can't mount on symlink destination", result) + + # ------ Edge cases: --file creates parents ------ + + @in_sandbox('--file', DataFd(b'deep'), '/tmp/a/b/c') + def test_file_creates_parents(self): + self.assertIsDir('/tmp/a/b') + self.assertFileContent('/tmp/a/b/c', 'deep') + + # ------ Edge cases: --symlink creates parents ------ + + @in_sandbox('--symlink', '/usr', '/tmp/a/b/link') + def test_symlink_creates_parents(self): + self.assertIsDir('/tmp/a/b') + self.assertSymlinkContent('/tmp/a/b/link', '/usr') + + # ------ Edge cases: --dir over existing dir is fine ------ + + @in_sandbox('--dir', '/tmp/d', '--dir', '/tmp/d') + def test_dir_idempotent(self): + self.assertIsDir('/tmp/d') + + # ------ Edge cases: bind-data creates parents ------ + + @in_sandbox('--ro-bind-data', DataFd(b'nested'), '/tmp/a/b/f') + def test_bind_data_creates_parents(self): + self.assertIsDir('/tmp/a/b') + self.assertFileContent('/tmp/a/b/f', 'nested') + + # ------ Symlink escape via /proc/self/fd ------ + + def _test_proc_symlink_escape(self, extra_bwrap_args): + """Verifies that a symlink pointing through /proc/self/fd + to an fd outside the sandbox cannot be used to escape the root.""" + escape_target = os.path.join(self.tmp, 'escape_target') + os.makedirs(escape_target) + + escape_fd = os.open(escape_target, os.O_PATH | os.O_DIRECTORY) + try: + result = run_bwrap( + *extra_bwrap_args, + '--dir', '/tmp/mnt', + '--symlink', f'/proc/self/fd/{escape_fd}', '/tmp/mnt/symlink', + '--dir', '/tmp/mnt/symlink/created', + 'true', + pass_fds=(escape_fd,)) + + self.assertBwrapFailed(result) + self.assertFalse(os.path.exists( + os.path.join(escape_target, 'created')), + 'directory was created outside sandbox via /proc/self/fd escape') + finally: + os.close(escape_fd) + + def test_proc_symlink_escape_blocked(self): + self._test_proc_symlink_escape([]) + + def test_proc_symlink_escape_blocked_fallback(self): + self._test_proc_symlink_escape(['--debug-opt=force-openat-fallback']) + + +if __name__ == '__main__': + run_tap_tests(sys.modules[__name__]) diff -Nru bubblewrap-0.11.0/tests/test-seccomp.py bubblewrap-0.12.0/tests/test-seccomp.py --- bubblewrap-0.11.0/tests/test-seccomp.py 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/tests/test-seccomp.py 2026-08-26 10:08:29.000000000 +0000 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # Copyright 2021 Simon McVittie -# SPDX-License-Identifier: LGPL-2.0-or-later +# SPDX-License-Identifier: LGPL-2.1-or-later import errno import logging diff -Nru bubblewrap-0.11.0/tests/test-utils.c bubblewrap-0.12.0/tests/test-utils.c --- bubblewrap-0.11.0/tests/test-utils.c 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/tests/test-utils.c 2026-08-26 10:08:29.000000000 +0000 @@ -1,12 +1,12 @@ /* * Copyright © 2019-2021 Collabora Ltd. * - * SPDX-License-Identifier: LGPL-2.0-or-later + * SPDX-License-Identifier: LGPL-2.1-or-later * * This program 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. + * 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 diff -Nru bubblewrap-0.11.0/tests/try-syscall.c bubblewrap-0.12.0/tests/try-syscall.c --- bubblewrap-0.11.0/tests/try-syscall.c 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/tests/try-syscall.c 2026-08-26 10:08:29.000000000 +0000 @@ -1,6 +1,6 @@ /* * Copyright 2021 Simon McVittie - * SPDX-License-Identifier: LGPL-2.0-or-later + * SPDX-License-Identifier: LGPL-2.1-or-later * * Try one or more system calls that might have been blocked by a * seccomp filter. Return the last value of errno seen. diff -Nru bubblewrap-0.11.0/tests/use-as-subproject/assert-correct-rpath.py bubblewrap-0.12.0/tests/use-as-subproject/assert-correct-rpath.py --- bubblewrap-0.11.0/tests/use-as-subproject/assert-correct-rpath.py 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/tests/use-as-subproject/assert-correct-rpath.py 2026-08-26 10:08:29.000000000 +0000 @@ -1,6 +1,6 @@ #!/usr/bin/python3 # Copyright 2022 Collabora Ltd. -# SPDX-License-Identifier: LGPL-2.0-or-later +# SPDX-License-Identifier: LGPL-2.1-or-later import subprocess import sys diff -Nru bubblewrap-0.11.0/utils.c bubblewrap-0.12.0/utils.c --- bubblewrap-0.11.0/utils.c 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/utils.c 2026-08-26 10:08:29.000000000 +0000 @@ -36,6 +36,7 @@ #endif bool bwrap_level_prefix = false; +int proc_fd = -1; __attribute__((format(printf, 2, 0))) static void bwrap_logv (int severity, @@ -137,7 +138,7 @@ if (pid == -1) die_with_error ("Can't fork for --pidns"); - /* Parent is an process not needed */ + /* The parent process is not needed */ if (pid != 0) exit (0); } @@ -190,6 +191,20 @@ return res; } +char * +xstrndup (const char *str, size_t n) +{ + char *res; + + assert (str != NULL); + + res = strndup (str, n); + if (res == NULL) + die_oom (); + + return res; +} + void strfreev (char **str_array) { @@ -369,8 +384,8 @@ } int -fdwalk (int proc_fd, int (*cb)(void *data, - int fd), void *data) +fdwalk (int (*cb)(void *data, + int fd), void *data) { int open_max; int fd; @@ -510,14 +525,18 @@ the create file will fail in the read-only case with EROFS instead of EEXIST. - We're trying to set up a mount point for a non-directory, so any - non-directory, non-symlink is acceptable - it doesn't necessarily - have to be a regular file. */ + We're trying to set up a mount point for a non-directory, for which + the kernel will accept any non-directory. If it's a symlink, follow + it and look at the target: again, any non-directory is good enough. + We'll only get S_ISLNK if the path is a dangling symlink (target + doesn't exist). */ if (stat (path, &buf) == 0 && !S_ISDIR (buf.st_mode) && !S_ISLNK (buf.st_mode)) return 0; + /* If the file didn't exist, create it. If it was a dangling symlink + * (S_ISLNK above) then this will create the target of the symlink. */ if (create_file (path, mode, NULL) != 0 && errno != EEXIST) return -1; @@ -662,11 +681,11 @@ /* Sets errno on error (< 0) */ int -get_file_mode (const char *pathname) +get_file_mode (int fd) { struct stat buf; - if (stat (pathname, &buf) != 0) + if (fstat (fd, &buf) != 0) return -1; return buf.st_mode & S_IFMT; @@ -681,7 +700,8 @@ /* We check this ahead of time, otherwise the mkdir call can fail in the read-only case with EROFS instead of EEXIST on some - filesystems (such as NFS) */ + filesystems (such as NFS). + We follow symlinks: it's OK if path is a symlink to a directory. */ if (stat (path, &buf) == 0) { if (!S_ISDIR (buf.st_mode)) @@ -864,14 +884,6 @@ return strconcat ("/oldroot/", path); } -char * -get_newroot_path (const char *path) -{ - while (*path == '/') - path++; - return strconcat ("/newroot/", path); -} - int raw_clone (unsigned long flags, void *child_stack) @@ -954,6 +966,12 @@ } } +char * +fd_to_proc_path (int fd) +{ + return xasprintf ("/proc/self/fd/%d", fd); +} + /* * Return a + b if it would not overflow. * Die with an "out of memory" error if it would. diff -Nru bubblewrap-0.11.0/utils.h bubblewrap-0.12.0/utils.h --- bubblewrap-0.11.0/utils.h 2024-10-30 16:07:47.000000000 +0000 +++ bubblewrap-0.12.0/utils.h 2026-08-26 10:08:29.000000000 +0000 @@ -41,6 +41,14 @@ #define UNUSED __attribute__((__unused__)) +#define BWRAP_KERNEL_VERSION(a, b, c) (((a) << 16) + ((b) << 8) + (c)) +#ifdef HAVE_ASSUMED_KERNEL +# define ASSUMED_KERNEL \ + BWRAP_KERNEL_VERSION (ASSUMED_KERNEL_MAJOR, ASSUMED_KERNEL_MINOR, ASSUMED_KERNEL_PATCH) +#else +# define ASSUMED_KERNEL 0 +#endif + #define N_ELEMENTS(arr) (sizeof (arr) / sizeof ((arr)[0])) #ifndef TEMP_FAILURE_RETRY @@ -60,6 +68,7 @@ #endif extern bool bwrap_level_prefix; +extern int proc_fd; void bwrap_log (int severity, const char *format, @@ -82,6 +91,7 @@ void *xrealloc (void *ptr, size_t size); char *xstrdup (const char *str); +char *xstrndup(const char *str, size_t n); void strfreev (char **str_array); void xclearenv (void); void xsetenv (const char *name, @@ -101,10 +111,9 @@ const char *prefix); bool path_equal (const char *path1, const char *path2); -int fdwalk (int proc_fd, - int (*cb)(void *data, - int fd), - void *data); +int fdwalk (int (*cb)(void *data, + int fd), + void *data); char *load_file_data (int fd, size_t *size); char *load_file_at (int dirfd, @@ -127,7 +136,7 @@ mode_t mode); int ensure_dir (const char *path, mode_t mode); -int get_file_mode (const char *pathname); +int get_file_mode (int fd); int mkdir_with_parents (const char *pathname, mode_t mode, bool create_last); @@ -135,8 +144,8 @@ void send_pid_on_socket (int socket); int read_pid_from_socket (int socket); char *get_oldroot_path (const char *path); -char *get_newroot_path (const char *path); char *readlink_malloc (const char *pathname); +char *fd_to_proc_path (int fd); /* syscall wrappers */ int raw_clone (unsigned long flags, @@ -148,6 +157,23 @@ int label_exec (const char *exec_label); int label_create_file (const char *file_label); +extern bool opt_force_openat_fallback; + +int safe_openat (int dirfd, + const char *rootfs, + const char *path, + int flags, + int mode); +char *chroot_realpath (const char *chroot, + const char *path, + char resolved_path[]); + +static inline bool +is_empty_string (const char *s) +{ + return s == NULL || s[0] == '\0'; +} + const char *mount_strerror (int errsv); static inline void @@ -183,6 +209,14 @@ #define cleanup_fd __attribute__((cleanup (cleanup_fdp))) #define cleanup_strv __attribute__((cleanup (cleanup_strvp))) +static inline int +steal_fd (int *fdp) +{ + int fd = *fdp; + *fdp = -1; + return fd; +} + static inline void * steal_pointer (void *pp) { @@ -199,6 +233,41 @@ #define steal_pointer(pp) \ (0 ? (*(pp)) : (steal_pointer) (pp)) +typedef struct { + int *fds; + size_t len; + size_t alloc; +} FdSet; + +static inline int +fdset_add (FdSet *set, int fd) +{ + if (set->len == set->alloc) + { + set->alloc = set->alloc ? set->alloc * 2 : 4; + set->fds = xrealloc (set->fds, set->alloc * sizeof (int)); + } + set->fds[set->len++] = fd; + return fd; +} + +static inline void +cleanup_fdsetp (FdSet *set) +{ + for (size_t i = 0; i < set->len; i++) + if (set->fds[i] >= 0) + close (set->fds[i]); + free (set->fds); +} + +#define cleanup_fdset __attribute__((cleanup (cleanup_fdsetp))) + +static inline char * +fdset_add_to_proc_path (FdSet *set, int fd) +{ + return fd_to_proc_path (fdset_add (set, fd)); +} + typedef struct _StringBuilder StringBuilder; struct _StringBuilder