├── .dir-locals.el ├── .editorconfig ├── .github └── workflows │ └── check.yml ├── CODE-OF-CONDUCT.md ├── COPYING ├── LICENSE ├── NEWS.md ├── README.md ├── SECURITY.md ├── bind-mount.c ├── bind-mount.h ├── bubblewrap.c ├── bubblewrap.jpg ├── bwrap.xml ├── ci └── builddeps.sh ├── completions ├── bash │ ├── bwrap │ └── meson.build ├── meson.build └── zsh │ ├── _bwrap │ └── meson.build ├── demos ├── bubblewrap-shell.sh ├── flatpak-run.sh ├── flatpak.bpf └── userns-block-fd.py ├── meson.build ├── meson_options.txt ├── network.c ├── network.h ├── packaging └── bubblewrap.spec ├── release-checklist.md ├── tests ├── libtest-core.sh ├── libtest.sh ├── meson.build ├── test-run.sh ├── test-seccomp.py ├── test-specifying-pidns.sh ├── test-specifying-userns.sh ├── test-utils.c ├── try-syscall.c └── use-as-subproject │ ├── .gitignore │ ├── README │ ├── assert-correct-rpath.py │ ├── config.h │ ├── dummy-config.h.in │ └── meson.build ├── uncrustify.cfg ├── uncrustify.sh ├── utils.c └── utils.h /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((c-mode . ((indent-tabs-mode . nil) (c-file-style . "gnu")))) 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.[ch]] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | indent_brace_style = gnu 6 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: CI checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | meson: 13 | name: Build with Meson and gcc, and test 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out 17 | uses: actions/checkout@v4 18 | - name: Install build-dependencies 19 | run: sudo ./ci/builddeps.sh 20 | - name: Create logs dir 21 | run: mkdir test-logs 22 | - name: setup 23 | run: | 24 | meson _build 25 | env: 26 | CFLAGS: >- 27 | -O2 28 | -Wp,-D_FORTIFY_SOURCE=2 29 | -fsanitize=address 30 | -fsanitize=undefined 31 | - name: compile 32 | run: ninja -C _build -v 33 | - name: smoke-test 34 | run: | 35 | set -x 36 | ./_build/bwrap --bind / / --tmpfs /tmp true 37 | env: 38 | ASAN_OPTIONS: detect_leaks=0 39 | - name: test 40 | run: | 41 | BWRAP_MUST_WORK=1 meson test -C _build 42 | env: 43 | ASAN_OPTIONS: detect_leaks=0 44 | - name: Collect overall test logs on failure 45 | if: failure() 46 | run: mv _build/meson-logs/testlog.txt test-logs/ || true 47 | - name: install 48 | run: | 49 | DESTDIR="$(pwd)/DESTDIR" meson install -C _build 50 | ( cd DESTDIR && find -ls ) 51 | - name: dist 52 | run: | 53 | BWRAP_MUST_WORK=1 meson dist -C _build 54 | - name: Collect dist test logs on failure 55 | if: failure() 56 | run: mv _build/meson-private/dist-build/meson-logs/testlog.txt test-logs/disttestlog.txt || true 57 | - name: use as subproject 58 | run: | 59 | mkdir tests/use-as-subproject/subprojects 60 | tar -C tests/use-as-subproject/subprojects -xf _build/meson-dist/bubblewrap-*.tar.xz 61 | mv tests/use-as-subproject/subprojects/bubblewrap-* tests/use-as-subproject/subprojects/bubblewrap 62 | ( cd tests/use-as-subproject && meson _build ) 63 | ninja -C tests/use-as-subproject/_build -v 64 | meson test -C tests/use-as-subproject/_build 65 | DESTDIR="$(pwd)/DESTDIR-as-subproject" meson install -C tests/use-as-subproject/_build 66 | ( cd DESTDIR-as-subproject && find -ls ) 67 | test -x DESTDIR-as-subproject/usr/local/libexec/not-flatpak-bwrap 68 | test ! -e DESTDIR-as-subproject/usr/local/bin/bwrap 69 | test ! -e DESTDIR-as-subproject/usr/local/libexec/bwrap 70 | tests/use-as-subproject/assert-correct-rpath.py DESTDIR-as-subproject/usr/local/libexec/not-flatpak-bwrap 71 | - name: Upload test logs 72 | uses: actions/upload-artifact@v4 73 | if: failure() || cancelled() 74 | with: 75 | name: test logs 76 | path: test-logs 77 | 78 | clang: 79 | name: Build with clang and analyze 80 | runs-on: ubuntu-latest 81 | strategy: 82 | fail-fast: false 83 | matrix: 84 | language: 85 | - cpp 86 | steps: 87 | - name: Initialize CodeQL 88 | uses: github/codeql-action/init@v2 89 | with: 90 | languages: ${{ matrix.language }} 91 | - name: Check out 92 | uses: actions/checkout@v4 93 | - name: Install build-dependencies 94 | run: sudo ./ci/builddeps.sh --clang 95 | - run: meson build -Dselinux=enabled 96 | env: 97 | CC: clang 98 | CFLAGS: >- 99 | -O2 100 | -Werror=unused-variable 101 | - run: meson compile -C build 102 | - name: CodeQL analysis 103 | uses: github/codeql-action/analyze@v2 104 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## The bubblewrap Project Community Code of Conduct 2 | 3 | The bubblewrap project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/HEAD/CODE-OF-CONDUCT.md). 4 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LIBRARY GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1991 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the library GPL. It is 10 | numbered 2 because it goes with version 2 of the ordinary GPL.] 11 | 12 | Preamble 13 | 14 | The licenses for most software are designed to take away your 15 | freedom to share and change it. By contrast, the GNU General Public 16 | Licenses are intended to guarantee your freedom to share and change 17 | free software--to make sure the software is free for all its users. 18 | 19 | This license, the Library General Public License, applies to some 20 | specially designated Free Software Foundation software, and to any 21 | other libraries whose authors decide to use it. You can use it for 22 | your libraries, too. 23 | 24 | When we speak of free software, we are referring to freedom, not 25 | price. Our General Public Licenses are designed to make sure that you 26 | have the freedom to distribute copies of free software (and charge for 27 | this service if you wish), that you receive source code or can get it 28 | if you want it, that you can change the software or use pieces of it 29 | in new free programs; and that you know you can do these things. 30 | 31 | To protect your rights, we need to make restrictions that forbid 32 | anyone to deny you these rights or to ask you to surrender the rights. 33 | These restrictions translate to certain responsibilities for you if 34 | you distribute copies of the library, or if you modify it. 35 | 36 | For example, if you distribute copies of the library, whether gratis 37 | or for a fee, you must give the recipients all the rights that we gave 38 | you. You must make sure that they, too, receive or can get the source 39 | code. If you link a program with the library, you must provide 40 | complete object files to the recipients so that they can relink them 41 | with the library, after making changes to the library and recompiling 42 | it. And you must show them these terms so they know their rights. 43 | 44 | Our method of protecting your rights has two steps: (1) copyright 45 | the library, and (2) offer you this license which gives you legal 46 | permission to copy, distribute and/or modify the library. 47 | 48 | Also, for each distributor's protection, we want to make certain 49 | that everyone understands that there is no warranty for this free 50 | library. If the library is modified by someone else and passed on, we 51 | want its recipients to know that what they have is not the original 52 | version, so that any problems introduced by others will not reflect on 53 | the original authors' reputations. 54 | 55 | Finally, any free program is threatened constantly by software 56 | patents. We wish to avoid the danger that companies distributing free 57 | software will individually obtain patent licenses, thus in effect 58 | transforming the program into proprietary software. To prevent this, 59 | we have made it clear that any patent must be licensed for everyone's 60 | free use or not licensed at all. 61 | 62 | Most GNU software, including some libraries, is covered by the ordinary 63 | GNU General Public License, which was designed for utility programs. This 64 | license, the GNU Library General Public License, applies to certain 65 | designated libraries. This license is quite different from the ordinary 66 | one; be sure to read it in full, and don't assume that anything in it is 67 | the same as in the ordinary license. 68 | 69 | The reason we have a separate public license for some libraries is that 70 | they blur the distinction we usually make between modifying or adding to a 71 | program and simply using it. Linking a program with a library, without 72 | changing the library, is in some sense simply using the library, and is 73 | analogous to running a utility program or application program. However, in 74 | a textual and legal sense, the linked executable is a combined work, a 75 | derivative of the original library, and the ordinary General Public License 76 | treats it as such. 77 | 78 | Because of this blurred distinction, using the ordinary General 79 | Public License for libraries did not effectively promote software 80 | sharing, because most developers did not use the libraries. We 81 | concluded that weaker conditions might promote sharing better. 82 | 83 | However, unrestricted linking of non-free programs would deprive the 84 | users of those programs of all benefit from the free status of the 85 | libraries themselves. This Library General Public License is intended to 86 | permit developers of non-free programs to use free libraries, while 87 | preserving your freedom as a user of such programs to change the free 88 | libraries that are incorporated in them. (We have not seen how to achieve 89 | this as regards changes in header files, but we have achieved it as regards 90 | changes in the actual functions of the Library.) The hope is that this 91 | will lead to faster development of free libraries. 92 | 93 | The precise terms and conditions for copying, distribution and 94 | modification follow. Pay close attention to the difference between a 95 | "work based on the library" and a "work that uses the library". The 96 | former contains code derived from the library, while the latter only 97 | works together with the library. 98 | 99 | Note that it is possible for a library to be covered by the ordinary 100 | General Public License rather than by this special one. 101 | 102 | GNU LIBRARY GENERAL PUBLIC LICENSE 103 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 104 | 105 | 0. This License Agreement applies to any software library which 106 | contains a notice placed by the copyright holder or other authorized 107 | party saying it may be distributed under the terms of this Library 108 | General Public License (also called "this License"). Each licensee is 109 | addressed as "you". 110 | 111 | A "library" means a collection of software functions and/or data 112 | prepared so as to be conveniently linked with application programs 113 | (which use some of those functions and data) to form executables. 114 | 115 | The "Library", below, refers to any such software library or work 116 | which has been distributed under these terms. A "work based on the 117 | Library" means either the Library or any derivative work under 118 | copyright law: that is to say, a work containing the Library or a 119 | portion of it, either verbatim or with modifications and/or translated 120 | straightforwardly into another language. (Hereinafter, translation is 121 | included without limitation in the term "modification".) 122 | 123 | "Source code" for a work means the preferred form of the work for 124 | making modifications to it. For a library, complete source code means 125 | all the source code for all modules it contains, plus any associated 126 | interface definition files, plus the scripts used to control compilation 127 | and installation of the library. 128 | 129 | Activities other than copying, distribution and modification are not 130 | covered by this License; they are outside its scope. The act of 131 | running a program using the Library is not restricted, and output from 132 | such a program is covered only if its contents constitute a work based 133 | on the Library (independent of the use of the Library in a tool for 134 | writing it). Whether that is true depends on what the Library does 135 | and what the program that uses the Library does. 136 | 137 | 1. You may copy and distribute verbatim copies of the Library's 138 | complete source code as you receive it, in any medium, provided that 139 | you conspicuously and appropriately publish on each copy an 140 | appropriate copyright notice and disclaimer of warranty; keep intact 141 | all the notices that refer to this License and to the absence of any 142 | warranty; and distribute a copy of this License along with the 143 | Library. 144 | 145 | You may charge a fee for the physical act of transferring a copy, 146 | and you may at your option offer warranty protection in exchange for a 147 | fee. 148 | 149 | 2. You may modify your copy or copies of the Library or any portion 150 | of it, thus forming a work based on the Library, and copy and 151 | distribute such modifications or work under the terms of Section 1 152 | above, provided that you also meet all of these conditions: 153 | 154 | a) The modified work must itself be a software library. 155 | 156 | b) You must cause the files modified to carry prominent notices 157 | stating that you changed the files and the date of any change. 158 | 159 | c) You must cause the whole of the work to be licensed at no 160 | charge to all third parties under the terms of this License. 161 | 162 | d) If a facility in the modified Library refers to a function or a 163 | table of data to be supplied by an application program that uses 164 | the facility, other than as an argument passed when the facility 165 | is invoked, then you must make a good faith effort to ensure that, 166 | in the event an application does not supply such function or 167 | table, the facility still operates, and performs whatever part of 168 | its purpose remains meaningful. 169 | 170 | (For example, a function in a library to compute square roots has 171 | a purpose that is entirely well-defined independent of the 172 | application. Therefore, Subsection 2d requires that any 173 | application-supplied function or table used by this function must 174 | be optional: if the application does not supply it, the square 175 | root function must still compute square roots.) 176 | 177 | These requirements apply to the modified work as a whole. If 178 | identifiable sections of that work are not derived from the Library, 179 | and can be reasonably considered independent and separate works in 180 | themselves, then this License, and its terms, do not apply to those 181 | sections when you distribute them as separate works. But when you 182 | distribute the same sections as part of a whole which is a work based 183 | on the Library, the distribution of the whole must be on the terms of 184 | this License, whose permissions for other licensees extend to the 185 | entire whole, and thus to each and every part regardless of who wrote 186 | it. 187 | 188 | Thus, it is not the intent of this section to claim rights or contest 189 | your rights to work written entirely by you; rather, the intent is to 190 | exercise the right to control the distribution of derivative or 191 | collective works based on the Library. 192 | 193 | In addition, mere aggregation of another work not based on the Library 194 | with the Library (or with a work based on the Library) on a volume of 195 | a storage or distribution medium does not bring the other work under 196 | the scope of this License. 197 | 198 | 3. You may opt to apply the terms of the ordinary GNU General Public 199 | License instead of this License to a given copy of the Library. To do 200 | this, you must alter all the notices that refer to this License, so 201 | that they refer to the ordinary GNU General Public License, version 2, 202 | instead of to this License. (If a newer version than version 2 of the 203 | ordinary GNU General Public License has appeared, then you can specify 204 | that version instead if you wish.) Do not make any other change in 205 | these notices. 206 | 207 | Once this change is made in a given copy, it is irreversible for 208 | that copy, so the ordinary GNU General Public License applies to all 209 | subsequent copies and derivative works made from that copy. 210 | 211 | This option is useful when you wish to copy part of the code of 212 | the Library into a program that is not a library. 213 | 214 | 4. You may copy and distribute the Library (or a portion or 215 | derivative of it, under Section 2) in object code or executable form 216 | under the terms of Sections 1 and 2 above provided that you accompany 217 | it with the complete corresponding machine-readable source code, which 218 | must be distributed under the terms of Sections 1 and 2 above on a 219 | medium customarily used for software interchange. 220 | 221 | If distribution of object code is made by offering access to copy 222 | from a designated place, then offering equivalent access to copy the 223 | source code from the same place satisfies the requirement to 224 | distribute the source code, even though third parties are not 225 | compelled to copy the source along with the object code. 226 | 227 | 5. A program that contains no derivative of any portion of the 228 | Library, but is designed to work with the Library by being compiled or 229 | linked with it, is called a "work that uses the Library". Such a 230 | work, in isolation, is not a derivative work of the Library, and 231 | therefore falls outside the scope of this License. 232 | 233 | However, linking a "work that uses the Library" with the Library 234 | creates an executable that is a derivative of the Library (because it 235 | contains portions of the Library), rather than a "work that uses the 236 | library". The executable is therefore covered by this License. 237 | Section 6 states terms for distribution of such executables. 238 | 239 | When a "work that uses the Library" uses material from a header file 240 | that is part of the Library, the object code for the work may be a 241 | derivative work of the Library even though the source code is not. 242 | Whether this is true is especially significant if the work can be 243 | linked without the Library, or if the work is itself a library. The 244 | threshold for this to be true is not precisely defined by law. 245 | 246 | If such an object file uses only numerical parameters, data 247 | structure layouts and accessors, and small macros and small inline 248 | functions (ten lines or less in length), then the use of the object 249 | file is unrestricted, regardless of whether it is legally a derivative 250 | work. (Executables containing this object code plus portions of the 251 | Library will still fall under Section 6.) 252 | 253 | Otherwise, if the work is a derivative of the Library, you may 254 | distribute the object code for the work under the terms of Section 6. 255 | Any executables containing that work also fall under Section 6, 256 | whether or not they are linked directly with the Library itself. 257 | 258 | 6. As an exception to the Sections above, you may also compile or 259 | link a "work that uses the Library" with the Library to produce a 260 | work containing portions of the Library, and distribute that work 261 | under terms of your choice, provided that the terms permit 262 | modification of the work for the customer's own use and reverse 263 | engineering for debugging such modifications. 264 | 265 | You must give prominent notice with each copy of the work that the 266 | Library is used in it and that the Library and its use are covered by 267 | this License. You must supply a copy of this License. If the work 268 | during execution displays copyright notices, you must include the 269 | copyright notice for the Library among them, as well as a reference 270 | directing the user to the copy of this License. Also, you must do one 271 | of these things: 272 | 273 | a) Accompany the work with the complete corresponding 274 | machine-readable source code for the Library including whatever 275 | changes were used in the work (which must be distributed under 276 | Sections 1 and 2 above); and, if the work is an executable linked 277 | with the Library, with the complete machine-readable "work that 278 | uses the Library", as object code and/or source code, so that the 279 | user can modify the Library and then relink to produce a modified 280 | executable containing the modified Library. (It is understood 281 | that the user who changes the contents of definitions files in the 282 | Library will not necessarily be able to recompile the application 283 | to use the modified definitions.) 284 | 285 | b) Accompany the work with a written offer, valid for at 286 | least three years, to give the same user the materials 287 | specified in Subsection 6a, above, for a charge no more 288 | than the cost of performing this distribution. 289 | 290 | c) If distribution of the work is made by offering access to copy 291 | from a designated place, offer equivalent access to copy the above 292 | specified materials from the same place. 293 | 294 | d) Verify that the user has already received a copy of these 295 | materials or that you have already sent this user a copy. 296 | 297 | For an executable, the required form of the "work that uses the 298 | Library" must include any data and utility programs needed for 299 | reproducing the executable from it. However, as a special exception, 300 | the source code distributed need not include anything that is normally 301 | distributed (in either source or binary form) with the major 302 | components (compiler, kernel, and so on) of the operating system on 303 | which the executable runs, unless that component itself accompanies 304 | the executable. 305 | 306 | It may happen that this requirement contradicts the license 307 | restrictions of other proprietary libraries that do not normally 308 | accompany the operating system. Such a contradiction means you cannot 309 | use both them and the Library together in an executable that you 310 | distribute. 311 | 312 | 7. You may place library facilities that are a work based on the 313 | Library side-by-side in a single library together with other library 314 | facilities not covered by this License, and distribute such a combined 315 | library, provided that the separate distribution of the work based on 316 | the Library and of the other library facilities is otherwise 317 | permitted, and provided that you do these two things: 318 | 319 | a) Accompany the combined library with a copy of the same work 320 | based on the Library, uncombined with any other library 321 | facilities. This must be distributed under the terms of the 322 | Sections above. 323 | 324 | b) Give prominent notice with the combined library of the fact 325 | that part of it is a work based on the Library, and explaining 326 | where to find the accompanying uncombined form of the same work. 327 | 328 | 8. You may not copy, modify, sublicense, link with, or distribute 329 | the Library except as expressly provided under this License. Any 330 | attempt otherwise to copy, modify, sublicense, link with, or 331 | distribute the Library is void, and will automatically terminate your 332 | rights under this License. However, parties who have received copies, 333 | or rights, from you under this License will not have their licenses 334 | terminated so long as such parties remain in full compliance. 335 | 336 | 9. You are not required to accept this License, since you have not 337 | signed it. However, nothing else grants you permission to modify or 338 | distribute the Library or its derivative works. These actions are 339 | prohibited by law if you do not accept this License. Therefore, by 340 | modifying or distributing the Library (or any work based on the 341 | Library), you indicate your acceptance of this License to do so, and 342 | all its terms and conditions for copying, distributing or modifying 343 | the Library or works based on it. 344 | 345 | 10. Each time you redistribute the Library (or any work based on the 346 | Library), the recipient automatically receives a license from the 347 | original licensor to copy, distribute, link with or modify the Library 348 | subject to these terms and conditions. You may not impose any further 349 | restrictions on the recipients' exercise of the rights granted herein. 350 | You are not responsible for enforcing compliance by third parties to 351 | this License. 352 | 353 | 11. If, as a consequence of a court judgment or allegation of patent 354 | infringement or for any other reason (not limited to patent issues), 355 | conditions are imposed on you (whether by court order, agreement or 356 | otherwise) that contradict the conditions of this License, they do not 357 | excuse you from the conditions of this License. If you cannot 358 | distribute so as to satisfy simultaneously your obligations under this 359 | License and any other pertinent obligations, then as a consequence you 360 | may not distribute the Library at all. For example, if a patent 361 | license would not permit royalty-free redistribution of the Library by 362 | all those who receive copies directly or indirectly through you, then 363 | the only way you could satisfy both it and this License would be to 364 | refrain entirely from distribution of the Library. 365 | 366 | If any portion of this section is held invalid or unenforceable under any 367 | particular circumstance, the balance of the section is intended to apply, 368 | and the section as a whole is intended to apply in other circumstances. 369 | 370 | It is not the purpose of this section to induce you to infringe any 371 | patents or other property right claims or to contest validity of any 372 | such claims; this section has the sole purpose of protecting the 373 | integrity of the free software distribution system which is 374 | implemented by public license practices. Many people have made 375 | generous contributions to the wide range of software distributed 376 | through that system in reliance on consistent application of that 377 | system; it is up to the author/donor to decide if he or she is willing 378 | to distribute software through any other system and a licensee cannot 379 | impose that choice. 380 | 381 | This section is intended to make thoroughly clear what is believed to 382 | be a consequence of the rest of this License. 383 | 384 | 12. If the distribution and/or use of the Library is restricted in 385 | certain countries either by patents or by copyrighted interfaces, the 386 | original copyright holder who places the Library under this License may add 387 | an explicit geographical distribution limitation excluding those countries, 388 | so that distribution is permitted only in or among countries not thus 389 | excluded. In such case, this License incorporates the limitation as if 390 | written in the body of this License. 391 | 392 | 13. The Free Software Foundation may publish revised and/or new 393 | versions of the Library General Public License from time to time. 394 | Such new versions will be similar in spirit to the present version, 395 | but may differ in detail to address new problems or concerns. 396 | 397 | Each version is given a distinguishing version number. If the Library 398 | specifies a version number of this License which applies to it and 399 | "any later version", you have the option of following the terms and 400 | conditions either of that version or of any later version published by 401 | the Free Software Foundation. If the Library does not specify a 402 | license version number, you may choose any version ever published by 403 | the Free Software Foundation. 404 | 405 | 14. If you wish to incorporate parts of the Library into other free 406 | programs whose distribution conditions are incompatible with these, 407 | write to the author to ask for permission. For software which is 408 | copyrighted by the Free Software Foundation, write to the Free 409 | Software Foundation; we sometimes make exceptions for this. Our 410 | decision will be guided by the two goals of preserving the free status 411 | of all derivatives of our free software and of promoting the sharing 412 | and reuse of software generally. 413 | 414 | NO WARRANTY 415 | 416 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 417 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 418 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 419 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 420 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 421 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 422 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 423 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 424 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 425 | 426 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 427 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 428 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 429 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 430 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 431 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 432 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 433 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 434 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 435 | DAMAGES. 436 | 437 | END OF TERMS AND CONDITIONS 438 | 439 | How to Apply These Terms to Your New Libraries 440 | 441 | If you develop a new library, and you want it to be of the greatest 442 | possible use to the public, we recommend making it free software that 443 | everyone can redistribute and change. You can do so by permitting 444 | redistribution under these terms (or, alternatively, under the terms of the 445 | ordinary General Public License). 446 | 447 | To apply these terms, attach the following notices to the library. It is 448 | safest to attach them to the start of each source file to most effectively 449 | convey the exclusion of warranty; and each file should have at least the 450 | "copyright" line and a pointer to where the full notice is found. 451 | 452 | 453 | Copyright (C) 454 | 455 | This library is free software; you can redistribute it and/or 456 | modify it under the terms of the GNU Library General Public 457 | License as published by the Free Software Foundation; either 458 | version 2 of the License, or (at your option) any later version. 459 | 460 | This library is distributed in the hope that it will be useful, 461 | but WITHOUT ANY WARRANTY; without even the implied warranty of 462 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 463 | Library General Public License for more details. 464 | 465 | You should have received a copy of the GNU Library General Public 466 | License along with this library; if not, write to the Free Software 467 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 468 | 469 | Also add information on how to contact you by electronic and paper mail. 470 | 471 | You should also get your employer (if you work as a programmer) or your 472 | school, if any, to sign a "copyright disclaimer" for the library, if 473 | necessary. Here is a sample; alter the names: 474 | 475 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 476 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 477 | 478 | , 1 April 1990 479 | Ty Coon, President of Vice 480 | 481 | That's all there is to it! 482 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | COPYING -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | bubblewrap 0.11.0 2 | ================= 3 | 4 | Released: 2024-10-30 5 | 6 | Dependencies: 7 | 8 | * Remove the Autotools build system. Meson ≥ 0.49.0 is now required 9 | at build-time. (#625, Hugo Osvaldo Barrera) 10 | 11 | * For users of bash-completion, bash-completion ≥ 2.10 is recommended. 12 | With older bash-completion, bubblewrap might install completions 13 | outside its `${prefix}` unless overridden with `-Dbash_completion_dir=…`. 14 | 15 | Enhancements: 16 | 17 | * New `--overlay`, `--tmp-overlay`, `--ro-overlay` and `--overlay-src` 18 | options allow creation of overlay mounts. 19 | This feature is not available when bubblewrap is installed setuid. 20 | (#412, #663; Ryan Hendrickson, William Manley, Simon McVittie) 21 | 22 | * New `--level-prefix` option produces output that can be parsed by 23 | tools like `logger --prio-prefix` and `systemd-cat --level-prefix=1` 24 | (#646, Simon McVittie) 25 | 26 | Bug fixes: 27 | 28 | * Handle `EINTR` when doing I/O on files or sockets (#657, Simon McVittie) 29 | 30 | * Don't make assumptions about alignment of socket control message data 31 | (#637, Simon McVittie) 32 | 33 | * Silence some Meson deprecation warnings (#647, @Sertonix) 34 | 35 | * Update URLs in documentation to https (#566, @TotalCaesar659) 36 | 37 | * Improve tests' compatibility with busybox (#627, @Sertonix) 38 | 39 | * Improve compatibility with Meson < 1.3.0 (#664, Simon McVittie) 40 | 41 | Internal changes: 42 | 43 | * Consistently use `` for booleans (#660, Simon McVittie) 44 | 45 | * Avoid `-Wshadow` compiler warnings (#661, Simon McVittie) 46 | 47 | * Update Github Actions configuration (#658, Simon McVittie) 48 | 49 | ---- 50 | 51 | See also 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bubblewrap 2 | ========== 3 | 4 | Many container runtime tools like `systemd-nspawn`, `docker`, 5 | etc. focus on providing infrastructure for system administrators and 6 | orchestration tools (e.g. Kubernetes) to run containers. 7 | 8 | These tools are not suitable to give to unprivileged users, because it 9 | is trivial to turn such access into a fully privileged root shell 10 | on the host. 11 | 12 | User namespaces 13 | --------------- 14 | 15 | There is an effort in the Linux kernel called 16 | [user namespaces](https://www.google.com/search?q=user+namespaces+site%3Ahttps%3A%2F%2Flwn.net) 17 | which attempts to allow unprivileged users to use container features. 18 | While significant progress has been made, there are 19 | [still concerns](https://lwn.net/Articles/673597/) about it, and 20 | it is not available to unprivileged users in several production distributions 21 | such as CentOS/Red Hat Enterprise Linux 7, Debian Jessie, etc. 22 | 23 | See for example 24 | [CVE-2016-3135](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-3135) 25 | which is a local root vulnerability introduced by userns. 26 | [This March 2016 post](https://lkml.org/lkml/2016/3/9/555) has some 27 | more discussion. 28 | 29 | Bubblewrap could be viewed as setuid implementation of a *subset* of 30 | user namespaces. Emphasis on subset - specifically relevant to the 31 | above CVE, bubblewrap does not allow control over iptables. 32 | 33 | The original bubblewrap code existed before user namespaces - it inherits code from 34 | [xdg-app helper](https://cgit.freedesktop.org/xdg-app/xdg-app/tree/common/xdg-app-helper.c?id=4c3bf179e2e4a2a298cd1db1d045adaf3f564532) 35 | which in turn distantly derives from 36 | [linux-user-chroot](https://git.gnome.org/browse/linux-user-chroot). 37 | 38 | System security 39 | --------------- 40 | 41 | The maintainers of this tool believe that it does not, even when used 42 | in combination with typical software installed on that distribution, 43 | allow privilege escalation. It may increase the ability of a logged 44 | in user to perform denial of service attacks, however. 45 | 46 | In particular, bubblewrap uses `PR_SET_NO_NEW_PRIVS` to turn off 47 | setuid binaries, which is the [traditional way](https://en.wikipedia.org/wiki/Chroot#Limitations) to get out of things 48 | like chroots. 49 | 50 | Sandbox security 51 | ---------------- 52 | 53 | bubblewrap is a tool for constructing sandbox environments. 54 | bubblewrap is not a complete, ready-made sandbox with a specific security 55 | policy. 56 | 57 | Some of bubblewrap's use-cases want a security boundary between the sandbox 58 | and the real system; other use-cases want the ability to change the layout of 59 | the filesystem for processes inside the sandbox, but do not aim to be a 60 | security boundary. 61 | As a result, the level of protection between the sandboxed processes and 62 | the host system is entirely determined by the arguments passed to 63 | bubblewrap. 64 | 65 | Whatever program constructs the command-line arguments for bubblewrap 66 | (often a larger framework like Flatpak, libgnome-desktop, sandwine 67 | or an ad-hoc script) is responsible for defining its own security model, 68 | and choosing appropriate bubblewrap command-line arguments to implement 69 | that security model. 70 | 71 | Some aspects of sandbox security that require particular care are described 72 | in the [Limitations](#limitations) section below. 73 | 74 | Users 75 | ----- 76 | 77 | This program can be shared by all container tools which perform 78 | non-root operation, such as: 79 | 80 | - [Flatpak](https://www.flatpak.org) 81 | - [rpm-ostree unprivileged](https://github.com/projectatomic/rpm-ostree/pull/209) 82 | - [bwrap-oci](https://github.com/projectatomic/bwrap-oci) 83 | 84 | We would also like to see this be available in Kubernetes/OpenShift 85 | clusters. Having the ability for unprivileged users to use container 86 | features would make it significantly easier to do interactive 87 | debugging scenarios and the like. 88 | 89 | Installation 90 | ------------ 91 | 92 | bubblewrap is available in the package repositories of the most Linux distributions 93 | and can be installed from there. 94 | 95 | If you need to build bubblewrap from source, you can do this with meson: 96 | 97 | ```sh 98 | meson _builddir 99 | meson compile -C _builddir 100 | meson test -C _builddir 101 | meson install -C _builddir 102 | ``` 103 | 104 | Usage 105 | ----- 106 | 107 | bubblewrap works by creating a new, completely empty, mount 108 | namespace where the root is on a tmpfs that is invisible from the 109 | host, and will be automatically cleaned up when the last process 110 | exits. You can then use commandline options to construct the root 111 | filesystem and process environment and command to run in the 112 | namespace. 113 | 114 | There's a larger [demo script](./demos/bubblewrap-shell.sh) in the 115 | source code, but here's a trimmed down version which runs 116 | a new shell reusing the host's `/usr`. 117 | 118 | ``` 119 | bwrap \ 120 | --ro-bind /usr /usr \ 121 | --symlink usr/lib64 /lib64 \ 122 | --proc /proc \ 123 | --dev /dev \ 124 | --unshare-pid \ 125 | --new-session \ 126 | bash 127 | ``` 128 | 129 | This is an incomplete example, but useful for purposes of 130 | illustration. More often, rather than creating a container using the 131 | host's filesystem tree, you want to target a chroot. There, rather 132 | than creating the symlink `lib64 -> usr/lib64` in the tmpfs, you might 133 | have already created it in the target rootfs. 134 | 135 | Sandboxing 136 | ---------- 137 | 138 | The goal of bubblewrap is to run an application in a sandbox, where it 139 | has restricted access to parts of the operating system or user data 140 | such as the home directory. 141 | 142 | bubblewrap always creates a new mount namespace, and the user can specify 143 | exactly what parts of the filesystem should be visible in the sandbox. 144 | Any such directories you specify mounted `nodev` by default, and can be made readonly. 145 | 146 | Additionally you can use these kernel features: 147 | 148 | User namespaces ([CLONE_NEWUSER](https://linux.die.net/man/2/clone)): This hides all but the current uid and gid from the 149 | sandbox. You can also change what the value of uid/gid should be in the sandbox. 150 | 151 | IPC namespaces ([CLONE_NEWIPC](https://linux.die.net/man/2/clone)): The sandbox will get its own copy of all the 152 | different forms of IPCs, like SysV shared memory and semaphores. 153 | 154 | 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/). 155 | 156 | 157 | 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. 158 | 159 | UTS namespace ([CLONE_NEWUTS](https://linux.die.net/man/2/clone)): The sandbox will have its own hostname. 160 | 161 | Seccomp filters: You can pass in seccomp filters that limit which syscalls can be done in the sandbox. For more information, see [Seccomp](https://en.wikipedia.org/wiki/Seccomp). 162 | 163 | Limitations 164 | ----------- 165 | 166 | As noted in the [Sandbox security](#sandbox-security) section above, 167 | the level of protection between the sandboxed processes and the host system 168 | is entirely determined by the arguments passed to bubblewrap. 169 | Some aspects that require special care are noted here. 170 | 171 | - If you are not filtering out `TIOCSTI` commands using seccomp filters, 172 | argument `--new-session` is needed to protect against out-of-sandbox 173 | command execution 174 | (see [CVE-2017-5226](https://github.com/containers/bubblewrap/issues/142)). 175 | 176 | - Everything mounted into the sandbox can potentially be used to escalate 177 | privileges. 178 | For example, if you bind a D-Bus socket into the sandbox, it can be used to 179 | execute commands via systemd. You can use 180 | [xdg-dbus-proxy](https://github.com/flatpak/xdg-dbus-proxy) to filter 181 | D-Bus communication. 182 | 183 | - Some applications deploy their own sandboxing mechanisms, and these can be 184 | restricted by the constraints imposed by bubblewrap's sandboxing. 185 | For example, some web browsers which configure their child proccesses via 186 | seccomp to not have access to the filesystem. If you limit the syscalls and 187 | don't allow the seccomp syscall, a browser cannot apply these restrictions. 188 | Similarly, if these rules were compiled into a file that is not available in 189 | the sandbox, the browser cannot load these rules from this file and cannot 190 | apply these restrictions. 191 | 192 | Related project comparison: Firejail 193 | ------------------------------------ 194 | 195 | [Firejail](https://github.com/netblue30/firejail/tree/HEAD/src/firejail) 196 | is similar to Flatpak before bubblewrap was split out in that it combines 197 | a setuid tool with a lot of desktop-specific sandboxing features. For 198 | example, Firejail knows about Pulseaudio, whereas bubblewrap does not. 199 | 200 | The bubblewrap authors believe it's much easier to audit a small 201 | setuid program, and keep features such as Pulseaudio filtering as an 202 | unprivileged process, as now occurs in Flatpak. 203 | 204 | Also, @cgwalters thinks trying to 205 | [whitelist file paths](https://github.com/netblue30/firejail/blob/37a5a3545ef6d8d03dad8bbd888f53e13274c9e5/src/firejail/fs_whitelist.c#L176) 206 | is a bad idea given the myriad ways users have to manipulate paths, 207 | and the myriad ways in which system administrators may configure a 208 | system. The bubblewrap approach is to only retain a few specific 209 | Linux capabilities such as `CAP_SYS_ADMIN`, but to always access the 210 | filesystem as the invoking uid. This entirely closes 211 | [TOCTTOU attacks](https://cwe.mitre.org/data/definitions/367.html) and 212 | such. 213 | 214 | Related project comparison: Sandstorm.io 215 | ---------------------------------------- 216 | 217 | [Sandstorm.io](https://sandstorm.io/) requires unprivileged user 218 | namespaces to set up its sandbox, though it could easily be adapted 219 | to operate in a setuid mode as well. @cgwalters believes their code is 220 | fairly good, but it could still make sense to unify on bubblewrap. 221 | However, @kentonv (of Sandstorm) feels that while this makes sense 222 | in principle, the switching cost outweighs the practical benefits for 223 | now. This decision could be re-evaluated in the future, but it is not 224 | being actively pursued today. 225 | 226 | Related project comparison: runc/binctr 227 | ---------------------------------------- 228 | 229 | [runC](https://github.com/opencontainers/runc) is currently working on 230 | supporting [rootless containers](https://github.com/opencontainers/runc/pull/774), 231 | without needing `setuid` or any other privileges during installation of 232 | runC (using unprivileged user namespaces rather than `setuid`), 233 | creation, and management of containers. However, the standard mode of 234 | using runC is similar to [systemd nspawn](https://www.freedesktop.org/software/systemd/man/systemd-nspawn.html) 235 | in that it is tooling intended to be invoked by root. 236 | 237 | The bubblewrap authors believe that runc and systemd-nspawn are not 238 | designed to be made setuid, and are distant from supporting such a mode. 239 | However with rootless containers, runC will be able to fulfill certain usecases 240 | that bubblewrap supports (with the added benefit of being a standardised and 241 | complete OCI runtime). 242 | 243 | [binctr](https://github.com/jfrazelle/binctr) is just a wrapper for 244 | runC, so inherits all of its design tradeoffs. 245 | 246 | What's with the name?! 247 | ---------------------- 248 | 249 | The name bubblewrap was chosen to convey that this 250 | tool runs as the parent of the application (so wraps it in some sense) and creates 251 | a protective layer (the sandbox) around it. 252 | 253 | ![](bubblewrap.jpg) 254 | 255 | (Bubblewrap cat by [dancing_stupidity](https://www.flickr.com/photos/27549668@N03/)) 256 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security and Disclosure Information Policy for the bubblewrap Project 2 | 3 | The bubblewrap Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/HEAD/SECURITY.md) for the Containers Projects. 4 | 5 | ### System security 6 | 7 | If bubblewrap is setuid root, then the goal is that it does not allow 8 | a malicious local user to do anything that would not have been possible 9 | on a kernel that allows unprivileged users to create new user namespaces. 10 | For example, [CVE-2020-5291](https://github.com/containers/bubblewrap/security/advisories/GHSA-j2qp-rvxj-43vj) 11 | was treated as a security vulnerability in bubblewrap. 12 | 13 | If bubblewrap is not setuid root, then it is not a security boundary 14 | between the user and the OS, because anything bubblewrap could do, a 15 | malicious user could equally well do by writing their own tool equivalent 16 | to bubblewrap. 17 | 18 | ### Sandbox security 19 | 20 | bubblewrap is a toolkit for constructing sandbox environments. 21 | bubblewrap is not a complete, ready-made sandbox with a specific security 22 | policy. 23 | 24 | Some of bubblewrap's use-cases want a security boundary between the sandbox 25 | and the real system; other use-cases want the ability to change the layout of 26 | the filesystem for processes inside the sandbox, but do not aim to be a 27 | security boundary. 28 | As a result, the level of protection between the sandboxed processes and 29 | the host system is entirely determined by the arguments passed to 30 | bubblewrap. 31 | 32 | Whatever program constructs the command-line arguments for bubblewrap 33 | (often a larger framework like Flatpak, libgnome-desktop, sandwine 34 | or an ad-hoc script) is responsible for defining its own security model, 35 | and choosing appropriate bubblewrap command-line arguments to implement 36 | that security model. 37 | 38 | For example, 39 | [CVE-2017-5226](https://github.com/flatpak/flatpak/security/advisories/GHSA-7gfv-rvfx-h87x) 40 | (in which a Flatpak app could send input to a parent terminal using the 41 | `TIOCSTI` ioctl) is considered to be a Flatpak vulnerability, not a 42 | bubblewrap vulnerability. 43 | -------------------------------------------------------------------------------- /bind-mount.c: -------------------------------------------------------------------------------- 1 | /* bubblewrap 2 | * Copyright (C) 2016 Alexander Larsson 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library. If not, see . 17 | * 18 | */ 19 | 20 | #include "config.h" 21 | 22 | #include 23 | 24 | #include "utils.h" 25 | #include "bind-mount.h" 26 | 27 | static char * 28 | skip_token (char *line, bool eat_whitespace) 29 | { 30 | while (*line != ' ' && *line != '\n') 31 | line++; 32 | 33 | if (eat_whitespace && *line == ' ') 34 | line++; 35 | 36 | return line; 37 | } 38 | 39 | static char * 40 | unescape_inline (char *escaped) 41 | { 42 | char *unescaped, *res; 43 | const char *end; 44 | 45 | res = escaped; 46 | end = escaped + strlen (escaped); 47 | 48 | unescaped = escaped; 49 | while (escaped < end) 50 | { 51 | if (*escaped == '\\') 52 | { 53 | *unescaped++ = 54 | ((escaped[1] - '0') << 6) | 55 | ((escaped[2] - '0') << 3) | 56 | ((escaped[3] - '0') << 0); 57 | escaped += 4; 58 | } 59 | else 60 | { 61 | *unescaped++ = *escaped++; 62 | } 63 | } 64 | *unescaped = 0; 65 | return res; 66 | } 67 | 68 | static bool 69 | match_token (const char *token, const char *token_end, const char *str) 70 | { 71 | while (token != token_end && *token == *str) 72 | { 73 | token++; 74 | str++; 75 | } 76 | if (token == token_end) 77 | return *str == 0; 78 | 79 | return false; 80 | } 81 | 82 | static unsigned long 83 | decode_mountoptions (const char *options) 84 | { 85 | const char *token, *end_token; 86 | int i; 87 | unsigned long flags = 0; 88 | static const struct { int flag; 89 | const char *name; 90 | } flags_data[] = { 91 | { 0, "rw" }, 92 | { MS_RDONLY, "ro" }, 93 | { MS_NOSUID, "nosuid" }, 94 | { MS_NODEV, "nodev" }, 95 | { MS_NOEXEC, "noexec" }, 96 | { MS_NOATIME, "noatime" }, 97 | { MS_NODIRATIME, "nodiratime" }, 98 | { MS_RELATIME, "relatime" }, 99 | { 0, NULL } 100 | }; 101 | 102 | token = options; 103 | do 104 | { 105 | end_token = strchr (token, ','); 106 | if (end_token == NULL) 107 | end_token = token + strlen (token); 108 | 109 | for (i = 0; flags_data[i].name != NULL; i++) 110 | { 111 | if (match_token (token, end_token, flags_data[i].name)) 112 | { 113 | flags |= flags_data[i].flag; 114 | break; 115 | } 116 | } 117 | 118 | if (*end_token != 0) 119 | token = end_token + 1; 120 | else 121 | token = NULL; 122 | } 123 | while (token != NULL); 124 | 125 | return flags; 126 | } 127 | 128 | typedef struct MountInfo MountInfo; 129 | struct MountInfo { 130 | char *mountpoint; 131 | unsigned long options; 132 | }; 133 | 134 | typedef MountInfo *MountTab; 135 | 136 | static void 137 | mount_tab_free (MountTab tab) 138 | { 139 | int i; 140 | 141 | for (i = 0; tab[i].mountpoint != NULL; i++) 142 | free (tab[i].mountpoint); 143 | free (tab); 144 | } 145 | 146 | static inline void 147 | cleanup_mount_tabp (void *p) 148 | { 149 | void **pp = (void **) p; 150 | 151 | if (*pp) 152 | mount_tab_free ((MountTab)*pp); 153 | } 154 | 155 | #define cleanup_mount_tab __attribute__((cleanup (cleanup_mount_tabp))) 156 | 157 | typedef struct MountInfoLine MountInfoLine; 158 | struct MountInfoLine { 159 | const char *mountpoint; 160 | const char *options; 161 | bool covered; 162 | int id; 163 | int parent_id; 164 | MountInfoLine *first_child; 165 | MountInfoLine *next_sibling; 166 | }; 167 | 168 | static unsigned int 169 | count_lines (const char *data) 170 | { 171 | unsigned int count = 0; 172 | const char *p = data; 173 | 174 | while (*p != 0) 175 | { 176 | if (*p == '\n') 177 | count++; 178 | p++; 179 | } 180 | 181 | /* If missing final newline, add one */ 182 | if (p > data && *(p-1) != '\n') 183 | count++; 184 | 185 | return count; 186 | } 187 | 188 | static int 189 | count_mounts (MountInfoLine *line) 190 | { 191 | MountInfoLine *child; 192 | int res = 0; 193 | 194 | if (!line->covered) 195 | res += 1; 196 | 197 | child = line->first_child; 198 | while (child != NULL) 199 | { 200 | res += count_mounts (child); 201 | child = child->next_sibling; 202 | } 203 | 204 | return res; 205 | } 206 | 207 | static MountInfo * 208 | collect_mounts (MountInfo *info, MountInfoLine *line) 209 | { 210 | MountInfoLine *child; 211 | 212 | if (!line->covered) 213 | { 214 | info->mountpoint = xstrdup (line->mountpoint); 215 | info->options = decode_mountoptions (line->options); 216 | info ++; 217 | } 218 | 219 | child = line->first_child; 220 | while (child != NULL) 221 | { 222 | info = collect_mounts (info, child); 223 | child = child->next_sibling; 224 | } 225 | 226 | return info; 227 | } 228 | 229 | static MountTab 230 | parse_mountinfo (int proc_fd, 231 | const char *root_mount) 232 | { 233 | cleanup_free char *mountinfo = NULL; 234 | cleanup_free MountInfoLine *lines = NULL; 235 | cleanup_free MountInfoLine **by_id = NULL; 236 | cleanup_mount_tab MountTab mount_tab = NULL; 237 | MountInfo *end_tab; 238 | int n_mounts; 239 | char *line; 240 | unsigned int i; 241 | int max_id; 242 | unsigned int n_lines; 243 | int root; 244 | 245 | mountinfo = load_file_at (proc_fd, "self/mountinfo"); 246 | if (mountinfo == NULL) 247 | die_with_error ("Can't open /proc/self/mountinfo"); 248 | 249 | n_lines = count_lines (mountinfo); 250 | lines = xcalloc (n_lines, sizeof (MountInfoLine)); 251 | 252 | max_id = 0; 253 | line = mountinfo; 254 | i = 0; 255 | root = -1; 256 | while (*line != 0) 257 | { 258 | int rc, consumed = 0; 259 | unsigned int maj, min; 260 | char *end; 261 | char *rest; 262 | char *mountpoint; 263 | char *mountpoint_end; 264 | char *options; 265 | char *options_end; 266 | char *next_line; 267 | 268 | assert (i < n_lines); 269 | 270 | end = strchr (line, '\n'); 271 | if (end != NULL) 272 | { 273 | *end = 0; 274 | next_line = end + 1; 275 | } 276 | else 277 | next_line = line + strlen (line); 278 | 279 | rc = sscanf (line, "%d %d %u:%u %n", &lines[i].id, &lines[i].parent_id, &maj, &min, &consumed); 280 | if (rc != 4) 281 | die ("Can't parse mountinfo line"); 282 | rest = line + consumed; 283 | 284 | rest = skip_token (rest, true); /* mountroot */ 285 | mountpoint = rest; 286 | rest = skip_token (rest, false); /* mountpoint */ 287 | mountpoint_end = rest++; 288 | options = rest; 289 | rest = skip_token (rest, false); /* vfs options */ 290 | options_end = rest; 291 | 292 | *mountpoint_end = 0; 293 | lines[i].mountpoint = unescape_inline (mountpoint); 294 | 295 | *options_end = 0; 296 | lines[i].options = options; 297 | 298 | if (lines[i].id > max_id) 299 | max_id = lines[i].id; 300 | if (lines[i].parent_id > max_id) 301 | max_id = lines[i].parent_id; 302 | 303 | if (path_equal (lines[i].mountpoint, root_mount)) 304 | root = i; 305 | 306 | i++; 307 | line = next_line; 308 | } 309 | assert (i == n_lines); 310 | 311 | if (root == -1) 312 | { 313 | mount_tab = xcalloc (1, sizeof (MountInfo)); 314 | return steal_pointer (&mount_tab); 315 | } 316 | 317 | by_id = xcalloc (max_id + 1, sizeof (MountInfoLine*)); 318 | for (i = 0; i < n_lines; i++) 319 | by_id[lines[i].id] = &lines[i]; 320 | 321 | for (i = 0; i < n_lines; i++) 322 | { 323 | MountInfoLine *this = &lines[i]; 324 | MountInfoLine *parent = by_id[this->parent_id]; 325 | MountInfoLine **to_sibling; 326 | MountInfoLine *sibling; 327 | bool covered = false; 328 | 329 | if (!has_path_prefix (this->mountpoint, root_mount)) 330 | continue; 331 | 332 | if (parent == NULL) 333 | continue; 334 | 335 | if (strcmp (parent->mountpoint, this->mountpoint) == 0) 336 | parent->covered = true; 337 | 338 | to_sibling = &parent->first_child; 339 | sibling = parent->first_child; 340 | while (sibling != NULL) 341 | { 342 | /* If this mountpoint is a path prefix of the sibling, 343 | * say this->mp=/foo/bar and sibling->mp=/foo, then it is 344 | * covered by the sibling, and we drop it. */ 345 | if (has_path_prefix (this->mountpoint, sibling->mountpoint)) 346 | { 347 | covered = true; 348 | break; 349 | } 350 | 351 | /* If the sibling is a path prefix of this mount point, 352 | * say this->mp=/foo and sibling->mp=/foo/bar, then the sibling 353 | * is covered, and we drop it. 354 | */ 355 | if (has_path_prefix (sibling->mountpoint, this->mountpoint)) 356 | *to_sibling = sibling->next_sibling; 357 | else 358 | to_sibling = &sibling->next_sibling; 359 | sibling = sibling->next_sibling; 360 | } 361 | 362 | if (covered) 363 | continue; 364 | 365 | *to_sibling = this; 366 | } 367 | 368 | n_mounts = count_mounts (&lines[root]); 369 | mount_tab = xcalloc (n_mounts + 1, sizeof (MountInfo)); 370 | 371 | end_tab = collect_mounts (&mount_tab[0], &lines[root]); 372 | assert (end_tab == &mount_tab[n_mounts]); 373 | 374 | return steal_pointer (&mount_tab); 375 | } 376 | 377 | bind_mount_result 378 | bind_mount (int proc_fd, 379 | const char *src, 380 | const char *dest, 381 | bind_option_t options, 382 | char **failing_path) 383 | { 384 | bool readonly = (options & BIND_READONLY) != 0; 385 | bool devices = (options & BIND_DEVICES) != 0; 386 | bool recursive = (options & BIND_RECURSIVE) != 0; 387 | unsigned long current_flags, new_flags; 388 | cleanup_mount_tab MountTab mount_tab = NULL; 389 | cleanup_free char *resolved_dest = NULL; 390 | cleanup_free char *dest_proc = NULL; 391 | cleanup_free char *oldroot_dest_proc = NULL; 392 | cleanup_free char *kernel_case_combination = NULL; 393 | cleanup_fd int dest_fd = -1; 394 | int i; 395 | 396 | if (src) 397 | { 398 | if (mount (src, dest, NULL, MS_SILENT | MS_BIND | (recursive ? MS_REC : 0), NULL) != 0) 399 | return BIND_MOUNT_ERROR_MOUNT; 400 | } 401 | 402 | /* The mount operation will resolve any symlinks in the destination 403 | path, so to find it in the mount table we need to do that too. */ 404 | resolved_dest = realpath (dest, NULL); 405 | if (resolved_dest == NULL) 406 | return BIND_MOUNT_ERROR_REALPATH_DEST; 407 | 408 | dest_fd = TEMP_FAILURE_RETRY (open (resolved_dest, O_PATH | O_CLOEXEC)); 409 | if (dest_fd < 0) 410 | { 411 | if (failing_path != NULL) 412 | *failing_path = steal_pointer (&resolved_dest); 413 | 414 | return BIND_MOUNT_ERROR_REOPEN_DEST; 415 | } 416 | 417 | /* If we are in a case-insensitive filesystem, mountinfo might contain a 418 | * different case combination of the path we requested to mount. 419 | * This is due to the fact that the kernel, as of the beginning of 2021, 420 | * populates mountinfo with whatever case combination first appeared in the 421 | * dcache; kernel developers plan to change this in future so that it 422 | * reflects the on-disk encoding instead. 423 | * To avoid throwing an error when this happens, we use readlink() result 424 | * instead of the provided @root_mount, so that we can compare the mountinfo 425 | * entries with the same case combination that the kernel is expected to 426 | * use. */ 427 | dest_proc = xasprintf ("/proc/self/fd/%d", dest_fd); 428 | oldroot_dest_proc = get_oldroot_path (dest_proc); 429 | kernel_case_combination = readlink_malloc (oldroot_dest_proc); 430 | if (kernel_case_combination == NULL) 431 | { 432 | if (failing_path != NULL) 433 | *failing_path = steal_pointer (&resolved_dest); 434 | 435 | return BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD; 436 | } 437 | 438 | mount_tab = parse_mountinfo (proc_fd, kernel_case_combination); 439 | if (mount_tab[0].mountpoint == NULL) 440 | { 441 | if (failing_path != NULL) 442 | *failing_path = steal_pointer (&kernel_case_combination); 443 | 444 | errno = EINVAL; 445 | return BIND_MOUNT_ERROR_FIND_DEST_MOUNT; 446 | } 447 | 448 | assert (path_equal (mount_tab[0].mountpoint, kernel_case_combination)); 449 | current_flags = mount_tab[0].options; 450 | new_flags = current_flags | (devices ? 0 : MS_NODEV) | MS_NOSUID | (readonly ? MS_RDONLY : 0); 451 | if (new_flags != current_flags && 452 | mount ("none", resolved_dest, 453 | NULL, MS_SILENT | MS_BIND | MS_REMOUNT | new_flags, NULL) != 0) 454 | { 455 | if (failing_path != NULL) 456 | *failing_path = steal_pointer (&resolved_dest); 457 | 458 | return BIND_MOUNT_ERROR_REMOUNT_DEST; 459 | } 460 | 461 | /* We need to work around the fact that a bind mount does not apply the flags, so we need to manually 462 | * apply the flags to all submounts in the recursive case. 463 | * Note: This does not apply the flags to mounts which are later propagated into this namespace. 464 | */ 465 | if (recursive) 466 | { 467 | for (i = 1; mount_tab[i].mountpoint != NULL; i++) 468 | { 469 | current_flags = mount_tab[i].options; 470 | new_flags = current_flags | (devices ? 0 : MS_NODEV) | MS_NOSUID | (readonly ? MS_RDONLY : 0); 471 | if (new_flags != current_flags && 472 | mount ("none", mount_tab[i].mountpoint, 473 | NULL, MS_SILENT | MS_BIND | MS_REMOUNT | new_flags, NULL) != 0) 474 | { 475 | /* If we can't read the mountpoint we can't remount it, but that should 476 | be safe to ignore because its not something the user can access. */ 477 | if (errno != EACCES) 478 | { 479 | if (failing_path != NULL) 480 | *failing_path = xstrdup (mount_tab[i].mountpoint); 481 | 482 | return BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT; 483 | } 484 | } 485 | } 486 | } 487 | 488 | return BIND_MOUNT_SUCCESS; 489 | } 490 | 491 | /** 492 | * Return a string representing bind_mount_result, like strerror(). 493 | * If want_errno_p is non-NULL, *want_errno_p is used to indicate whether 494 | * it would make sense to print strerror(saved_errno). 495 | */ 496 | static char * 497 | bind_mount_result_to_string (bind_mount_result res, 498 | const char *failing_path, 499 | bool *want_errno_p) 500 | { 501 | char *string = NULL; 502 | bool want_errno = true; 503 | 504 | switch (res) 505 | { 506 | case BIND_MOUNT_ERROR_MOUNT: 507 | string = xstrdup ("Unable to mount source on destination"); 508 | break; 509 | 510 | case BIND_MOUNT_ERROR_REALPATH_DEST: 511 | string = xstrdup ("realpath(destination)"); 512 | break; 513 | 514 | case BIND_MOUNT_ERROR_REOPEN_DEST: 515 | string = xasprintf ("open(\"%s\", O_PATH)", failing_path); 516 | break; 517 | 518 | case BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD: 519 | string = xasprintf ("readlink(/proc/self/fd/N) for \"%s\"", failing_path); 520 | break; 521 | 522 | case BIND_MOUNT_ERROR_FIND_DEST_MOUNT: 523 | string = xasprintf ("Unable to find \"%s\" in mount table", failing_path); 524 | want_errno = false; 525 | break; 526 | 527 | case BIND_MOUNT_ERROR_REMOUNT_DEST: 528 | string = xasprintf ("Unable to remount destination \"%s\" with correct flags", 529 | failing_path); 530 | break; 531 | 532 | case BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT: 533 | string = xasprintf ("Unable to apply mount flags: remount \"%s\"", 534 | failing_path); 535 | break; 536 | 537 | case BIND_MOUNT_SUCCESS: 538 | string = xstrdup ("Success"); 539 | break; 540 | 541 | default: 542 | string = xstrdup ("(unknown/invalid bind_mount_result)"); 543 | break; 544 | } 545 | 546 | if (want_errno_p != NULL) 547 | *want_errno_p = want_errno; 548 | 549 | return string; 550 | } 551 | 552 | void 553 | die_with_bind_result (bind_mount_result res, 554 | int saved_errno, 555 | const char *failing_path, 556 | const char *format, 557 | ...) 558 | { 559 | va_list args; 560 | bool want_errno = true; 561 | char *message; 562 | 563 | if (bwrap_level_prefix) 564 | fprintf (stderr, "<%d>", LOG_ERR); 565 | 566 | fprintf (stderr, "bwrap: "); 567 | 568 | va_start (args, format); 569 | vfprintf (stderr, format, args); 570 | va_end (args); 571 | 572 | message = bind_mount_result_to_string (res, failing_path, &want_errno); 573 | fprintf (stderr, ": %s", message); 574 | /* message is leaked, but we're exiting unsuccessfully anyway, so ignore */ 575 | 576 | if (want_errno) 577 | { 578 | switch (res) 579 | { 580 | case BIND_MOUNT_ERROR_MOUNT: 581 | case BIND_MOUNT_ERROR_REMOUNT_DEST: 582 | case BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT: 583 | fprintf (stderr, ": %s", mount_strerror (saved_errno)); 584 | break; 585 | 586 | case BIND_MOUNT_ERROR_REALPATH_DEST: 587 | case BIND_MOUNT_ERROR_REOPEN_DEST: 588 | case BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD: 589 | case BIND_MOUNT_ERROR_FIND_DEST_MOUNT: 590 | case BIND_MOUNT_SUCCESS: 591 | default: 592 | fprintf (stderr, ": %s", strerror (saved_errno)); 593 | } 594 | } 595 | 596 | fprintf (stderr, "\n"); 597 | exit (1); 598 | } 599 | -------------------------------------------------------------------------------- /bind-mount.h: -------------------------------------------------------------------------------- 1 | /* bubblewrap 2 | * Copyright (C) 2016 Alexander Larsson 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library. If not, see . 17 | * 18 | */ 19 | 20 | #pragma once 21 | 22 | #include "utils.h" 23 | 24 | typedef enum { 25 | BIND_READONLY = (1 << 0), 26 | BIND_DEVICES = (1 << 2), 27 | BIND_RECURSIVE = (1 << 3), 28 | } bind_option_t; 29 | 30 | typedef enum 31 | { 32 | BIND_MOUNT_SUCCESS = 0, 33 | BIND_MOUNT_ERROR_MOUNT, 34 | BIND_MOUNT_ERROR_REALPATH_DEST, 35 | BIND_MOUNT_ERROR_REOPEN_DEST, 36 | BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD, 37 | BIND_MOUNT_ERROR_FIND_DEST_MOUNT, 38 | BIND_MOUNT_ERROR_REMOUNT_DEST, 39 | BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT, 40 | } bind_mount_result; 41 | 42 | bind_mount_result bind_mount (int proc_fd, 43 | const char *src, 44 | const char *dest, 45 | bind_option_t options, 46 | char **failing_path); 47 | 48 | void die_with_bind_result (bind_mount_result res, 49 | int saved_errno, 50 | const char *failing_path, 51 | const char *format, 52 | ...) 53 | __attribute__((__noreturn__)) 54 | __attribute__((format (printf, 4, 5))); 55 | -------------------------------------------------------------------------------- /bubblewrap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/containers/bubblewrap/9ca3b05ec787acfb4b17bed37db5719fa777834f/bubblewrap.jpg -------------------------------------------------------------------------------- /ci/builddeps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2021 Simon McVittie 3 | # SPDX-License-Identifier: LGPL-2.0-or-later 4 | 5 | set -eux 6 | set -o pipefail 7 | 8 | usage() { 9 | if [ "${1-2}" -ne 0 ]; then 10 | exec >&2 11 | fi 12 | cat <&2 46 | usage 2 47 | ;; 48 | esac 49 | done 50 | 51 | # No more arguments please 52 | for arg in "$@"; do 53 | usage 2 54 | done 55 | 56 | if dpkg-vendor --derives-from Debian; then 57 | apt-get -y update 58 | apt-get -q -y install \ 59 | build-essential \ 60 | docbook-xml \ 61 | docbook-xsl \ 62 | libcap-dev \ 63 | libselinux1-dev \ 64 | libtool \ 65 | meson \ 66 | pkg-config \ 67 | python3 \ 68 | xsltproc \ 69 | ${NULL+} 70 | 71 | if [ -n "${opt_clang}" ]; then 72 | apt-get -y install clang 73 | fi 74 | 75 | exit 0 76 | fi 77 | 78 | if command -v yum; then 79 | yum -y install \ 80 | 'pkgconfig(libselinux)' \ 81 | /usr/bin/eu-readelf \ 82 | docbook-style-xsl \ 83 | gcc \ 84 | git \ 85 | libasan \ 86 | libcap-devel \ 87 | libtool \ 88 | libtsan \ 89 | libubsan \ 90 | libxslt \ 91 | make \ 92 | meson \ 93 | redhat-rpm-config \ 94 | rsync \ 95 | ${NULL+} 96 | 97 | if [ -n "${opt_clang}" ]; then 98 | yum -y install clang 99 | fi 100 | 101 | exit 0 102 | fi 103 | 104 | echo "Unknown distribution" >&2 105 | exit 1 106 | 107 | # vim:set sw=4 sts=4 et: 108 | -------------------------------------------------------------------------------- /completions/bash/bwrap: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | # bash completion file for bubblewrap commands 4 | # 5 | 6 | _bwrap() { 7 | local cur prev words cword 8 | _init_completion || return 9 | 10 | # Please keep sorted in LC_ALL=C order 11 | local boolean_options=" 12 | --as-pid-1 13 | --assert-userns-disabled 14 | --clearenv 15 | --disable-userns 16 | --help 17 | --new-session 18 | --unshare-all 19 | --unshare-cgroup 20 | --unshare-cgroup-try 21 | --unshare-ipc 22 | --unshare-net 23 | --unshare-pid 24 | --unshare-user 25 | --unshare-user-try 26 | --unshare-uts 27 | --version 28 | " 29 | 30 | # Please keep sorted in LC_ALL=C order 31 | local options_with_args=" 32 | $boolean_optons 33 | --add-seccomp-fd 34 | --args 35 | --argv0 36 | --bind 37 | --bind-data 38 | --block-fd 39 | --cap-add 40 | --cap-drop 41 | --chdir 42 | --chmod 43 | --dev 44 | --dev-bind 45 | --die-with-parent 46 | --dir 47 | --exec-label 48 | --file 49 | --file-label 50 | --gid 51 | --hostname 52 | --info-fd 53 | --lock-file 54 | --overlay 55 | --overlay-src 56 | --perms 57 | --proc 58 | --remount-ro 59 | --ro-bind 60 | --ro-overlay 61 | --seccomp 62 | --setenv 63 | --size 64 | --symlink 65 | --sync-fd 66 | --tmp-overlay 67 | --uid 68 | --unsetenv 69 | --userns-block-fd 70 | " 71 | 72 | if [[ "$cur" == -* ]]; then 73 | COMPREPLY=( $( compgen -W "$boolean_options $options_with_args" -- "$cur" ) ) 74 | fi 75 | 76 | return 0 77 | } 78 | complete -F _bwrap bwrap 79 | 80 | # vim:set ft=bash: 81 | -------------------------------------------------------------------------------- /completions/bash/meson.build: -------------------------------------------------------------------------------- 1 | bash_completion_dir = get_option('bash_completion_dir') 2 | 3 | if bash_completion_dir == '' 4 | bash_completion = dependency( 5 | 'bash-completion', 6 | version : '>=2.0', 7 | required : false, 8 | ) 9 | 10 | if bash_completion.found() 11 | if meson.version().version_compare('>=0.51.0') 12 | bash_completion_dir = bash_completion.get_variable( 13 | default_value: '', 14 | pkgconfig: 'completionsdir', 15 | pkgconfig_define: [ 16 | 'datadir', get_option('prefix') / get_option('datadir'), 17 | ], 18 | ) 19 | else 20 | bash_completion_dir = bash_completion.get_pkgconfig_variable( 21 | 'completionsdir', 22 | default: '', 23 | define_variable: [ 24 | 'datadir', get_option('prefix') / get_option('datadir'), 25 | ], 26 | ) 27 | endif 28 | endif 29 | endif 30 | 31 | if bash_completion_dir == '' 32 | bash_completion_dir = get_option('datadir') / 'bash-completion' / 'completions' 33 | endif 34 | 35 | install_data('bwrap', install_dir : bash_completion_dir) 36 | -------------------------------------------------------------------------------- /completions/meson.build: -------------------------------------------------------------------------------- 1 | if get_option('bash_completion').enabled() 2 | subdir('bash') 3 | endif 4 | 5 | if get_option('zsh_completion').enabled() 6 | subdir('zsh') 7 | endif 8 | -------------------------------------------------------------------------------- /completions/zsh/_bwrap: -------------------------------------------------------------------------------- 1 | #compdef bwrap 2 | 3 | _bwrap_args_after_perms_size=( 4 | # Please sort alphabetically (in LC_ALL=C order) by option name 5 | '--tmpfs[Mount new tmpfs on DEST]:mount point for tmpfs:_files -/' 6 | ) 7 | 8 | _bwrap_args_after_perms=( 9 | # Please sort alphabetically (in LC_ALL=C order) by option name 10 | '--bind-data[Copy from FD to file which is bind-mounted on DEST]: :_guard "[0-9]#" "file descriptor to read content":destination:_files' 11 | '--dir[Create dir at DEST]:directory to create:_files -/' 12 | '--file[Copy from FD to destination DEST]: :_guard "[0-9]#" "file descriptor to read content from":destination:_files' 13 | '--ro-bind-data[Copy from FD to file which is readonly bind-mounted on DEST]: :_guard "[0-9]#" "file descriptor to read content from":destination:_files' 14 | '--size[Set size in bytes for next action argument]: :->after_perms_size' 15 | '--tmpfs[Mount new tmpfs on DEST]:mount point for tmpfs:_files -/' 16 | ) 17 | 18 | _bwrap_args_after_size=( 19 | # Please sort alphabetically (in LC_ALL=C order) by option name 20 | '--perms[Set permissions for next action argument]: :_guard "[0-7]#" "permissions in octal": :->after_perms_size' 21 | '--tmpfs[Mount new tmpfs on DEST]:mount point for tmpfs:_files -/' 22 | ) 23 | 24 | _bwrap_args=( 25 | '*::arguments:_normal' 26 | $_bwrap_args_after_perms 27 | 28 | # Please sort alphabetically (in LC_ALL=C order) by option name 29 | '--add-seccomp-fd[Load and use seccomp rules from FD]: :_guard "[0-9]#" "file descriptor to read seccomp rules from"' 30 | '--assert-userns-disabled[Fail unless further use of user namespace inside sandbox is disabled]' 31 | '--args[Parse NUL-separated args from FD]: :_guard "[0-9]#" "file descriptor with NUL-separated arguments"' 32 | '--argv0[Set argv0 to the value VALUE before running the program]:value:' 33 | '--as-pid-1[Do not install a reaper process with PID=1]' 34 | '--bind-try[Equal to --bind but ignores non-existent SRC]:source:_files:destination:_files' 35 | '--bind[Bind mount the host path SRC on DEST]:source:_files:destination:_files' 36 | '--block-fd[Block on FD until some data to read is available]: :_guard "[0-9]#" "file descriptor to block on"' 37 | '--cap-add[Add cap CAP when running as privileged user]:capability to add:->caps' 38 | '--cap-drop[Drop cap CAP when running as privileged user]:capability to add:->caps' 39 | '--chdir[Change directory to DIR]:working directory for sandbox: _files -/' 40 | '--chmod[Set permissions]: :_guard "[0-7]#" "permissions in octal":path to set permissions:_files' 41 | '--clearenv[Unset all environment variables]' 42 | '--dev-bind-try[Equal to --dev-bind but ignores non-existent SRC]:source:_files:destination:_files' 43 | '--dev-bind[Bind mount the host path SRC on DEST, allowing device access]:source:_files:destination:_files' 44 | '--dev[Mount new dev on DEST]:mount point for /dev:_files -/' 45 | "--die-with-parent[Kills with SIGKILL child process (COMMAND) when bwrap or bwrap's parent dies.]" 46 | '--disable-userns[Disable further use of user namespaces inside sandbox]' 47 | '--exec-label[Exec label for the sandbox]:SELinux label:_selinux_contexts' 48 | '--file-label[File label for temporary sandbox content]:SELinux label:_selinux_contexts' 49 | '--gid[Custom gid in the sandbox (requires --unshare-user or --userns)]: :_guard "[0-9]#" "numeric group ID"' 50 | '--help[Print help and exit]' 51 | '--hostname[Custom hostname in the sandbox (requires --unshare-uts)]:hostname:' 52 | '--info-fd[Write information about the running container to FD]: :_guard "[0-9]#" "file descriptor to write to"' 53 | '--json-status-fd[Write container status to FD as multiple JSON documents]: :_guard "[0-9]#" "file descriptor to write to"' 54 | '--lock-file[Take a lock on DEST while sandbox is running]:lock file:_files' 55 | '--mqueue[Mount new mqueue on DEST]:mount point for mqueue:_files -/' 56 | '--new-session[Create a new terminal session]' 57 | '--perms[Set permissions for next action argument]: :_guard "[0-7]#" "permissions in octal": :->after_perms' 58 | '--pidns[Use this user namespace (as parent namespace if using --unshare-pid)]: :' 59 | '--proc[Mount new procfs on DEST]:mount point for procfs:_files -/' 60 | '--remount-ro[Remount DEST as readonly; does not recursively remount]:mount point to remount read-only:_files' 61 | '--ro-bind-try[Equal to --ro-bind but ignores non-existent SRC]:source:_files:destination:_files' 62 | '--ro-bind[Bind mount the host path SRC readonly on DEST]:source:_files:destination:_files' 63 | '--seccomp[Load and use seccomp rules from FD]: :_guard "[0-9]#" "file descriptor to read seccomp rules from"' 64 | '--setenv[Set an environment variable]:variable to set:_parameters -g "*export*":value of variable: :' 65 | '--size[Set size in bytes for next action argument]: :->after_size' 66 | '--symlink[Create symlink at DEST with target SRC]:symlink target:_files:symlink to create:_files:' 67 | '--sync-fd[Keep this fd open while sandbox is running]: :_guard "[0-9]#" "file descriptor to keep open"' 68 | '--uid[Custom uid in the sandbox (requires --unshare-user or --userns)]: :_guard "[0-9]#" "numeric group ID"' 69 | '(--clearenv)--unsetenv[Unset an environment variable]:variable to unset:_parameters -g "*export*"' 70 | '--unshare-all[Unshare every namespace we support by default]' 71 | '--unshare-cgroup-try[Create new cgroup namespace if possible else continue by skipping it]' 72 | '--unshare-cgroup[Create new cgroup namespace]' 73 | '--unshare-ipc[Create new ipc namespace]' 74 | '--unshare-net[Create new network namespace]' 75 | '--unshare-pid[Create new pid namespace]' 76 | '(--userns --userns2)--unshare-user[Create new user namespace (may be automatically implied if not setuid)]' 77 | '--unshare-user-try[Create new user namespace if possible else continue by skipping it]' 78 | '--unshare-uts[Create new uts namespace]' 79 | '(--unshare-user)--userns[Use this user namespace (cannot combine with --unshare-user)]: :' 80 | '--userns-block-fd[Block on FD until the user namespace is ready]: :_guard "[0-9]#" "file descriptor to block on"' 81 | '(--unshare-user)--userns2[After setup switch to this user namespace, only useful with --userns]: :' 82 | '--version[Print version]' 83 | ) 84 | 85 | _bwrap() { 86 | _arguments -S $_bwrap_args 87 | case "$state" in 88 | after_perms) 89 | _values -S ' ' 'option' $_bwrap_args_after_perms 90 | ;; 91 | 92 | after_size) 93 | _values -S ' ' 'option' $_bwrap_args_after_size 94 | ;; 95 | 96 | after_perms_size) 97 | _values -S ' ' 'option' $_bwrap_args_after_perms_size 98 | ;; 99 | 100 | caps) 101 | # $ grep -E '#define\sCAP_\w+\s+[0-9]+' /usr/include/linux/capability.h | awk '{print $2}' | xargs echo 102 | local all_caps=( 103 | CAP_CHOWN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_FSETID \ 104 | CAP_KILL CAP_SETGID CAP_SETUID CAP_SETPCAP CAP_LINUX_IMMUTABLE \ 105 | CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_ADMIN CAP_NET_RAW \ 106 | CAP_IPC_LOCK CAP_IPC_OWNER CAP_SYS_MODULE CAP_SYS_RAWIO CAP_SYS_CHROOT \ 107 | CAP_SYS_PTRACE CAP_SYS_PACCT CAP_SYS_ADMIN CAP_SYS_BOOT CAP_SYS_NICE \ 108 | CAP_SYS_RESOURCE CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_MKNOD CAP_LEASE \ 109 | CAP_AUDIT_WRITE CAP_AUDIT_CONTROL CAP_SETFCAP CAP_MAC_OVERRIDE \ 110 | CAP_MAC_ADMIN CAP_SYSLOG CAP_WAKE_ALARM CAP_BLOCK_SUSPEND CAP_AUDIT_READ 111 | ) 112 | _values 'caps' $all_caps 113 | ;; 114 | esac 115 | } 116 | -------------------------------------------------------------------------------- /completions/zsh/meson.build: -------------------------------------------------------------------------------- 1 | zsh_completion_dir = get_option('zsh_completion_dir') 2 | 3 | if zsh_completion_dir == '' 4 | zsh_completion_dir = get_option('datadir') / 'zsh' / 'site-functions' 5 | endif 6 | 7 | install_data('_bwrap', install_dir : zsh_completion_dir) 8 | -------------------------------------------------------------------------------- /demos/bubblewrap-shell.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use bubblewrap to run /bin/sh reusing the host OS binaries (/usr), but with 3 | # separate /tmp, /home, /var, /run, and /etc. For /etc we just inherit the 4 | # host's resolv.conf, and set up "stub" passwd/group files. Not sharing 5 | # /home for example is intentional. If you wanted to, you could design 6 | # a bwrap-using program that shared individual parts of /home, perhaps 7 | # public content. 8 | # 9 | # Another way to build on this example is to remove --share-net to disable 10 | # networking. 11 | set -euo pipefail 12 | (exec bwrap --ro-bind /usr /usr \ 13 | --dir /tmp \ 14 | --dir /var \ 15 | --symlink ../tmp var/tmp \ 16 | --proc /proc \ 17 | --dev /dev \ 18 | --ro-bind /etc/resolv.conf /etc/resolv.conf \ 19 | --symlink usr/lib /lib \ 20 | --symlink usr/lib64 /lib64 \ 21 | --symlink usr/bin /bin \ 22 | --symlink usr/sbin /sbin \ 23 | --chdir / \ 24 | --unshare-all \ 25 | --share-net \ 26 | --die-with-parent \ 27 | --dir /run/user/$(id -u) \ 28 | --setenv XDG_RUNTIME_DIR "/run/user/`id -u`" \ 29 | --setenv PS1 "bwrap-demo$ " \ 30 | --file 11 /etc/passwd \ 31 | --file 12 /etc/group \ 32 | /bin/sh) \ 33 | 11< <(getent passwd $UID 65534) \ 34 | 12< <(getent group $(id -g) 65534) 35 | -------------------------------------------------------------------------------- /demos/flatpak-run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # For this to work you first have to run these commands: 3 | # curl -O http://sdk.gnome.org/nightly/keys/nightly.gpg 4 | # flatpak --user remote-add --gpg-key=nightly.gpg gnome-nightly http://sdk.gnome.org/nightly/repo/ 5 | # flatpak --user install gnome-nightly org.gnome.Platform 6 | # flatpak --user install gnome-nightly org.gnome.Weather 7 | 8 | mkdir -p ~/.var/app/org.gnome.Weather/cache ~/.var/app/org.gnome.Weather/config ~/.var/app/org.gnome.Weather/data 9 | 10 | ( 11 | exec bwrap \ 12 | --ro-bind ~/.local/share/flatpak/runtime/org.gnome.Platform/x86_64/master/active/files /usr \ 13 | --lock-file /usr/.ref \ 14 | --ro-bind ~/.local/share/flatpak/app/org.gnome.Weather/x86_64/master/active/files/ /app \ 15 | --lock-file /app/.ref \ 16 | --dev /dev \ 17 | --proc /proc \ 18 | --dir /tmp \ 19 | --symlink /tmp /var/tmp \ 20 | --symlink /run /var/run \ 21 | --symlink usr/lib /lib \ 22 | --symlink usr/lib64 /lib64 \ 23 | --symlink usr/bin /bin \ 24 | --symlink usr/sbin /sbin \ 25 | --symlink usr/etc /etc \ 26 | --dir /run/user/`id -u` \ 27 | --ro-bind /etc/machine-id /usr/etc/machine-id \ 28 | --ro-bind /etc/resolv.conf /run/host/monitor/resolv.conf \ 29 | --ro-bind /sys/block /sys/block \ 30 | --ro-bind /sys/bus /sys/bus \ 31 | --ro-bind /sys/class /sys/class \ 32 | --ro-bind /sys/dev /sys/dev \ 33 | --ro-bind /sys/devices /sys/devices \ 34 | --dev-bind /dev/dri /dev/dri \ 35 | --bind /tmp/.X11-unix/X0 /tmp/.X11-unix/X99 \ 36 | --bind ~/.var/app/org.gnome.Weather ~/.var/app/org.gnome.Weather \ 37 | --bind ~/.config/dconf ~/.config/dconf \ 38 | --bind /run/user/`id -u`/dconf /run/user/`id -u`/dconf \ 39 | --unshare-pid \ 40 | --setenv XDG_RUNTIME_DIR "/run/user/`id -u`" \ 41 | --setenv DISPLAY :99 \ 42 | --setenv GI_TYPELIB_PATH /app/lib/girepository-1.0 \ 43 | --setenv GST_PLUGIN_PATH /app/lib/gstreamer-1.0 \ 44 | --setenv LD_LIBRARY_PATH /app/lib:/usr/lib/GL \ 45 | --setenv DCONF_USER_CONFIG_DIR .config/dconf \ 46 | --setenv PATH /app/bin:/usr/bin \ 47 | --setenv XDG_CONFIG_DIRS /app/etc/xdg:/etc/xdg \ 48 | --setenv XDG_DATA_DIRS /app/share:/usr/share \ 49 | --setenv SHELL /bin/sh \ 50 | --setenv XDG_CACHE_HOME ~/.var/app/org.gnome.Weather/cache \ 51 | --setenv XDG_CONFIG_HOME ~/.var/app/org.gnome.Weather/config \ 52 | --setenv XDG_DATA_HOME ~/.var/app/org.gnome.Weather/data \ 53 | --file 10 /run/user/`id -u`/flatpak-info \ 54 | --bind-data 11 /usr/etc/passwd \ 55 | --bind-data 12 /usr/etc/group \ 56 | --seccomp 13 \ 57 | /bin/sh) \ 58 | 11< <(getent passwd $UID 65534 ) \ 59 | 12< <(getent group $(id -g) 65534) \ 60 | 13< `dirname $0`/flatpak.bpf \ 61 | 10<=0.49.0', 6 | default_options : [ 7 | 'warning_level=2', 8 | ], 9 | ) 10 | 11 | cc = meson.get_compiler('c') 12 | add_project_arguments('-D_GNU_SOURCE', language : 'c') 13 | common_include_directories = include_directories('.') 14 | 15 | # Keep this in sync with ostree, except remove -Wall (part of Meson 16 | # warning_level 2) and -Werror=declaration-after-statement 17 | add_project_arguments( 18 | cc.get_supported_arguments([ 19 | '-Werror=shadow', 20 | '-Werror=empty-body', 21 | '-Werror=strict-prototypes', 22 | '-Werror=missing-prototypes', 23 | '-Werror=implicit-function-declaration', 24 | '-Werror=pointer-arith', 25 | '-Werror=init-self', 26 | '-Werror=missing-declarations', 27 | '-Werror=return-type', 28 | '-Werror=overflow', 29 | '-Werror=int-conversion', 30 | '-Werror=parenthesis', 31 | '-Werror=incompatible-pointer-types', 32 | '-Werror=misleading-indentation', 33 | '-Werror=missing-include-dirs', 34 | '-Werror=aggregate-return', 35 | 36 | # Extra warnings specific to bubblewrap 37 | '-Werror=switch-default', 38 | '-Wswitch-enum', 39 | 40 | # Deliberately not warning about these, ability to zero-initialize 41 | # a struct is a feature 42 | '-Wno-missing-field-initializers', 43 | '-Wno-error=missing-field-initializers', 44 | ]), 45 | language : 'c', 46 | ) 47 | 48 | if ( 49 | cc.has_argument('-Werror=format=2') 50 | and cc.has_argument('-Werror=format-security') 51 | and cc.has_argument('-Werror=format-nonliteral') 52 | ) 53 | add_project_arguments([ 54 | '-Werror=format=2', 55 | '-Werror=format-security', 56 | '-Werror=format-nonliteral', 57 | ], language : 'c') 58 | endif 59 | 60 | bash = find_program('bash', required : false) 61 | 62 | if get_option('python') == '' 63 | python = find_program('python3') 64 | else 65 | python = find_program(get_option('python')) 66 | endif 67 | 68 | libcap_dep = dependency('libcap', required : true) 69 | 70 | selinux_dep = dependency( 71 | 'libselinux', 72 | version : '>=2.1.9', 73 | # if disabled, Meson will behave as though libselinux was not found 74 | required : get_option('selinux'), 75 | ) 76 | 77 | cdata = configuration_data() 78 | cdata.set_quoted( 79 | 'PACKAGE_STRING', 80 | '@0@ @1@'.format(meson.project_name(), meson.project_version()), 81 | ) 82 | 83 | if selinux_dep.found() 84 | cdata.set('HAVE_SELINUX', 1) 85 | if selinux_dep.version().version_compare('>=2.3') 86 | cdata.set('HAVE_SELINUX_2_3', 1) 87 | endif 88 | endif 89 | 90 | if get_option('require_userns') 91 | cdata.set('ENABLE_REQUIRE_USERNS', 1) 92 | endif 93 | 94 | configure_file( 95 | output : 'config.h', 96 | configuration : cdata, 97 | ) 98 | 99 | if meson.is_subproject() and get_option('program_prefix') == '' 100 | error('program_prefix option must be set when bwrap is a subproject') 101 | endif 102 | 103 | if get_option('bwrapdir') != '' 104 | bwrapdir = get_option('bwrapdir') 105 | elif meson.is_subproject() 106 | bwrapdir = get_option('libexecdir') 107 | else 108 | bwrapdir = get_option('bindir') 109 | endif 110 | 111 | bwrap = executable( 112 | get_option('program_prefix') + 'bwrap', 113 | [ 114 | 'bubblewrap.c', 115 | 'bind-mount.c', 116 | 'network.c', 117 | 'utils.c', 118 | ], 119 | build_rpath : get_option('build_rpath'), 120 | install : true, 121 | install_dir : bwrapdir, 122 | install_rpath : get_option('install_rpath'), 123 | dependencies : [selinux_dep, libcap_dep], 124 | ) 125 | 126 | manpages_xsl = 'http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl' 127 | xsltproc = find_program('xsltproc', required : get_option('man')) 128 | build_man_page = false 129 | 130 | if xsltproc.found() and not meson.is_subproject() 131 | if run_command([ 132 | xsltproc, '--nonet', manpages_xsl, 133 | ], check : false).returncode() == 0 134 | message('Docbook XSL found, man page enabled') 135 | build_man_page = true 136 | elif get_option('man').enabled() 137 | error('Man page requested, but Docbook XSL stylesheets not found') 138 | else 139 | message('Docbook XSL not found, man page disabled automatically') 140 | endif 141 | endif 142 | 143 | if build_man_page 144 | custom_target( 145 | 'bwrap.1', 146 | output : 'bwrap.1', 147 | input : 'bwrap.xml', 148 | command : [ 149 | xsltproc, 150 | '--nonet', 151 | '--stringparam', 'man.output.quietly', '1', 152 | '--stringparam', 'funcsynopsis.style', 'ansi', 153 | '--stringparam', 'man.th.extra1.suppress', '1', 154 | '--stringparam', 'man.authors.section.enabled', '0', 155 | '--stringparam', 'man.copyright.section.enabled', '0', 156 | '-o', '@OUTPUT@', 157 | manpages_xsl, 158 | '@INPUT@', 159 | ], 160 | install : true, 161 | install_dir : get_option('mandir') / 'man1', 162 | ) 163 | endif 164 | 165 | if not meson.is_subproject() 166 | subdir('completions') 167 | endif 168 | 169 | if get_option('tests') 170 | subdir('tests') 171 | endif 172 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option( 2 | 'bash_completion', 3 | type : 'feature', 4 | description : 'install bash completion script', 5 | value : 'enabled', 6 | ) 7 | option( 8 | 'bash_completion_dir', 9 | type : 'string', 10 | description : 'install bash completion script in this directory', 11 | value : '', 12 | ) 13 | option( 14 | 'bwrapdir', 15 | type : 'string', 16 | description : 'install bwrap in this directory [default: bindir, or libexecdir in subprojects]', 17 | ) 18 | option( 19 | 'build_rpath', 20 | type : 'string', 21 | description : 'set a RUNPATH or RPATH on the bwrap executable', 22 | ) 23 | option( 24 | 'install_rpath', 25 | type : 'string', 26 | description : 'set a RUNPATH or RPATH on the bwrap executable', 27 | ) 28 | option( 29 | 'man', 30 | type : 'feature', 31 | description : 'generate man pages', 32 | value : 'auto', 33 | ) 34 | option( 35 | 'program_prefix', 36 | type : 'string', 37 | description : 'Prepend string to bwrap executable name, for use with subprojects', 38 | ) 39 | option( 40 | 'python', 41 | type : 'string', 42 | description : 'Path to Python 3, or empty to use python3', 43 | ) 44 | option( 45 | 'require_userns', 46 | type : 'boolean', 47 | description : 'require user namespaces by default when installed setuid', 48 | value : false, 49 | ) 50 | option( 51 | 'selinux', 52 | type : 'feature', 53 | description : 'enable optional SELINUX support', 54 | value : 'auto', 55 | ) 56 | option( 57 | 'tests', 58 | type : 'boolean', 59 | description : 'build tests', 60 | value : true, 61 | ) 62 | option( 63 | 'zsh_completion', 64 | type : 'feature', 65 | description : 'install zsh completion script', 66 | value : 'enabled', 67 | ) 68 | option( 69 | 'zsh_completion_dir', 70 | type : 'string', 71 | description : 'install zsh completion script in this directory', 72 | value : '', 73 | ) 74 | -------------------------------------------------------------------------------- /network.c: -------------------------------------------------------------------------------- 1 | /* bubblewrap 2 | * Copyright (C) 2016 Alexander Larsson 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library. If not, see . 17 | * 18 | */ 19 | 20 | #include "config.h" 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include "utils.h" 30 | #include "network.h" 31 | 32 | static void * 33 | add_rta (struct nlmsghdr *header, 34 | int type, 35 | size_t size) 36 | { 37 | struct rtattr *rta; 38 | size_t rta_size = RTA_LENGTH (size); 39 | 40 | rta = (struct rtattr *) ((char *) header + NLMSG_ALIGN (header->nlmsg_len)); 41 | rta->rta_type = type; 42 | rta->rta_len = rta_size; 43 | 44 | header->nlmsg_len = NLMSG_ALIGN (header->nlmsg_len) + rta_size; 45 | 46 | return RTA_DATA (rta); 47 | } 48 | 49 | static int 50 | rtnl_send_request (int rtnl_fd, 51 | struct nlmsghdr *header) 52 | { 53 | struct sockaddr_nl dst_addr = { AF_NETLINK, 0 }; 54 | ssize_t sent; 55 | 56 | sent = TEMP_FAILURE_RETRY (sendto (rtnl_fd, (void *) header, header->nlmsg_len, 0, 57 | (struct sockaddr *) &dst_addr, sizeof (dst_addr))); 58 | if (sent < 0) 59 | return -1; 60 | 61 | return 0; 62 | } 63 | 64 | static int 65 | rtnl_read_reply (int rtnl_fd, 66 | unsigned int seq_nr) 67 | { 68 | char buffer[1024]; 69 | ssize_t received; 70 | struct nlmsghdr *rheader; 71 | 72 | while (1) 73 | { 74 | received = TEMP_FAILURE_RETRY (recv (rtnl_fd, buffer, sizeof (buffer), 0)); 75 | if (received < 0) 76 | return -1; 77 | 78 | rheader = (struct nlmsghdr *) buffer; 79 | while (received >= NLMSG_HDRLEN) 80 | { 81 | if (rheader->nlmsg_seq != seq_nr) 82 | return -1; 83 | if ((pid_t)rheader->nlmsg_pid != getpid ()) 84 | return -1; 85 | if (rheader->nlmsg_type == NLMSG_ERROR) 86 | { 87 | uint32_t *err = NLMSG_DATA (rheader); 88 | if (*err == 0) 89 | return 0; 90 | 91 | return -1; 92 | } 93 | if (rheader->nlmsg_type == NLMSG_DONE) 94 | return 0; 95 | 96 | rheader = NLMSG_NEXT (rheader, received); 97 | } 98 | } 99 | } 100 | 101 | static int 102 | rtnl_do_request (int rtnl_fd, 103 | struct nlmsghdr *header) 104 | { 105 | if (rtnl_send_request (rtnl_fd, header) != 0) 106 | return -1; 107 | 108 | if (rtnl_read_reply (rtnl_fd, header->nlmsg_seq) != 0) 109 | return -1; 110 | 111 | return 0; 112 | } 113 | 114 | static struct nlmsghdr * 115 | rtnl_setup_request (char *buffer, 116 | int type, 117 | int flags, 118 | size_t size) 119 | { 120 | struct nlmsghdr *header; 121 | size_t len = NLMSG_LENGTH (size); 122 | static uint32_t counter = 0; 123 | 124 | memset (buffer, 0, len); 125 | 126 | header = (struct nlmsghdr *) buffer; 127 | header->nlmsg_len = len; 128 | header->nlmsg_type = type; 129 | header->nlmsg_flags = flags | NLM_F_REQUEST; 130 | header->nlmsg_seq = counter++; 131 | header->nlmsg_pid = getpid (); 132 | 133 | return header; 134 | } 135 | 136 | void 137 | loopback_setup (void) 138 | { 139 | int r, if_loopback; 140 | cleanup_fd int rtnl_fd = -1; 141 | char buffer[1024]; 142 | struct sockaddr_nl src_addr = { AF_NETLINK, 0 }; 143 | struct nlmsghdr *header; 144 | struct ifaddrmsg *addmsg; 145 | struct ifinfomsg *infomsg; 146 | struct in_addr *ip_addr; 147 | 148 | src_addr.nl_pid = getpid (); 149 | 150 | if_loopback = (int) if_nametoindex ("lo"); 151 | if (if_loopback <= 0) 152 | die_with_error ("loopback: Failed to look up lo"); 153 | 154 | rtnl_fd = socket (PF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); 155 | if (rtnl_fd < 0) 156 | die_with_error ("loopback: Failed to create NETLINK_ROUTE socket"); 157 | 158 | r = bind (rtnl_fd, (struct sockaddr *) &src_addr, sizeof (src_addr)); 159 | if (r < 0) 160 | die_with_error ("loopback: Failed to bind NETLINK_ROUTE socket"); 161 | 162 | header = rtnl_setup_request (buffer, RTM_NEWADDR, 163 | NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK, 164 | sizeof (struct ifaddrmsg)); 165 | addmsg = NLMSG_DATA (header); 166 | 167 | addmsg->ifa_family = AF_INET; 168 | addmsg->ifa_prefixlen = 8; 169 | addmsg->ifa_flags = IFA_F_PERMANENT; 170 | addmsg->ifa_scope = RT_SCOPE_HOST; 171 | addmsg->ifa_index = if_loopback; 172 | 173 | ip_addr = add_rta (header, IFA_LOCAL, sizeof (*ip_addr)); 174 | ip_addr->s_addr = htonl (INADDR_LOOPBACK); 175 | 176 | ip_addr = add_rta (header, IFA_ADDRESS, sizeof (*ip_addr)); 177 | ip_addr->s_addr = htonl (INADDR_LOOPBACK); 178 | 179 | assert (header->nlmsg_len < sizeof (buffer)); 180 | 181 | if (rtnl_do_request (rtnl_fd, header) != 0) 182 | die_with_error ("loopback: Failed RTM_NEWADDR"); 183 | 184 | header = rtnl_setup_request (buffer, RTM_NEWLINK, 185 | NLM_F_ACK, 186 | sizeof (struct ifinfomsg)); 187 | infomsg = NLMSG_DATA (header); 188 | 189 | infomsg->ifi_family = AF_UNSPEC; 190 | infomsg->ifi_type = 0; 191 | infomsg->ifi_index = if_loopback; 192 | infomsg->ifi_flags = IFF_UP; 193 | infomsg->ifi_change = IFF_UP; 194 | 195 | assert (header->nlmsg_len < sizeof (buffer)); 196 | 197 | if (rtnl_do_request (rtnl_fd, header) != 0) 198 | die_with_error ("loopback: Failed RTM_NEWLINK"); 199 | } 200 | -------------------------------------------------------------------------------- /network.h: -------------------------------------------------------------------------------- 1 | /* bubblewrap 2 | * Copyright (C) 2016 Alexander Larsson 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library. If not, see . 17 | * 18 | */ 19 | 20 | #pragma once 21 | 22 | void loopback_setup (void); 23 | -------------------------------------------------------------------------------- /packaging/bubblewrap.spec: -------------------------------------------------------------------------------- 1 | %global commit0 66d12bb23b04e201c5846e325f0b10930ed802f8 2 | %global shortcommit0 %(c=%{commit0}; echo ${c:0:7}) 3 | 4 | Summary: Core execution tool for unprivileged containers 5 | Name: bubblewrap 6 | Version: 0 7 | Release: 1%{?dist} 8 | #VCS: git:https://github.com/projectatomic/bubblewrap 9 | Source0: https://github.com/projectatomic/%{name}/archive/%{commit0}.tar.gz#/%{name}-%{shortcommit0}.tar.gz 10 | License: LGPLv2+ 11 | URL: https://github.com/projectatomic/bubblewrap 12 | 13 | BuildRequires: git 14 | # We always run autogen.sh 15 | BuildRequires: autoconf automake libtool 16 | BuildRequires: libcap-devel 17 | BuildRequires: pkgconfig(libselinux) 18 | BuildRequires: libxslt 19 | BuildRequires: docbook-style-xsl 20 | 21 | %description 22 | Bubblewrap (/usr/bin/bwrap) is a core execution engine for unprivileged 23 | containers that works as a setuid binary on kernels without 24 | user namespaces. 25 | 26 | %prep 27 | %autosetup -Sgit -n %{name}-%{version} 28 | 29 | %build 30 | env NOCONFIGURE=1 ./autogen.sh 31 | %configure --disable-silent-rules --with-priv-mode=none 32 | 33 | make %{?_smp_mflags} 34 | 35 | %install 36 | make install DESTDIR=$RPM_BUILD_ROOT INSTALL="install -p -c" 37 | find $RPM_BUILD_ROOT -name '*.la' -delete 38 | 39 | %files 40 | %license COPYING 41 | %doc README.md 42 | %{_datadir}/bash-completion/completions/bwrap 43 | %if (0%{?rhel} != 0 && 0%{?rhel} <= 7) 44 | %attr(4755,root,root) %{_bindir}/bwrap 45 | %else 46 | %{_bindir}/bwrap 47 | %endif 48 | %{_mandir}/man1/* 49 | -------------------------------------------------------------------------------- /release-checklist.md: -------------------------------------------------------------------------------- 1 | bubblewrap release checklist 2 | ============================ 3 | 4 | * Collect release notes in `NEWS` 5 | * Update version number in `meson.build` and release date in `NEWS` 6 | * Commit the changes 7 | * `meson dist -C ${builddir}` 8 | * Do any final smoke-testing, e.g. update a package, install and test it 9 | * `git evtag sign v$VERSION` 10 | * Include the release notes from `NEWS` in the tag message 11 | * `git push --atomic origin main v$VERSION` 12 | * https://github.com/containers/bubblewrap/releases/new 13 | * Fill in the new version's tag in the "Tag version" box 14 | * Title: `$VERSION` 15 | * Copy the release notes into the description 16 | * Upload the tarball that you built with `meson dist` 17 | * Get the `sha256sum` of the tarball and append it to the description 18 | * `Publish release` 19 | -------------------------------------------------------------------------------- /tests/libtest-core.sh: -------------------------------------------------------------------------------- 1 | # Core source library for shell script tests; the 2 | # canonical version lives in: 3 | # 4 | # https://github.com/ostreedev/ostree 5 | # 6 | # Known copies are in the following repos: 7 | # 8 | # - https://github.com/containers/bubblewrap 9 | # - https://github.com/coreos/rpm-ostree 10 | # 11 | # Copyright (C) 2017 Colin Walters 12 | # 13 | # SPDX-License-Identifier: LGPL-2.0-or-later 14 | # 15 | # This library is free software; you can redistribute it and/or 16 | # modify it under the terms of the GNU Lesser General Public 17 | # License as published by the Free Software Foundation; either 18 | # version 2 of the License, or (at your option) any later version. 19 | # 20 | # This library is distributed in the hope that it will be useful, 21 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 23 | # Lesser General Public License for more details. 24 | # 25 | # You should have received a copy of the GNU Lesser General Public 26 | # License along with this library; if not, write to the 27 | # Free Software Foundation, Inc., 59 Temple Place - Suite 330, 28 | # Boston, MA 02111-1307, USA. 29 | 30 | fatal() { 31 | echo $@ 1>&2; exit 1 32 | } 33 | # fatal() is shorter to type, but retain this alias 34 | assert_not_reached () { 35 | fatal "$@" 36 | } 37 | 38 | # Some tests look for specific English strings. Use a UTF-8 version 39 | # of the C (POSIX) locale if we have one, or fall back to en_US.UTF-8 40 | # (https://sourceware.org/glibc/wiki/Proposals/C.UTF-8) 41 | # 42 | # If we can't find the locale command assume we have support for C.UTF-8 43 | # (e.g. musl based systems) 44 | if type -p locale >/dev/null; then 45 | export LC_ALL=$(locale -a | grep -iEe '^(C|en_US)\.(UTF-8|utf8)$' | head -n1 || true) 46 | if [ -z "${LC_ALL}" ]; then fatal "Can't find suitable UTF-8 locale"; fi 47 | else 48 | export LC_ALL=C.UTF-8 49 | fi 50 | # A GNU extension, used whenever LC_ALL is not C 51 | unset LANGUAGE 52 | 53 | # This should really be the default IMO 54 | export G_DEBUG=fatal-warnings 55 | 56 | assert_streq () { 57 | test "$1" = "$2" || fatal "$1 != $2" 58 | } 59 | 60 | assert_str_match () { 61 | if ! echo "$1" | grep -E -q "$2"; then 62 | fatal "$1 does not match regexp $2" 63 | fi 64 | } 65 | 66 | assert_not_streq () { 67 | (! test "$1" = "$2") || fatal "$1 == $2" 68 | } 69 | 70 | assert_has_file () { 71 | test -f "$1" || fatal "Couldn't find '$1'" 72 | } 73 | 74 | assert_has_dir () { 75 | test -d "$1" || fatal "Couldn't find '$1'" 76 | } 77 | 78 | # Dump ls -al + file contents to stderr, then fatal() 79 | _fatal_print_file() { 80 | file="$1" 81 | shift 82 | ls -al "$file" >&2 83 | sed -e 's/^/# /' < "$file" >&2 84 | fatal "$@" 85 | } 86 | 87 | _fatal_print_files() { 88 | file1="$1" 89 | shift 90 | file2="$1" 91 | shift 92 | ls -al "$file1" >&2 93 | sed -e 's/^/# /' < "$file1" >&2 94 | ls -al "$file2" >&2 95 | sed -e 's/^/# /' < "$file2" >&2 96 | fatal "$@" 97 | } 98 | 99 | assert_not_has_file () { 100 | if test -f "$1"; then 101 | _fatal_print_file "$1" "File '$1' exists" 102 | fi 103 | } 104 | 105 | assert_not_file_has_content () { 106 | fpath=$1 107 | shift 108 | for re in "$@"; do 109 | if grep -q -e "$re" "$fpath"; then 110 | _fatal_print_file "$fpath" "File '$fpath' matches regexp '$re'" 111 | fi 112 | done 113 | } 114 | 115 | assert_not_has_dir () { 116 | if test -d "$1"; then 117 | fatal "Directory '$1' exists" 118 | fi 119 | } 120 | 121 | assert_file_has_content () { 122 | fpath=$1 123 | shift 124 | for re in "$@"; do 125 | if ! grep -q -e "$re" "$fpath"; then 126 | _fatal_print_file "$fpath" "File '$fpath' doesn't match regexp '$re'" 127 | fi 128 | done 129 | } 130 | 131 | assert_file_has_content_once () { 132 | fpath=$1 133 | shift 134 | for re in "$@"; do 135 | if ! test $(grep -e "$re" "$fpath" | wc -l) = "1"; then 136 | _fatal_print_file "$fpath" "File '$fpath' doesn't match regexp '$re' exactly once" 137 | fi 138 | done 139 | } 140 | 141 | assert_file_has_content_literal () { 142 | fpath=$1; shift 143 | for s in "$@"; do 144 | if ! grep -q -F -e "$s" "$fpath"; then 145 | _fatal_print_file "$fpath" "File '$fpath' doesn't match fixed string list '$s'" 146 | fi 147 | done 148 | } 149 | 150 | assert_file_has_mode () { 151 | mode=$(stat -c '%a' $1) 152 | if [ "$mode" != "$2" ]; then 153 | fatal "File '$1' has wrong mode: expected $2, but got $mode" 154 | fi 155 | } 156 | 157 | assert_symlink_has_content () { 158 | if ! test -L "$1"; then 159 | fatal "File '$1' is not a symbolic link" 160 | fi 161 | if ! readlink "$1" | grep -q -e "$2"; then 162 | _fatal_print_file "$1" "Symbolic link '$1' doesn't match regexp '$2'" 163 | fi 164 | } 165 | 166 | assert_file_empty() { 167 | if test -s "$1"; then 168 | _fatal_print_file "$1" "File '$1' is not empty" 169 | fi 170 | } 171 | 172 | assert_files_equal() { 173 | if ! cmp "$1" "$2"; then 174 | _fatal_print_files "$1" "$2" "File '$1' and '$2' is not equal" 175 | fi 176 | } 177 | 178 | # Use to skip all of these tests 179 | skip() { 180 | echo "1..0 # SKIP" "$@" 181 | exit 0 182 | } 183 | 184 | report_err () { 185 | local exit_status="$?" 186 | { { local BASH_XTRACEFD=3; } 2> /dev/null 187 | echo "Unexpected nonzero exit status $exit_status while running: $BASH_COMMAND" >&2 188 | } 3> /dev/null 189 | } 190 | trap report_err ERR 191 | -------------------------------------------------------------------------------- /tests/libtest.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | # Source library for shell script tests. 4 | # Add non-bubblewrap-specific code to libtest-core.sh instead. 5 | # 6 | # Copyright (C) 2017 Colin Walters 7 | # SPDX-License-Identifier: LGPL-2.0-or-later 8 | # 9 | # This library is free software; you can redistribute it and/or 10 | # modify it under the terms of the GNU Lesser General Public 11 | # License as published by the Free Software Foundation; either 12 | # version 2 of the License, or (at your option) any later version. 13 | # 14 | # This library is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | # Lesser General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Lesser General Public 20 | # License along with this library; if not, write to the 21 | # Free Software Foundation, Inc., 59 Temple Place - Suite 330, 22 | # Boston, MA 02111-1307, USA. 23 | 24 | set -e 25 | 26 | if [ -n "${G_TEST_SRCDIR:-}" ]; then 27 | test_srcdir="${G_TEST_SRCDIR}/tests" 28 | else 29 | test_srcdir=$(dirname "$0") 30 | fi 31 | 32 | if [ -n "${G_TEST_BUILDDIR:-}" ]; then 33 | test_builddir="${G_TEST_BUILDDIR}/tests" 34 | else 35 | test_builddir=$(dirname "$0") 36 | fi 37 | 38 | . "${test_srcdir}/libtest-core.sh" 39 | 40 | # Make sure /sbin/getpcaps etc. are in our PATH even if non-root 41 | PATH="$PATH:/usr/sbin:/sbin" 42 | 43 | tempdir=$(mktemp -d /var/tmp/tap-test.XXXXXX) 44 | touch "${tempdir}/.testtmp" 45 | cleanup() { 46 | if test -n "${TEST_SKIP_CLEANUP:-}"; then 47 | echo "Skipping cleanup of ${tempdir}" 48 | elif test -f "${tempdir}/.testtmp"; then 49 | rm -rf "${tempdir}" 50 | fi 51 | } 52 | trap cleanup EXIT 53 | cd "${tempdir}" 54 | 55 | : "${BWRAP:=bwrap}" 56 | if test -u "$(type -p ${BWRAP})"; then 57 | bwrap_is_suid=true 58 | fi 59 | 60 | FUSE_DIR= 61 | for mp in $(grep " fuse[. ]" /proc/self/mounts | grep "user_id=$(id -u)" | awk '{print $2}'); do 62 | if test -d "$mp"; then 63 | echo "# Using $mp as test fuse mount" 64 | FUSE_DIR="$mp" 65 | break 66 | fi 67 | done 68 | 69 | if test "$(id -u)" = "0"; then 70 | is_uidzero=true 71 | else 72 | is_uidzero=false 73 | fi 74 | 75 | # This is supposed to be an otherwise readable file in an unreadable (by the user) dir 76 | UNREADABLE=/root/.bashrc 77 | if "${is_uidzero}" || test -x "$(dirname "$UNREADABLE")"; then 78 | UNREADABLE= 79 | fi 80 | 81 | # https://github.com/projectatomic/bubblewrap/issues/217 82 | # are we on a merged-/usr system? 83 | if [ /lib -ef /usr/lib ]; then 84 | BWRAP_RO_HOST_ARGS="--ro-bind /usr /usr 85 | --ro-bind /etc /etc 86 | --dir /var/tmp 87 | --symlink usr/lib /lib 88 | --symlink usr/lib64 /lib64 89 | --symlink usr/bin /bin 90 | --symlink usr/sbin /sbin 91 | --proc /proc 92 | --dev /dev" 93 | else 94 | BWRAP_RO_HOST_ARGS="--ro-bind /usr /usr 95 | --ro-bind /etc /etc 96 | --ro-bind /bin /bin 97 | --ro-bind-try /lib /lib 98 | --ro-bind-try /lib64 /lib64 99 | --ro-bind-try /sbin /sbin 100 | --ro-bind-try /nix/store /nix/store 101 | --dir /var/tmp 102 | --proc /proc 103 | --dev /dev" 104 | fi 105 | 106 | # Default arg, bind whole host fs to /, tmpfs on /tmp 107 | RUN="${BWRAP} --bind / / --tmpfs /tmp" 108 | 109 | if [ -z "${BWRAP_MUST_WORK-}" ] && ! $RUN true; then 110 | skip Seems like bwrap is not working at all. Maybe setuid is not working 111 | fi 112 | 113 | extract_child_pid() { 114 | grep child-pid "$1" | sed "s/^.*: \([0-9]*\).*/\1/" 115 | } 116 | -------------------------------------------------------------------------------- /tests/meson.build: -------------------------------------------------------------------------------- 1 | test_programs = [ 2 | ['test-utils', executable( 3 | 'test-utils', 4 | 'test-utils.c', 5 | '../utils.c', 6 | '../utils.h', 7 | dependencies : [selinux_dep], 8 | include_directories : common_include_directories, 9 | )], 10 | ] 11 | 12 | executable( 13 | 'try-syscall', 14 | 'try-syscall.c', 15 | override_options: ['b_sanitize=none'], 16 | ) 17 | 18 | test_scripts = [ 19 | 'test-run.sh', 20 | 'test-seccomp.py', 21 | 'test-specifying-pidns.sh', 22 | 'test-specifying-userns.sh', 23 | ] 24 | 25 | test_env = environment() 26 | test_env.set('BWRAP', bwrap.full_path()) 27 | test_env.set('G_TEST_BUILDDIR', meson.current_build_dir() / '..') 28 | test_env.set('G_TEST_SRCDIR', meson.current_source_dir() / '..') 29 | 30 | foreach pair : test_programs 31 | name = pair[0] 32 | test_program = pair[1] 33 | if meson.version().version_compare('>=0.50.0') 34 | test( 35 | name, 36 | test_program, 37 | env : test_env, 38 | protocol : 'tap', 39 | ) 40 | else 41 | test( 42 | name, 43 | test_program, 44 | env : test_env, 45 | ) 46 | endif 47 | endforeach 48 | 49 | foreach test_script : test_scripts 50 | if test_script.endswith('.py') 51 | interpreter = python 52 | else 53 | interpreter = bash 54 | endif 55 | 56 | if meson.version().version_compare('>=0.50.0') 57 | test( 58 | test_script, 59 | interpreter, 60 | args : [files(test_script)], 61 | env : test_env, 62 | protocol : 'tap', 63 | ) 64 | else 65 | test( 66 | test_script, 67 | interpreter, 68 | args : [files(test_script)], 69 | env : test_env, 70 | ) 71 | endif 72 | endforeach 73 | -------------------------------------------------------------------------------- /tests/test-run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeuo pipefail 4 | 5 | srcd=$(cd $(dirname "$0") && pwd) 6 | 7 | . ${srcd}/libtest.sh 8 | 9 | bn=$(basename "$0") 10 | 11 | test_count=0 12 | ok () { 13 | test_count=$((test_count + 1)) 14 | echo ok $test_count "$@" 15 | } 16 | ok_skip () { 17 | ok "# SKIP" "$@" 18 | } 19 | done_testing () { 20 | echo "1..$test_count" 21 | } 22 | 23 | # Test help 24 | ${BWRAP} --help > help.txt 25 | assert_file_has_content help.txt "usage: ${BWRAP}" 26 | ok "Help works" 27 | 28 | for ALT in "" "--unshare-user-try" "--unshare-pid" "--unshare-user-try --unshare-pid"; do 29 | # Test fuse fs as bind source 30 | if [ "x$FUSE_DIR" != "x" ]; then 31 | $RUN $ALT --proc /proc --dev /dev --bind $FUSE_DIR /tmp/foo true 32 | ok "can bind-mount a FUSE directory with $ALT" 33 | else 34 | ok_skip "no FUSE support" 35 | fi 36 | # no --dev => no devpts => no map_root workaround 37 | $RUN $ALT --proc /proc true 38 | ok "can mount /proc with $ALT" 39 | # No network 40 | $RUN $ALT --unshare-net --proc /proc --dev /dev true 41 | ok "can unshare network, create new /dev with $ALT" 42 | # Unreadable file 43 | echo -n "expect EPERM: " >&2 44 | 45 | # Test caps when bwrap is not setuid 46 | if test -n "${bwrap_is_suid:-}"; then 47 | CAP="--cap-add ALL" 48 | else 49 | CAP="" 50 | fi 51 | 52 | if ! cat /etc/shadow >/dev/null && 53 | $RUN $CAP $ALT --unshare-net --proc /proc --bind /etc/shadow /tmp/foo cat /tmp/foo; then 54 | assert_not_reached Could read /etc/shadow via /tmp/foo bind-mount 55 | fi 56 | 57 | if ! cat /etc/shadow >/dev/null && 58 | $RUN $CAP $ALT --unshare-net --proc /proc --bind /etc/shadow /tmp/foo cat /etc/shadow; then 59 | assert_not_reached Could read /etc/shadow 60 | fi 61 | 62 | ok "cannot read /etc/shadow with $ALT" 63 | # Unreadable dir 64 | if [ "x$UNREADABLE" != "x" ]; then 65 | echo -n "expect EPERM: " >&2 66 | if $RUN $ALT --unshare-net --proc /proc --dev /dev --bind $UNREADABLE /tmp/foo cat /tmp/foo; then 67 | assert_not_reached Could read $UNREADABLE 68 | fi 69 | ok "cannot read $UNREADABLE with $ALT" 70 | else 71 | ok_skip "not sure what unreadable file to use" 72 | fi 73 | 74 | # bind dest in symlink (https://github.com/projectatomic/bubblewrap/pull/119) 75 | $RUN $ALT --dir /tmp/dir --symlink dir /tmp/link --bind /etc /tmp/link true 76 | ok "can bind a destination over a symlink" 77 | done 78 | 79 | # Test symlink behaviour 80 | rm -f ./symlink 81 | $RUN --ro-bind / / --bind "$(pwd)" "$(pwd)" --symlink /dev/null "$(pwd)/symlink" true >&2 82 | readlink ./symlink > target.txt 83 | assert_file_has_content target.txt /dev/null 84 | ok "--symlink works" 85 | $RUN --ro-bind / / --bind "$(pwd)" "$(pwd)" --symlink /dev/null "$(pwd)/symlink" true >&2 86 | ok "--symlink is idempotent" 87 | if $RUN --ro-bind / / --bind "$(pwd)" "$(pwd)" --symlink /dev/full "$(pwd)/symlink" true 2>err.txt; then 88 | fatal "creating a conflicting symlink should have failed" 89 | else 90 | assert_file_has_content err.txt "Can't make symlink .*: existing destination is /dev/null" 91 | fi 92 | ok "--symlink doesn't overwrite a conflicting symlink" 93 | 94 | # Test devices 95 | $RUN --unshare-pid --dev /dev ls -al /dev/{stdin,stdout,stderr,null,random,urandom,fd,core} >/dev/null 96 | ok "all expected devices were created" 97 | 98 | # Test --as-pid-1 99 | $RUN --unshare-pid --as-pid-1 --bind / / bash -c 'echo $$' > as_pid_1.txt 100 | assert_file_has_content as_pid_1.txt "1" 101 | ok "can run as pid 1" 102 | 103 | # Test --info-fd and --json-status-fd 104 | if $RUN --unshare-all --info-fd 42 --json-status-fd 43 -- bash -c 'exit 42' 42>info.json 43>json-status.json 2>err.txt; then 105 | fatal "should have been exit 42" 106 | fi 107 | assert_file_has_content info.json '"child-pid": [0-9]' 108 | assert_file_has_content json-status.json '"child-pid": [0-9]' 109 | assert_file_has_content_literal json-status.json '"exit-code": 42' 110 | ok "info and json-status fd" 111 | 112 | DATA=$($RUN --proc /proc --unshare-all --info-fd 42 --json-status-fd 43 -- bash -c 'stat -L -c "%n %i" /proc/self/ns/*' 42>info.json 43>json-status.json 2>err.txt) 113 | 114 | for NS in "ipc" "mnt" "net" "pid" "uts"; do 115 | 116 | want=$(echo "$DATA" | grep "/proc/self/ns/$NS" | awk '{print $2}') 117 | assert_file_has_content info.json "$want" 118 | assert_file_has_content json-status.json "$want" 119 | done 120 | 121 | ok "namespace id info in info and json-status fd" 122 | 123 | if ! command -v strace >/dev/null || ! strace -h | grep -v -e default | grep -e fault >/dev/null; then 124 | ok_skip "no strace fault injection" 125 | else 126 | ! strace -o /dev/null -f -e trace=prctl -e fault=prctl:when=39 $RUN --die-with-parent --json-status-fd 42 true 42>json-status.json 127 | assert_not_file_has_content json-status.json '"exit-code": [0-9]' 128 | ok "pre-exec failure doesn't include exit-code in json-status" 129 | fi 130 | 131 | notanexecutable=/ 132 | $RUN --json-status-fd 42 $notanexecutable 42>json-status.json || true 133 | assert_not_file_has_content json-status.json '"exit-code": [0-9]' 134 | ok "exec failure doesn't include exit-code in json-status" 135 | 136 | # These tests require --unshare-user 137 | if test -n "${bwrap_is_suid:-}"; then 138 | ok_skip "no --cap-add support" 139 | ok_skip "no --cap-add support" 140 | ok_skip "no --disable-userns" 141 | else 142 | BWRAP_RECURSE="$BWRAP --unshare-user --uid 0 --gid 0 --cap-add ALL --bind / / --bind /proc /proc" 143 | 144 | # $BWRAP May be inaccessible due to the user namespace so use /proc/self/exe 145 | $BWRAP_RECURSE -- /proc/self/exe --unshare-all --bind / / --bind /proc /proc echo hello > recursive_proc.txt 146 | assert_file_has_content recursive_proc.txt "hello" 147 | ok "can mount /proc recursively" 148 | 149 | $BWRAP_RECURSE -- /proc/self/exe --unshare-all ${BWRAP_RO_HOST_ARGS} findmnt > recursive-newroot.txt 150 | assert_file_has_content recursive-newroot.txt "/usr" 151 | ok "can pivot to new rootfs recursively" 152 | 153 | $BWRAP --dev-bind / / -- true 154 | ! $BWRAP --assert-userns-disabled --dev-bind / / -- true 155 | $BWRAP --unshare-user --disable-userns --dev-bind / / -- true 156 | ! $BWRAP --unshare-user --disable-userns --dev-bind / / -- $BWRAP --dev-bind / / -- true 157 | $BWRAP --unshare-user --disable-userns --dev-bind / / -- sh -c "echo 2 > /proc/sys/user/max_user_namespaces || true; ! $BWRAP --unshare-user --dev-bind / / -- true" 158 | $BWRAP --unshare-user --disable-userns --dev-bind / / -- sh -c "echo 100 > /proc/sys/user/max_user_namespaces || true; ! $BWRAP --unshare-user --dev-bind / / -- true" 159 | $BWRAP --unshare-user --disable-userns --dev-bind / / -- sh -c "! $BWRAP --unshare-user --dev-bind / / --assert-userns-disabled -- true" 160 | 161 | $BWRAP_RECURSE --dev-bind / / -- true 162 | ! $BWRAP_RECURSE --assert-userns-disabled --dev-bind / / -- true 163 | $BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- true 164 | ! $BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- /proc/self/exe --dev-bind / / -- true 165 | $BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- sh -c "echo 2 > /proc/sys/user/max_user_namespaces || true; ! $BWRAP --unshare-user --dev-bind / / -- true" 166 | $BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- sh -c "echo 100 > /proc/sys/user/max_user_namespaces || true; ! $BWRAP --unshare-user --dev-bind / / -- true" 167 | $BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- sh -c "! $BWRAP --unshare-user --dev-bind / / --assert-userns-disabled -- true" 168 | 169 | ok "can disable nested userns" 170 | fi 171 | 172 | # Test error prefixing 173 | if $RUN --unshare-pid --bind /source-enoent /dest true 2>err.txt; then 174 | assert_not_reached "bound nonexistent source" 175 | fi 176 | assert_file_has_content err.txt "^bwrap: Can't find source path.*source-enoent" 177 | ok "error prefixing" 178 | 179 | if ! ${is_uidzero}; then 180 | # When invoked as non-root, check that by default we have no caps left 181 | for OPT in "" "--unshare-user-try --as-pid-1" "--unshare-user-try" "--as-pid-1"; do 182 | e=0 183 | $RUN $OPT --unshare-pid getpcaps 1 >&2 2> caps.test || e=$? 184 | sed -e 's/^/# /' < caps.test >&2 185 | test "$e" = 0 186 | assert_not_file_has_content caps.test ': =.*cap' 187 | done 188 | ok "we have no caps as uid != 0" 189 | else 190 | capsh --print | sed -e 's/no-new-privs=0/no-new-privs=1/' > caps.expected 191 | 192 | for OPT in "" "--as-pid-1"; do 193 | $RUN $OPT --unshare-pid capsh --print >caps.test 194 | diff -u caps.expected caps.test 195 | done 196 | # And test that we can drop all, as well as specific caps 197 | $RUN $OPT --cap-drop ALL --unshare-pid capsh --print >caps.test 198 | assert_file_has_content caps.test 'Current: =$' 199 | # Check for dropping kill/fowner (we assume all uid 0 callers have this) 200 | # But we should still have net_bind_service for example 201 | $RUN $OPT --cap-drop CAP_KILL --cap-drop CAP_FOWNER --unshare-pid capsh --print >caps.test 202 | # capsh's output format changed from v2.29 -> drops are now indicated with -eip 203 | if grep 'Current: =.*+eip$' caps.test; then 204 | assert_not_file_has_content caps.test '^Current: =.*cap_kill.*+eip$' 205 | assert_not_file_has_content caps.test '^Current: =.*cap_fowner.*+eip$' 206 | assert_file_has_content caps.test '^Current: =.*cap_net_bind_service.*+eip$' 207 | else 208 | assert_file_has_content caps.test '^Current: =eip.*cap_kill.*-eip$' 209 | assert_file_has_content caps.test '^Current: =eip.*cap_fowner.*-eip$' 210 | assert_not_file_has_content caps.test '^Current: =.*cap_net_bind_service.*-eip$' 211 | fi 212 | ok "we have the expected caps as uid 0" 213 | fi 214 | 215 | # Test --die-with-parent 216 | 217 | cat >lockf-n.py < test.args 263 | printf '%s--dir\0/tmp/hello/world2\0' '' > test.args2 264 | printf '%s--dir\0/tmp/hello/world3\0' '' > test.args3 265 | $RUN --args 3 --args 4 --args 5 /bin/sh -c 'test -d /tmp/hello/world && test -d /tmp/hello/world2 && test -d /tmp/hello/world3' 3 bin/--inadvisable-executable-name-- 270 | echo "echo hello" >> bin/--inadvisable-executable-name-- 271 | chmod +x bin/--inadvisable-executable-name-- 272 | PATH="${srcd}:$PATH" $RUN -- sh -c "echo hello" > stdout 273 | assert_file_has_content stdout hello 274 | ok "we can run with --" 275 | PATH="$(pwd)/bin:$PATH" $RUN -- --inadvisable-executable-name-- > stdout 276 | assert_file_has_content stdout hello 277 | ok "we can run an inadvisable executable name with --" 278 | if $RUN -- --dev-bind /dev /dev sh -c 'echo should not have run'; then 279 | assert_not_reached "'--dev-bind' should have been interpreted as a (silly) executable name" 280 | fi 281 | ok "options like --dev-bind are defanged by --" 282 | 283 | if command -v mktemp > /dev/null; then 284 | tempfile="$(mktemp /tmp/bwrap-test-XXXXXXXX)" 285 | echo "hello" > "$tempfile" 286 | $BWRAP --bind / / cat "$tempfile" > stdout 287 | assert_file_has_content stdout hello 288 | ok "bind-mount of / exposes real /tmp" 289 | $BWRAP --bind / / --bind /tmp /tmp cat "$tempfile" > stdout 290 | assert_file_has_content stdout hello 291 | ok "bind-mount of /tmp exposes real /tmp" 292 | if [ -d /mnt ] && [ ! -L /mnt ]; then 293 | $BWRAP --bind / / --bind /tmp /mnt cat "/mnt/${tempfile#/tmp/}" > stdout 294 | assert_file_has_content stdout hello 295 | ok "bind-mount of /tmp onto /mnt exposes real /tmp" 296 | else 297 | ok_skip "/mnt does not exist or is a symlink" 298 | fi 299 | else 300 | ok_skip "mktemp not found" 301 | ok_skip "mktemp not found" 302 | ok_skip "mktemp not found" 303 | fi 304 | 305 | if $RUN test -d /tmp/oldroot; then 306 | assert_not_reached "/tmp/oldroot should not be visible" 307 | fi 308 | if $RUN test -d /tmp/newroot; then 309 | assert_not_reached "/tmp/newroot should not be visible" 310 | fi 311 | 312 | echo "hello" > input.$$ 313 | $BWRAP --bind / / --bind "$(pwd)" /tmp cat /tmp/input.$$ > stdout 314 | assert_file_has_content stdout hello 315 | if $BWRAP --bind / / --bind "$(pwd)" /tmp test -d /tmp/oldroot; then 316 | assert_not_reached "/tmp/oldroot should not be visible" 317 | fi 318 | if $BWRAP --bind / / --bind "$(pwd)" /tmp test -d /tmp/newroot; then 319 | assert_not_reached "/tmp/newroot should not be visible" 320 | fi 321 | ok "we can mount another directory onto /tmp" 322 | 323 | echo "hello" > input.$$ 324 | $RUN --bind "$(pwd)" /tmp/here cat /tmp/here/input.$$ > stdout 325 | assert_file_has_content stdout hello 326 | if $RUN --bind "$(pwd)" /tmp/here test -d /tmp/oldroot; then 327 | assert_not_reached "/tmp/oldroot should not be visible" 328 | fi 329 | if $RUN --bind "$(pwd)" /tmp/here test -d /tmp/newroot; then 330 | assert_not_reached "/tmp/newroot should not be visible" 331 | fi 332 | ok "we can mount another directory inside /tmp" 333 | 334 | touch some-file 335 | mkdir -p some-dir 336 | rm -fr new-dir-mountpoint 337 | rm -fr new-file-mountpoint 338 | $RUN \ 339 | --bind "$(pwd -P)/some-dir" "$(pwd -P)/new-dir-mountpoint" \ 340 | --bind "$(pwd -P)/some-file" "$(pwd -P)/new-file-mountpoint" \ 341 | true 342 | command stat -c '%a' new-dir-mountpoint > new-dir-permissions 343 | assert_file_has_content new-dir-permissions 755 344 | command stat -c '%a' new-file-mountpoint > new-file-permissions 345 | assert_file_has_content new-file-permissions 444 346 | ok "Files and directories created as mount points have expected permissions" 347 | 348 | 349 | if [ -S /dev/log ]; then 350 | $RUN --bind / / --bind "$(realpath /dev/log)" "$(realpath /dev/log)" true 351 | ok "Can bind-mount a socket (/dev/log) onto a socket" 352 | else 353 | ok_skip "- /dev/log is not a socket, cannot test bubblewrap#409" 354 | fi 355 | 356 | mkdir -p dir-already-existed 357 | chmod 0710 dir-already-existed 358 | mkdir -p dir-already-existed2 359 | chmod 0754 dir-already-existed2 360 | rm -fr new-dir-default-perms 361 | rm -fr new-dir-set-perms 362 | $RUN \ 363 | --perms 1741 --dir "$(pwd -P)/new-dir-set-perms" \ 364 | --dir "$(pwd -P)/dir-already-existed" \ 365 | --perms 0741 --dir "$(pwd -P)/dir-already-existed2" \ 366 | --dir "$(pwd -P)/dir-chmod" \ 367 | --chmod 1755 "$(pwd -P)/dir-chmod" \ 368 | --dir "$(pwd -P)/new-dir-default-perms" \ 369 | true 370 | command stat -c '%a' new-dir-default-perms > new-dir-permissions 371 | assert_file_has_content new-dir-permissions '^755$' 372 | command stat -c '%a' new-dir-set-perms > new-dir-permissions 373 | assert_file_has_content new-dir-permissions '^1741$' 374 | command stat -c '%a' dir-already-existed > dir-permissions 375 | assert_file_has_content dir-permissions '^710$' 376 | command stat -c '%a' dir-already-existed2 > dir-permissions 377 | assert_file_has_content dir-permissions '^754$' 378 | command stat -c '%a' dir-chmod > dir-permissions 379 | assert_file_has_content dir-permissions '^1755$' 380 | ok "Directories created explicitly have expected permissions" 381 | 382 | rm -fr parent 383 | rm -fr parent-of-1777 384 | rm -fr parent-of-0755 385 | rm -fr parent-of-0644 386 | rm -fr parent-of-0750 387 | rm -fr parent-of-0710 388 | rm -fr parent-of-0720 389 | rm -fr parent-of-0640 390 | rm -fr parent-of-0700 391 | rm -fr parent-of-0600 392 | rm -fr parent-of-0705 393 | rm -fr parent-of-0604 394 | rm -fr parent-of-0000 395 | $RUN \ 396 | --dir "$(pwd -P)"/parent/dir \ 397 | --perms 1777 --dir "$(pwd -P)"/parent-of-1777/dir \ 398 | --perms 0755 --dir "$(pwd -P)"/parent-of-0755/dir \ 399 | --perms 0644 --dir "$(pwd -P)"/parent-of-0644/dir \ 400 | --perms 0750 --dir "$(pwd -P)"/parent-of-0750/dir \ 401 | --perms 0710 --dir "$(pwd -P)"/parent-of-0710/dir \ 402 | --perms 0720 --dir "$(pwd -P)"/parent-of-0720/dir \ 403 | --perms 0640 --dir "$(pwd -P)"/parent-of-0640/dir \ 404 | --perms 0700 --dir "$(pwd -P)"/parent-of-0700/dir \ 405 | --perms 0600 --dir "$(pwd -P)"/parent-of-0600/dir \ 406 | --perms 0705 --dir "$(pwd -P)"/parent-of-0705/dir \ 407 | --perms 0604 --dir "$(pwd -P)"/parent-of-0604/dir \ 408 | --perms 0000 --dir "$(pwd -P)"/parent-of-0000/dir \ 409 | true 410 | command stat -c '%a' parent > dir-permissions 411 | assert_file_has_content dir-permissions '^755$' 412 | command stat -c '%a' parent-of-1777 > dir-permissions 413 | assert_file_has_content dir-permissions '^755$' 414 | command stat -c '%a' parent-of-0755 > dir-permissions 415 | assert_file_has_content dir-permissions '^755$' 416 | command stat -c '%a' parent-of-0644 > dir-permissions 417 | assert_file_has_content dir-permissions '^755$' 418 | command stat -c '%a' parent-of-0750 > dir-permissions 419 | assert_file_has_content dir-permissions '^750$' 420 | command stat -c '%a' parent-of-0710 > dir-permissions 421 | assert_file_has_content dir-permissions '^750$' 422 | command stat -c '%a' parent-of-0720 > dir-permissions 423 | assert_file_has_content dir-permissions '^750$' 424 | command stat -c '%a' parent-of-0640 > dir-permissions 425 | assert_file_has_content dir-permissions '^750$' 426 | command stat -c '%a' parent-of-0700 > dir-permissions 427 | assert_file_has_content dir-permissions '^700$' 428 | command stat -c '%a' parent-of-0600 > dir-permissions 429 | assert_file_has_content dir-permissions '^700$' 430 | command stat -c '%a' parent-of-0705 > dir-permissions 431 | assert_file_has_content dir-permissions '^705$' 432 | command stat -c '%a' parent-of-0604 > dir-permissions 433 | assert_file_has_content dir-permissions '^705$' 434 | command stat -c '%a' parent-of-0000 > dir-permissions 435 | assert_file_has_content dir-permissions '^700$' 436 | chmod -R 0700 parent* 437 | rm -fr parent* 438 | ok "Directories created as parents have expected permissions" 439 | 440 | $RUN \ 441 | --perms 01777 --tmpfs "$(pwd -P)" \ 442 | cat /proc/self/mountinfo >&2 443 | $RUN \ 444 | --perms 01777 --tmpfs "$(pwd -P)" \ 445 | stat -c '%a' "$(pwd -P)" > dir-permissions 446 | assert_file_has_content dir-permissions '^1777$' 447 | $RUN \ 448 | --tmpfs "$(pwd -P)" \ 449 | stat -c '%a' "$(pwd -P)" > dir-permissions 450 | assert_file_has_content dir-permissions '^755$' 451 | ok "tmpfs has expected permissions" 452 | 453 | # 1048576 = 1 MiB 454 | if test -n "${bwrap_is_suid:-}"; then 455 | if $RUN --size 1048576 --tmpfs "$(pwd -P)" true; then 456 | assert_not_reached "Should not allow --size --tmpfs when setuid" 457 | fi 458 | ok "--size --tmpfs is not allowed when setuid" 459 | elif df --output=size --block-size=1K "$(pwd -P)" >/dev/null 2>/dev/null; then 460 | $RUN \ 461 | --size 1048576 --tmpfs "$(pwd -P)" \ 462 | df --output=size --block-size=1K "$(pwd -P)" > dir-size 463 | assert_file_has_content dir-size '^ *1024$' 464 | $RUN \ 465 | --size 1048576 --perms 01777 --tmpfs "$(pwd -P)" \ 466 | stat -c '%a' "$(pwd -P)" > dir-permissions 467 | assert_file_has_content dir-permissions '^1777$' 468 | $RUN \ 469 | --size 1048576 --perms 01777 --tmpfs "$(pwd -P)" \ 470 | df --output=size --block-size=1K "$(pwd -P)" > dir-size 471 | assert_file_has_content dir-size '^ *1024$' 472 | $RUN \ 473 | --perms 01777 --size 1048576 --tmpfs "$(pwd -P)" \ 474 | stat -c '%a' "$(pwd -P)" > dir-permissions 475 | assert_file_has_content dir-permissions '^1777$' 476 | $RUN \ 477 | --perms 01777 --size 1048576 --tmpfs "$(pwd -P)" \ 478 | df --output=size --block-size=1K "$(pwd -P)" > dir-size 479 | assert_file_has_content dir-size '^ *1024$' 480 | ok "tmpfs has expected size" 481 | else 482 | $RUN --size 1048576 --tmpfs "$(pwd -P)" true 483 | $RUN --perms 01777 --size 1048576 --tmpfs "$(pwd -P)" true 484 | $RUN --size 1048576 --perms 01777 --tmpfs "$(pwd -P)" true 485 | ok_skip "df is too old, cannot test --size --tmpfs fully" 486 | fi 487 | 488 | $RUN \ 489 | --file 0 /tmp/file \ 490 | stat -c '%a' /tmp/file < /dev/null > file-permissions 491 | assert_file_has_content file-permissions '^666$' 492 | $RUN \ 493 | --perms 0640 --file 0 /tmp/file \ 494 | stat -c '%a' /tmp/file < /dev/null > file-permissions 495 | assert_file_has_content file-permissions '^640$' 496 | $RUN \ 497 | --bind-data 0 /tmp/file \ 498 | stat -c '%a' /tmp/file < /dev/null > file-permissions 499 | assert_file_has_content file-permissions '^600$' 500 | $RUN \ 501 | --perms 0640 --bind-data 0 /tmp/file \ 502 | stat -c '%a' /tmp/file < /dev/null > file-permissions 503 | assert_file_has_content file-permissions '^640$' 504 | $RUN \ 505 | --ro-bind-data 0 /tmp/file \ 506 | stat -c '%a' /tmp/file < /dev/null > file-permissions 507 | assert_file_has_content file-permissions '^600$' 508 | $RUN \ 509 | --perms 0640 --ro-bind-data 0 /tmp/file \ 510 | stat -c '%a' /tmp/file < /dev/null > file-permissions 511 | assert_file_has_content file-permissions '^640$' 512 | ok "files have expected permissions" 513 | 514 | if $RUN --size 0 --tmpfs /tmp/a true; then 515 | assert_not_reached Zero tmpfs size allowed 516 | fi 517 | if $RUN --size 123bogus --tmpfs /tmp/a true; then 518 | assert_not_reached Bogus tmpfs size allowed 519 | fi 520 | if $RUN --size '' --tmpfs /tmp/a true; then 521 | assert_not_reached Empty tmpfs size allowed 522 | fi 523 | if $RUN --size -12345678 --tmpfs /tmp/a true; then 524 | assert_not_reached Negative tmpfs size allowed 525 | fi 526 | if $RUN --size ' -12345678' --tmpfs /tmp/a true; then 527 | assert_not_reached Negative tmpfs size with space allowed 528 | fi 529 | # This is 2^64 530 | if $RUN --size 18446744073709551616 --tmpfs /tmp/a true; then 531 | assert_not_reached Overflowing tmpfs size allowed 532 | fi 533 | # This is 2^63 + 1; note that the current max size is SIZE_MAX/2 534 | if $RUN --size 9223372036854775809 --tmpfs /tmp/a true; then 535 | assert_not_reached Too-large tmpfs size allowed 536 | fi 537 | ok "bogus tmpfs size not allowed" 538 | 539 | if $RUN --perms 0640 --perms 0640 --tmpfs /tmp/a true; then 540 | assert_not_reached Multiple perms options allowed 541 | fi 542 | if $RUN --size 1048576 --size 1048576 --tmpfs /tmp/a true; then 543 | assert_not_reached Multiple perms options allowed 544 | fi 545 | ok "--perms and --size only allowed once" 546 | 547 | 548 | FOO= BAR=baz $RUN --setenv FOO bar sh -c 'echo "$FOO$BAR"' > stdout 549 | assert_file_has_content stdout barbaz 550 | FOO=wrong BAR=baz $RUN --setenv FOO bar sh -c 'echo "$FOO$BAR"' > stdout 551 | assert_file_has_content stdout barbaz 552 | FOO=wrong BAR=baz $RUN --unsetenv FOO sh -c 'printf "%s%s" "$FOO" "$BAR"' > stdout 553 | printf baz > reference 554 | assert_files_equal stdout reference 555 | FOO=wrong BAR=wrong $RUN --clearenv /usr/bin/env > stdout 556 | echo "PWD=$(pwd -P)" > reference 557 | assert_files_equal stdout reference 558 | ok "environment manipulation" 559 | 560 | $RUN sh -c 'echo $0' > stdout 561 | assert_file_has_content stdout sh 562 | $RUN --argv0 sh sh -c 'echo $0' > stdout 563 | assert_file_has_content stdout sh 564 | $RUN --argv0 right sh -c 'echo $0' > stdout 565 | assert_file_has_content stdout right 566 | ok "argv0 manipulation" 567 | 568 | echo "foobar" > file-data 569 | $RUN --proc /proc --dev /dev --bind / / --bind-fd 100 /tmp cat /tmp/file-data 100< . > stdout 570 | assert_file_has_content stdout foobar 571 | 572 | ok "bind-fd" 573 | 574 | $RUN --chdir / --chdir / true > stdout 2>&1 575 | assert_file_has_content stdout '^bwrap: Only the last --chdir option will take effect$' 576 | ok "warning logged for redundant --chdir" 577 | 578 | $RUN --level-prefix --chdir / --chdir / true > stdout 2>&1 579 | assert_file_has_content stdout '^<4>bwrap: Only the last --chdir option will take effect$' 580 | ok "--level-prefix" 581 | 582 | if test -n "${bwrap_is_suid:-}"; then 583 | ok_skip "no --overlay support" 584 | ok_skip "no --overlay support" 585 | ok_skip "no --tmp-overlay support" 586 | ok_skip "no --ro-overlay support" 587 | ok_skip "no --overlay-src support" 588 | else 589 | mkdir lower1 lower2 upper work 590 | printf 1 > lower1/a 591 | printf 2 > lower1/b 592 | printf 3 > lower2/b 593 | printf 4 > upper/a 594 | 595 | # Check if unprivileged overlayfs is available 596 | if ! unshare -rm mount -t overlay -o lowerdir=lower1,upperdir=upper,workdir=work,userxattr overlay lower2; then 597 | ok_skip "no kernel support for unprivileged overlayfs" 598 | ok_skip "no kernel support for unprivileged overlayfs" 599 | ok_skip "no kernel support for unprivileged overlayfs" 600 | ok_skip "no kernel support for unprivileged overlayfs" 601 | ok_skip "no kernel support for unprivileged overlayfs" 602 | else 603 | 604 | # Test --overlay 605 | if $RUN --overlay upper work /tmp true 2>err.txt; then 606 | assert_not_reached At least one --overlay-src not required 607 | fi 608 | assert_file_has_content err.txt "^bwrap: --overlay requires at least one --overlay-src" 609 | $RUN --overlay-src lower1 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/a > stdout 610 | assert_file_has_content stdout '^4$' 611 | $RUN --overlay-src lower1 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/b > stdout 612 | assert_file_has_content stdout '^2$' 613 | $RUN --overlay-src lower1 --overlay-src lower2 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/a > stdout 614 | assert_file_has_content stdout '^4$' 615 | $RUN --overlay-src lower1 --overlay-src lower2 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/b > stdout 616 | assert_file_has_content stdout '^3$' 617 | $RUN --overlay-src lower1 --overlay-src lower2 --overlay upper work /tmp/x/y/z sh -c 'printf 5 > /tmp/x/y/z/b; cat /tmp/x/y/z/b' > stdout 618 | assert_file_has_content stdout '^5$' 619 | assert_file_has_content upper/b '^5$' 620 | ok "--overlay" 621 | 622 | # Test --overlay path escaping 623 | # Coincidentally, ":,\ is the face I make contemplating anyone who might 624 | # need this functionality, not that that's going to stop me from supporting 625 | # it. 626 | mkdir 'lower ":,\' 'upper ":,\' 'work ":,\' 627 | printf 1 > 'lower ":,\'/a 628 | $RUN --overlay-src 'lower ":,\' --overlay 'upper ":,\' 'work ":,\' /tmp/x sh -c 'cat /tmp/x/a; printf 2 > /tmp/x/a; cat /tmp/x/a' > stdout 629 | assert_file_has_content stdout '^12$' 630 | assert_file_has_content 'lower ":,\'/a '^1$' 631 | assert_file_has_content 'upper ":,\'/a '^2$' 632 | ok "--overlay path escaping" 633 | 634 | # Test --tmp-overlay 635 | printf 1 > lower1/a 636 | printf 2 > lower1/b 637 | printf 3 > lower2/b 638 | if $RUN --tmp-overlay /tmp true 2>err.txt; then 639 | assert_not_reached At least one --overlay-src not required 640 | fi 641 | assert_file_has_content err.txt "^bwrap: --tmp-overlay requires at least one --overlay-src" 642 | $RUN --overlay-src lower1 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/a > stdout 643 | assert_file_has_content stdout '^1$' 644 | $RUN --overlay-src lower1 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/b > stdout 645 | assert_file_has_content stdout '^2$' 646 | $RUN --overlay-src lower1 --overlay-src lower2 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/a > stdout 647 | assert_file_has_content stdout '^1$' 648 | $RUN --overlay-src lower1 --overlay-src lower2 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/b > stdout 649 | assert_file_has_content stdout '^3$' 650 | $RUN --overlay-src lower1 --overlay-src lower2 --tmp-overlay /tmp/x/y/z sh -c 'printf 4 > /tmp/x/y/z/b; cat /tmp/x/y/z/b' > stdout 651 | assert_file_has_content stdout '^4$' 652 | $RUN --overlay-src lower1 --tmp-overlay /tmp/x --overlay-src lower2 --tmp-overlay /tmp/y sh -c 'cat /tmp/x/b; printf 4 > /tmp/x/b; cat /tmp/x/b; cat /tmp/y/b' > stdout 653 | assert_file_has_content stdout '^243$' 654 | assert_file_has_content lower1/b '^2$' 655 | assert_file_has_content lower2/b '^3$' 656 | ok "--tmp-overlay" 657 | 658 | # Test --ro-overlay 659 | printf 1 > lower1/a 660 | printf 2 > lower1/b 661 | printf 3 > lower2/b 662 | if $RUN --ro-overlay /tmp true 2>err.txt; then 663 | assert_not_reached At least two --overlay-src not required 664 | fi 665 | assert_file_has_content err.txt "^bwrap: --ro-overlay requires at least two --overlay-src" 666 | if $RUN --overlay-src lower1 --ro-overlay /tmp true 2>err.txt; then 667 | assert_not_reached At least two --overlay-src not required 668 | fi 669 | assert_file_has_content err.txt "^bwrap: --ro-overlay requires at least two --overlay-src" 670 | $RUN --overlay-src lower1 --overlay-src lower2 --ro-overlay /tmp/x/y/z cat /tmp/x/y/z/a > stdout 671 | assert_file_has_content stdout '^1$' 672 | $RUN --overlay-src lower1 --overlay-src lower2 --ro-overlay /tmp/x/y/z cat /tmp/x/y/z/b > stdout 673 | assert_file_has_content stdout '^3$' 674 | $RUN --overlay-src lower1 --overlay-src lower2 --ro-overlay /tmp/x/y/z sh -c 'printf 4 > /tmp/x/y/z/b; cat /tmp/x/y/z/b' > stdout 675 | assert_file_has_content stdout '^3$' 676 | ok "--ro-overlay" 677 | 678 | # Test --overlay-src restrictions 679 | if $RUN --overlay-src /tmp true 2>err.txt; then 680 | assert_not_reached Trailing --overlay-src allowed 681 | fi 682 | assert_file_has_content err.txt "^bwrap: --overlay-src must be followed by another --overlay-src or one of --overlay, --tmp-overlay, or --ro-overlay" 683 | if $RUN --overlay-src /tmp --chdir / true 2>err.txt; then 684 | assert_not_reached --overlay-src allowed to precede non-overlay options 685 | fi 686 | assert_file_has_content err.txt "^bwrap: --overlay-src must be followed by another --overlay-src or one of --overlay, --tmp-overlay, or --ro-overlay" 687 | ok "--overlay-src restrictions" 688 | 689 | fi 690 | fi 691 | 692 | done_testing 693 | -------------------------------------------------------------------------------- /tests/test-seccomp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright 2021 Simon McVittie 3 | # SPDX-License-Identifier: LGPL-2.0-or-later 4 | 5 | import errno 6 | import logging 7 | import os 8 | import subprocess 9 | import sys 10 | import tempfile 11 | import termios 12 | import unittest 13 | 14 | try: 15 | import seccomp 16 | except ImportError: 17 | print('1..0 # SKIP cannot import seccomp Python module') 18 | sys.exit(0) 19 | 20 | 21 | # This is the @default set from systemd as of 2021-10-11 22 | DEFAULT_SET = set(''' 23 | brk 24 | cacheflush 25 | clock_getres 26 | clock_getres_time64 27 | clock_gettime 28 | clock_gettime64 29 | clock_nanosleep 30 | clock_nanosleep_time64 31 | execve 32 | exit 33 | exit_group 34 | futex 35 | futex_time64 36 | get_robust_list 37 | get_thread_area 38 | getegid 39 | getegid32 40 | geteuid 41 | geteuid32 42 | getgid 43 | getgid32 44 | getgroups 45 | getgroups32 46 | getpgid 47 | getpgrp 48 | getpid 49 | getppid 50 | getrandom 51 | getresgid 52 | getresgid32 53 | getresuid 54 | getresuid32 55 | getrlimit 56 | getsid 57 | gettid 58 | gettimeofday 59 | getuid 60 | getuid32 61 | membarrier 62 | mmap 63 | mmap2 64 | munmap 65 | nanosleep 66 | pause 67 | prlimit64 68 | restart_syscall 69 | rseq 70 | rt_sigreturn 71 | sched_getaffinity 72 | sched_yield 73 | set_robust_list 74 | set_thread_area 75 | set_tid_address 76 | set_tls 77 | sigreturn 78 | time 79 | ugetrlimit 80 | '''.split()) 81 | 82 | # This is the @basic-io set from systemd 83 | BASIC_IO_SET = set(''' 84 | _llseek 85 | close 86 | close_range 87 | dup 88 | dup2 89 | dup3 90 | lseek 91 | pread64 92 | preadv 93 | preadv2 94 | pwrite64 95 | pwritev 96 | pwritev2 97 | read 98 | readv 99 | write 100 | writev 101 | '''.split()) 102 | 103 | # This is the @filesystem-io set from systemd 104 | FILESYSTEM_SET = set(''' 105 | access 106 | chdir 107 | chmod 108 | close 109 | creat 110 | faccessat 111 | faccessat2 112 | fallocate 113 | fchdir 114 | fchmod 115 | fchmodat 116 | fcntl 117 | fcntl64 118 | fgetxattr 119 | flistxattr 120 | fremovexattr 121 | fsetxattr 122 | fstat 123 | fstat64 124 | fstatat64 125 | fstatfs 126 | fstatfs64 127 | ftruncate 128 | ftruncate64 129 | futimesat 130 | getcwd 131 | getdents 132 | getdents64 133 | getxattr 134 | inotify_add_watch 135 | inotify_init 136 | inotify_init1 137 | inotify_rm_watch 138 | lgetxattr 139 | link 140 | linkat 141 | listxattr 142 | llistxattr 143 | lremovexattr 144 | lsetxattr 145 | lstat 146 | lstat64 147 | mkdir 148 | mkdirat 149 | mknod 150 | mknodat 151 | newfstatat 152 | oldfstat 153 | oldlstat 154 | oldstat 155 | open 156 | openat 157 | openat2 158 | readlink 159 | readlinkat 160 | removexattr 161 | rename 162 | renameat 163 | renameat2 164 | rmdir 165 | setxattr 166 | stat 167 | stat64 168 | statfs 169 | statfs64 170 | statx 171 | symlink 172 | symlinkat 173 | truncate 174 | truncate64 175 | unlink 176 | unlinkat 177 | utime 178 | utimensat 179 | utimensat_time64 180 | utimes 181 | '''.split()) 182 | 183 | # Miscellaneous syscalls used during process startup, at least on x86_64 184 | ALLOWED = DEFAULT_SET | BASIC_IO_SET | FILESYSTEM_SET | set(''' 185 | arch_prctl 186 | ioctl 187 | madvise 188 | mprotect 189 | mremap 190 | prctl 191 | readdir 192 | umask 193 | '''.split()) 194 | 195 | # Syscalls we will try to use, expecting them to be either allowed or 196 | # blocked by our allow and/or deny lists 197 | TRY_SYSCALLS = [ 198 | 'chmod', 199 | 'chroot', 200 | 'clone3', 201 | 'ioctl TIOCNOTTY', 202 | 'ioctl TIOCSTI CVE-2019-10063', 203 | 'ioctl TIOCSTI', 204 | 'listen', 205 | 'prctl', 206 | ] 207 | 208 | 209 | class Test(unittest.TestCase): 210 | def setUp(self) -> None: 211 | here = os.path.dirname(os.path.abspath(__file__)) 212 | 213 | if 'G_TEST_SRCDIR' in os.environ: 214 | self.test_srcdir = os.getenv('G_TEST_SRCDIR') + '/tests' 215 | else: 216 | self.test_srcdir = here 217 | 218 | if 'G_TEST_BUILDDIR' in os.environ: 219 | self.test_builddir = os.getenv('G_TEST_BUILDDIR') + '/tests' 220 | else: 221 | self.test_builddir = here 222 | 223 | self.bwrap = os.getenv('BWRAP', 'bwrap') 224 | self.try_syscall = os.path.join(self.test_builddir, 'try-syscall') 225 | 226 | completed = subprocess.run( 227 | [ 228 | self.bwrap, 229 | '--ro-bind', '/', '/', 230 | 'true', 231 | ], 232 | stdin=subprocess.DEVNULL, 233 | stdout=subprocess.DEVNULL, 234 | stderr=2, 235 | ) 236 | 237 | if completed.returncode != 0: 238 | raise unittest.SkipTest( 239 | 'cannot run bwrap (does it need to be setuid?)' 240 | ) 241 | 242 | def tearDown(self) -> None: 243 | pass 244 | 245 | def test_no_seccomp(self) -> None: 246 | for syscall in TRY_SYSCALLS: 247 | print('# {} without seccomp'.format(syscall)) 248 | completed = subprocess.run( 249 | [ 250 | self.bwrap, 251 | '--ro-bind', '/', '/', 252 | self.try_syscall, syscall, 253 | ], 254 | stdin=subprocess.DEVNULL, 255 | stdout=subprocess.DEVNULL, 256 | stderr=2, 257 | ) 258 | 259 | if ( 260 | syscall == 'ioctl TIOCSTI CVE-2019-10063' 261 | and completed.returncode == errno.ENOENT 262 | ): 263 | print('# Cannot test 64-bit syscall parameter on 32-bit') 264 | continue 265 | 266 | if syscall == 'clone3': 267 | # If the kernel supports it, we didn't block it so 268 | # it fails with EFAULT. If the kernel doesn't support it, 269 | # it'll fail with ENOSYS instead. 270 | self.assertIn( 271 | completed.returncode, 272 | (errno.ENOSYS, errno.EFAULT), 273 | ) 274 | elif syscall.startswith('ioctl') or syscall == 'listen': 275 | self.assertEqual(completed.returncode, errno.EBADF) 276 | else: 277 | self.assertEqual(completed.returncode, errno.EFAULT) 278 | 279 | def test_seccomp_allowlist(self) -> None: 280 | with tempfile.TemporaryFile() as allowlist_temp: 281 | allowlist = seccomp.SyscallFilter(seccomp.ERRNO(errno.ENOSYS)) 282 | 283 | if os.uname().machine == 'x86_64': 284 | # Allow Python and try-syscall to be different word sizes 285 | allowlist.add_arch(seccomp.Arch.X86) 286 | 287 | for syscall in ALLOWED: 288 | try: 289 | allowlist.add_rule(seccomp.ALLOW, syscall) 290 | except Exception as e: 291 | print('# Cannot add {} to allowlist: {!r}'.format(syscall, e)) 292 | 293 | allowlist.export_bpf(allowlist_temp) 294 | 295 | for syscall in TRY_SYSCALLS: 296 | print('# allowlist vs. {}'.format(syscall)) 297 | allowlist_temp.seek(0, os.SEEK_SET) 298 | 299 | completed = subprocess.run( 300 | [ 301 | self.bwrap, 302 | '--ro-bind', '/', '/', 303 | '--seccomp', str(allowlist_temp.fileno()), 304 | self.try_syscall, syscall, 305 | ], 306 | pass_fds=(allowlist_temp.fileno(),), 307 | stdin=subprocess.DEVNULL, 308 | stdout=subprocess.DEVNULL, 309 | stderr=2, 310 | ) 311 | 312 | if ( 313 | syscall == 'ioctl TIOCSTI CVE-2019-10063' 314 | and completed.returncode == errno.ENOENT 315 | ): 316 | print('# Cannot test 64-bit syscall parameter on 32-bit') 317 | continue 318 | 319 | if syscall.startswith('ioctl'): 320 | # We allow this, so it is executed (and in this simple 321 | # example, immediately fails) 322 | self.assertEqual(completed.returncode, errno.EBADF) 323 | elif syscall in ('chroot', 'listen', 'clone3'): 324 | # We don't allow these, so they fail with ENOSYS. 325 | # clone3 might also be failing with ENOSYS because 326 | # the kernel genuinely doesn't support it. 327 | self.assertEqual(completed.returncode, errno.ENOSYS) 328 | else: 329 | # We allow this, so it is executed (and in this simple 330 | # example, immediately fails) 331 | self.assertEqual(completed.returncode, errno.EFAULT) 332 | 333 | def test_seccomp_denylist(self) -> None: 334 | with tempfile.TemporaryFile() as denylist_temp: 335 | denylist = seccomp.SyscallFilter(seccomp.ALLOW) 336 | 337 | if os.uname().machine == 'x86_64': 338 | # Allow Python and try-syscall to be different word sizes 339 | denylist.add_arch(seccomp.Arch.X86) 340 | 341 | # Using ECONNREFUSED here because it's unlikely that any of 342 | # these syscalls will legitimately fail with that code, so 343 | # if they fail like this, it will be as a result of seccomp. 344 | denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chmod') 345 | denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chroot') 346 | denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'prctl') 347 | denylist.add_rule( 348 | seccomp.ERRNO(errno.ECONNREFUSED), 'ioctl', 349 | seccomp.Arg(1, seccomp.MASKED_EQ, 0xffffffff, termios.TIOCSTI), 350 | ) 351 | 352 | denylist.export_bpf(denylist_temp) 353 | 354 | for syscall in TRY_SYSCALLS: 355 | print('# denylist vs. {}'.format(syscall)) 356 | denylist_temp.seek(0, os.SEEK_SET) 357 | 358 | completed = subprocess.run( 359 | [ 360 | self.bwrap, 361 | '--ro-bind', '/', '/', 362 | '--seccomp', str(denylist_temp.fileno()), 363 | self.try_syscall, syscall, 364 | ], 365 | pass_fds=(denylist_temp.fileno(),), 366 | stdin=subprocess.DEVNULL, 367 | stdout=subprocess.DEVNULL, 368 | stderr=2, 369 | ) 370 | 371 | if ( 372 | syscall == 'ioctl TIOCSTI CVE-2019-10063' 373 | and completed.returncode == errno.ENOENT 374 | ): 375 | print('# Cannot test 64-bit syscall parameter on 32-bit') 376 | continue 377 | 378 | if syscall == 'clone3': 379 | # If the kernel supports it, we didn't block it so 380 | # it fails with EFAULT. If the kernel doesn't support it, 381 | # it'll fail with ENOSYS instead. 382 | self.assertIn( 383 | completed.returncode, 384 | (errno.ENOSYS, errno.EFAULT), 385 | ) 386 | elif syscall in ('ioctl TIOCNOTTY', 'listen'): 387 | # Not on the denylist 388 | self.assertEqual(completed.returncode, errno.EBADF) 389 | else: 390 | # We blocked all of these 391 | self.assertEqual(completed.returncode, errno.ECONNREFUSED) 392 | 393 | def test_seccomp_stacked(self, allowlist_first=False) -> None: 394 | with tempfile.TemporaryFile( 395 | ) as allowlist_temp, tempfile.TemporaryFile( 396 | ) as denylist_temp: 397 | # This filter is a simplified version of what Flatpak wants 398 | 399 | allowlist = seccomp.SyscallFilter(seccomp.ERRNO(errno.ENOSYS)) 400 | denylist = seccomp.SyscallFilter(seccomp.ALLOW) 401 | 402 | if os.uname().machine == 'x86_64': 403 | # Allow Python and try-syscall to be different word sizes 404 | allowlist.add_arch(seccomp.Arch.X86) 405 | denylist.add_arch(seccomp.Arch.X86) 406 | 407 | for syscall in ALLOWED: 408 | try: 409 | allowlist.add_rule(seccomp.ALLOW, syscall) 410 | except Exception as e: 411 | print('# Cannot add {} to allowlist: {!r}'.format(syscall, e)) 412 | 413 | denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chmod') 414 | denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chroot') 415 | denylist.add_rule( 416 | seccomp.ERRNO(errno.ECONNREFUSED), 'ioctl', 417 | seccomp.Arg(1, seccomp.MASKED_EQ, 0xffffffff, termios.TIOCSTI), 418 | ) 419 | 420 | # All seccomp programs except the last must allow prctl(), 421 | # because otherwise we wouldn't be able to add the remaining 422 | # seccomp programs. We document that the last program can 423 | # block prctl, so test that. 424 | if allowlist_first: 425 | denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'prctl') 426 | 427 | allowlist.export_bpf(allowlist_temp) 428 | denylist.export_bpf(denylist_temp) 429 | 430 | for syscall in TRY_SYSCALLS: 431 | print('# stacked vs. {}'.format(syscall)) 432 | allowlist_temp.seek(0, os.SEEK_SET) 433 | denylist_temp.seek(0, os.SEEK_SET) 434 | 435 | if allowlist_first: 436 | fds = [allowlist_temp.fileno(), denylist_temp.fileno()] 437 | else: 438 | fds = [denylist_temp.fileno(), allowlist_temp.fileno()] 439 | 440 | completed = subprocess.run( 441 | [ 442 | self.bwrap, 443 | '--ro-bind', '/', '/', 444 | '--add-seccomp-fd', str(fds[0]), 445 | '--add-seccomp-fd', str(fds[1]), 446 | self.try_syscall, syscall, 447 | ], 448 | pass_fds=fds, 449 | stdin=subprocess.DEVNULL, 450 | stdout=subprocess.DEVNULL, 451 | stderr=2, 452 | ) 453 | 454 | if ( 455 | syscall == 'ioctl TIOCSTI CVE-2019-10063' 456 | and completed.returncode == errno.ENOENT 457 | ): 458 | print('# Cannot test 64-bit syscall parameter on 32-bit') 459 | continue 460 | 461 | if syscall == 'ioctl TIOCNOTTY': 462 | # Not denied by the denylist, and allowed by the allowlist 463 | self.assertEqual(completed.returncode, errno.EBADF) 464 | elif syscall in ('clone3', 'listen'): 465 | # We didn't deny these, so the denylist has no effect 466 | # and we fall back to the allowlist, which doesn't 467 | # include them either. 468 | # clone3 might also be failing with ENOSYS because 469 | # the kernel genuinely doesn't support it. 470 | self.assertEqual(completed.returncode, errno.ENOSYS) 471 | elif syscall == 'chroot': 472 | # This is denied by the denylist *and* not allowed by 473 | # the allowlist. The result depends which one we added 474 | # first: the most-recently-added filter "wins". 475 | if allowlist_first: 476 | self.assertEqual( 477 | completed.returncode, 478 | errno.ECONNREFUSED, 479 | ) 480 | else: 481 | self.assertEqual(completed.returncode, errno.ENOSYS) 482 | elif syscall == 'prctl': 483 | # We can only put this on the denylist if the denylist 484 | # is the last to be added. 485 | if allowlist_first: 486 | self.assertEqual( 487 | completed.returncode, 488 | errno.ECONNREFUSED, 489 | ) 490 | else: 491 | self.assertEqual(completed.returncode, errno.EFAULT) 492 | else: 493 | # chmod is allowed by the allowlist but blocked by the 494 | # denylist. Denying takes precedence over allowing, 495 | # regardless of order. 496 | self.assertEqual(completed.returncode, errno.ECONNREFUSED) 497 | 498 | def test_seccomp_stacked_allowlist_first(self) -> None: 499 | self.test_seccomp_stacked(allowlist_first=True) 500 | 501 | def test_seccomp_invalid(self) -> None: 502 | with tempfile.TemporaryFile( 503 | ) as allowlist_temp, tempfile.TemporaryFile( 504 | ) as denylist_temp: 505 | completed = subprocess.run( 506 | [ 507 | self.bwrap, 508 | '--ro-bind', '/', '/', 509 | '--add-seccomp-fd', '-1', 510 | 'true', 511 | ], 512 | stdin=subprocess.DEVNULL, 513 | stdout=subprocess.DEVNULL, 514 | stderr=subprocess.PIPE, 515 | ) 516 | self.assertIn(b'bwrap: Invalid fd: -1\n', completed.stderr) 517 | self.assertEqual(completed.returncode, 1) 518 | 519 | completed = subprocess.run( 520 | [ 521 | self.bwrap, 522 | '--ro-bind', '/', '/', 523 | '--seccomp', '0a', 524 | 'true', 525 | ], 526 | stdin=subprocess.DEVNULL, 527 | stdout=subprocess.DEVNULL, 528 | stderr=subprocess.PIPE, 529 | ) 530 | self.assertIn(b'bwrap: Invalid fd: 0a\n', completed.stderr) 531 | self.assertEqual(completed.returncode, 1) 532 | 533 | completed = subprocess.run( 534 | [ 535 | self.bwrap, 536 | '--ro-bind', '/', '/', 537 | '--add-seccomp-fd', str(denylist_temp.fileno()), 538 | '--seccomp', str(allowlist_temp.fileno()), 539 | 'true', 540 | ], 541 | pass_fds=(allowlist_temp.fileno(), denylist_temp.fileno()), 542 | stdin=subprocess.DEVNULL, 543 | stdout=subprocess.DEVNULL, 544 | stderr=subprocess.PIPE, 545 | ) 546 | self.assertIn( 547 | b'bwrap: --seccomp cannot be combined with --add-seccomp-fd\n', 548 | completed.stderr, 549 | ) 550 | self.assertEqual(completed.returncode, 1) 551 | 552 | completed = subprocess.run( 553 | [ 554 | self.bwrap, 555 | '--ro-bind', '/', '/', 556 | '--seccomp', str(allowlist_temp.fileno()), 557 | '--add-seccomp-fd', str(denylist_temp.fileno()), 558 | 'true', 559 | ], 560 | pass_fds=(allowlist_temp.fileno(), denylist_temp.fileno()), 561 | stdin=subprocess.DEVNULL, 562 | stdout=subprocess.DEVNULL, 563 | stderr=subprocess.PIPE, 564 | ) 565 | self.assertIn( 566 | b'--add-seccomp-fd cannot be combined with --seccomp', 567 | completed.stderr, 568 | ) 569 | self.assertEqual(completed.returncode, 1) 570 | 571 | completed = subprocess.run( 572 | [ 573 | self.bwrap, 574 | '--ro-bind', '/', '/', 575 | '--add-seccomp-fd', str(allowlist_temp.fileno()), 576 | '--add-seccomp-fd', str(allowlist_temp.fileno()), 577 | 'true', 578 | ], 579 | pass_fds=(allowlist_temp.fileno(), allowlist_temp.fileno()), 580 | stdin=subprocess.DEVNULL, 581 | stdout=subprocess.DEVNULL, 582 | stderr=subprocess.PIPE, 583 | ) 584 | self.assertIn( 585 | b"bwrap: Can't read seccomp data: ", 586 | completed.stderr, 587 | ) 588 | self.assertEqual(completed.returncode, 1) 589 | 590 | allowlist_temp.write(b'\x01') 591 | allowlist_temp.seek(0, os.SEEK_SET) 592 | completed = subprocess.run( 593 | [ 594 | self.bwrap, 595 | '--ro-bind', '/', '/', 596 | '--add-seccomp-fd', str(denylist_temp.fileno()), 597 | '--add-seccomp-fd', str(allowlist_temp.fileno()), 598 | 'true', 599 | ], 600 | pass_fds=(allowlist_temp.fileno(), denylist_temp.fileno()), 601 | stdin=subprocess.DEVNULL, 602 | stdout=subprocess.DEVNULL, 603 | stderr=subprocess.PIPE, 604 | ) 605 | self.assertIn( 606 | b'bwrap: Invalid seccomp data, must be multiple of 8\n', 607 | completed.stderr, 608 | ) 609 | self.assertEqual(completed.returncode, 1) 610 | 611 | 612 | def main(): 613 | logging.basicConfig(level=logging.DEBUG) 614 | 615 | try: 616 | from tap.runner import TAPTestRunner 617 | except ImportError: 618 | TAPTestRunner = None # type: ignore 619 | 620 | if TAPTestRunner is not None: 621 | runner = TAPTestRunner() 622 | runner.set_stream(True) 623 | unittest.main(testRunner=runner) 624 | else: 625 | print('# tap.runner not available, using simple TAP output') 626 | print('1..1') 627 | program = unittest.main(exit=False) 628 | if program.result.wasSuccessful(): 629 | print('ok 1 - %r' % program.result) 630 | else: 631 | print('not ok 1 - %r' % program.result) 632 | sys.exit(1) 633 | 634 | if __name__ == '__main__': 635 | main() 636 | -------------------------------------------------------------------------------- /tests/test-specifying-pidns.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeuo pipefail 4 | 5 | srcd=$(cd $(dirname "$0") && pwd) 6 | . "${srcd}/libtest.sh" 7 | 8 | echo "1..1" 9 | 10 | # This test needs user namespaces 11 | if test -n "${bwrap_is_suid:-}"; then 12 | echo "ok - # SKIP no setuid support for --unshare-user" 13 | else 14 | mkfifo donepipe 15 | $RUN --info-fd 42 --unshare-user --unshare-pid sh -c 'readlink /proc/self/ns/pid > sandbox-pidns; cat < donepipe' >/dev/null 42>info.json & 16 | while ! test -f sandbox-pidns; do sleep 1; done 17 | SANDBOX1PID=$(extract_child_pid info.json) 18 | 19 | ASAN_OPTIONS=detect_leaks=0 LSAN_OPTIONS=detect_leaks=0 \ 20 | $RUN --userns 11 --pidns 12 readlink /proc/self/ns/pid > sandbox2-pidns 11< /proc/$SANDBOX1PID/ns/user 12< /proc/$SANDBOX1PID/ns/pid 21 | echo foo > donepipe 22 | 23 | assert_files_equal sandbox-pidns sandbox2-pidns 24 | 25 | rm donepipe info.json sandbox-pidns 26 | 27 | echo "ok - Test --pidns" 28 | fi 29 | -------------------------------------------------------------------------------- /tests/test-specifying-userns.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeuo pipefail 4 | 5 | srcd=$(cd $(dirname "$0") && pwd) 6 | . "${srcd}/libtest.sh" 7 | 8 | echo "1..1" 9 | 10 | # This test needs user namespaces 11 | if test -n "${bwrap_is_suid:-}"; then 12 | echo "ok - # SKIP no setuid support for --unshare-user" 13 | else 14 | mkfifo donepipe 15 | 16 | $RUN --info-fd 42 --unshare-user sh -c 'readlink /proc/self/ns/user > sandbox-userns; cat < donepipe' >/dev/null 42>info.json & 17 | while ! test -f sandbox-userns; do sleep 1; done 18 | SANDBOX1PID=$(extract_child_pid info.json) 19 | 20 | $RUN --userns 11 readlink /proc/self/ns/user > sandbox2-userns 11< /proc/$SANDBOX1PID/ns/user 21 | echo foo > donepipe 22 | 23 | assert_files_equal sandbox-userns sandbox2-userns 24 | 25 | rm donepipe info.json sandbox-userns 26 | 27 | echo "ok - Test --userns" 28 | fi 29 | -------------------------------------------------------------------------------- /tests/test-utils.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019-2021 Collabora Ltd. 3 | * 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library. If not, see . 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | #include "utils.h" 25 | 26 | /* A small implementation of TAP */ 27 | static unsigned int test_number = 0; 28 | 29 | __attribute__((format(printf, 1, 2))) 30 | static void 31 | ok (const char *format, ...) 32 | { 33 | va_list ap; 34 | 35 | printf ("ok %u - ", ++test_number); 36 | va_start (ap, format); 37 | vprintf (format, ap); 38 | va_end (ap); 39 | printf ("\n"); 40 | } 41 | 42 | /* for simplicity we always die immediately on failure */ 43 | #define not_ok(fmt, ...) die (fmt, ## __VA_ARGS__) 44 | 45 | /* approximately GLib-compatible helper macros */ 46 | #define g_test_message(fmt, ...) printf ("# " fmt "\n", ## __VA_ARGS__) 47 | #define g_assert_cmpstr(left_expr, op, right_expr) \ 48 | do { \ 49 | const char *left = (left_expr); \ 50 | const char *right = (right_expr); \ 51 | if (strcmp0 (left, right) op 0) \ 52 | ok ("%s (\"%s\") %s %s (\"%s\")", #left_expr, left, #op, #right_expr, right); \ 53 | else \ 54 | not_ok ("expected %s (\"%s\") %s %s (\"%s\")", \ 55 | #left_expr, left, #op, #right_expr, right); \ 56 | } while (0) 57 | #define g_assert_cmpint(left_expr, op, right_expr) \ 58 | do { \ 59 | intmax_t left = (left_expr); \ 60 | intmax_t right = (right_expr); \ 61 | if (left op right) \ 62 | ok ("%s (%ji) %s %s (%ji)", #left_expr, left, #op, #right_expr, right); \ 63 | else \ 64 | not_ok ("expected %s (%ji) %s %s (%ji)", \ 65 | #left_expr, left, #op, #right_expr, right); \ 66 | } while (0) 67 | #define g_assert_cmpuint(left_expr, op, right_expr) \ 68 | do { \ 69 | uintmax_t left = (left_expr); \ 70 | uintmax_t right = (right_expr); \ 71 | if (left op right) \ 72 | ok ("%s (%ju) %s %s (%ju)", #left_expr, left, #op, #right_expr, right); \ 73 | else \ 74 | not_ok ("expected %s (%ju) %s %s (%ju)", \ 75 | #left_expr, left, #op, #right_expr, right); \ 76 | } while (0) 77 | #define g_assert_true(expr) \ 78 | do { \ 79 | if ((expr)) \ 80 | ok ("%s", #expr); \ 81 | else \ 82 | not_ok ("expected %s to be true", #expr); \ 83 | } while (0) 84 | #define g_assert_false(expr) \ 85 | do { \ 86 | if (!(expr)) \ 87 | ok ("!(%s)", #expr); \ 88 | else \ 89 | not_ok ("expected %s to be false", #expr); \ 90 | } while (0) 91 | #define g_assert_null(expr) \ 92 | do { \ 93 | if ((expr) == NULL) \ 94 | ok ("%s was null", #expr); \ 95 | else \ 96 | not_ok ("expected %s to be null", #expr); \ 97 | } while (0) 98 | #define g_assert_nonnull(expr) \ 99 | do { \ 100 | if ((expr) != NULL) \ 101 | ok ("%s wasn't null", #expr); \ 102 | else \ 103 | not_ok ("expected %s to be non-null", #expr); \ 104 | } while (0) 105 | 106 | static int 107 | strcmp0 (const char *left, 108 | const char *right) 109 | { 110 | if (left == right) 111 | return 0; 112 | 113 | if (left == NULL) 114 | return -1; 115 | 116 | if (right == NULL) 117 | return 1; 118 | 119 | return strcmp (left, right); 120 | } 121 | 122 | static void 123 | test_n_elements (void) 124 | { 125 | int three[] = { 1, 2, 3 }; 126 | g_assert_cmpuint (N_ELEMENTS (three), ==, 3); 127 | } 128 | 129 | static void 130 | test_strconcat (void) 131 | { 132 | const char *a = "aaa"; 133 | const char *b = "bbb"; 134 | char *ab = strconcat (a, b); 135 | g_assert_cmpstr (ab, ==, "aaabbb"); 136 | free (ab); 137 | } 138 | 139 | static void 140 | test_strconcat3 (void) 141 | { 142 | const char *a = "aaa"; 143 | const char *b = "bbb"; 144 | const char *c = "ccc"; 145 | char *abc = strconcat3 (a, b, c); 146 | g_assert_cmpstr (abc, ==, "aaabbbccc"); 147 | free (abc); 148 | } 149 | 150 | static void 151 | test_has_prefix (void) 152 | { 153 | g_assert_true (has_prefix ("foo", "foo")); 154 | g_assert_true (has_prefix ("foobar", "foo")); 155 | g_assert_false (has_prefix ("foobar", "fool")); 156 | g_assert_false (has_prefix ("foo", "fool")); 157 | g_assert_true (has_prefix ("foo", "")); 158 | g_assert_true (has_prefix ("", "")); 159 | g_assert_false (has_prefix ("", "no")); 160 | g_assert_false (has_prefix ("yes", "no")); 161 | } 162 | 163 | static void 164 | test_has_path_prefix (void) 165 | { 166 | static const struct 167 | { 168 | const char *str; 169 | const char *prefix; 170 | bool expected; 171 | } tests[] = 172 | { 173 | { "/run/host/usr", "/run/host", true }, 174 | { "/run/host/usr", "/run/host/", true }, 175 | { "/run/host", "/run/host", true }, 176 | { "////run///host////usr", "//run//host", true }, 177 | { "////run///host////usr", "//run//host////", true }, 178 | { "/run/hostage", "/run/host", false }, 179 | /* Any number of leading slashes is ignored, even zero */ 180 | { "foo/bar", "/foo", true }, 181 | { "/foo/bar", "foo", true }, 182 | }; 183 | size_t i; 184 | 185 | for (i = 0; i < N_ELEMENTS (tests); i++) 186 | { 187 | const char *str = tests[i].str; 188 | const char *prefix = tests[i].prefix; 189 | bool expected = tests[i].expected; 190 | 191 | if (expected) 192 | g_test_message ("%s should have path prefix %s", str, prefix); 193 | else 194 | g_test_message ("%s should not have path prefix %s", str, prefix); 195 | 196 | if (expected) 197 | g_assert_true (has_path_prefix (str, prefix)); 198 | else 199 | g_assert_false (has_path_prefix (str, prefix)); 200 | } 201 | } 202 | 203 | static void 204 | test_string_builder (void) 205 | { 206 | StringBuilder sb = {0}; 207 | 208 | strappend (&sb, "aaa"); 209 | g_assert_cmpstr (sb.str, ==, "aaa"); 210 | strappend (&sb, "bbb"); 211 | g_assert_cmpstr (sb.str, ==, "aaabbb"); 212 | strappendf (&sb, "c%dc%s", 9, "x"); 213 | g_assert_cmpstr (sb.str, ==, "aaabbbc9cx"); 214 | strappend_escape_for_mount_options (&sb, "/path :,\\"); 215 | g_assert_cmpstr (sb.str, ==, "aaabbbc9cx/path \\:\\,\\\\"); 216 | strappend (&sb, "zzz"); 217 | g_assert_cmpstr (sb.str, ==, "aaabbbc9cx/path \\:\\,\\\\zzz"); 218 | 219 | free (sb.str); 220 | sb = (StringBuilder){0}; 221 | 222 | strappend_escape_for_mount_options (&sb, "aaa"); 223 | g_assert_cmpstr (sb.str, ==, "aaa"); 224 | 225 | free (sb.str); 226 | sb = (StringBuilder){0}; 227 | 228 | strappend_escape_for_mount_options (&sb, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); 229 | g_assert_cmpstr (sb.str, ==, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); 230 | 231 | free (sb.str); 232 | } 233 | 234 | int 235 | main (int argc UNUSED, 236 | char **argv UNUSED) 237 | { 238 | setvbuf (stdout, NULL, _IONBF, 0); 239 | test_n_elements (); 240 | test_strconcat (); 241 | test_strconcat3 (); 242 | test_has_prefix (); 243 | test_has_path_prefix (); 244 | test_string_builder (); 245 | printf ("1..%u\n", test_number); 246 | return 0; 247 | } 248 | -------------------------------------------------------------------------------- /tests/try-syscall.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Simon McVittie 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | * 5 | * Try one or more system calls that might have been blocked by a 6 | * seccomp filter. Return the last value of errno seen. 7 | * 8 | * In general, we pass a bad fd or pointer to each syscall that will 9 | * accept one, so that it will fail with EBADF or EFAULT without side-effects. 10 | * 11 | * This helper is used for regression tests in both bubblewrap and flatpak. 12 | * Please keep both copies in sync. 13 | */ 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #if defined(_MIPS_SIM) 27 | # if _MIPS_SIM == _ABIO32 28 | # define MISSING_SYSCALL_BASE 4000 29 | # elif _MIPS_SIM == _ABI64 30 | # define MISSING_SYSCALL_BASE 5000 31 | # elif _MIPS_SIM == _ABIN32 32 | # define MISSING_SYSCALL_BASE 6000 33 | # else 34 | # error "Unknown MIPS ABI" 35 | # endif 36 | #endif 37 | 38 | #if defined(__ia64__) 39 | # define MISSING_SYSCALL_BASE 1024 40 | #endif 41 | 42 | #if defined(__alpha__) 43 | # define MISSING_SYSCALL_BASE 110 44 | #endif 45 | 46 | #if defined(__x86_64__) && defined(__ILP32__) 47 | # define MISSING_SYSCALL_BASE 0x40000000 48 | #endif 49 | 50 | /* 51 | * MISSING_SYSCALL_BASE: 52 | * 53 | * Number to add to the syscall numbers of recently-added syscalls 54 | * to get the appropriate syscall for the current ABI. 55 | */ 56 | #ifndef MISSING_SYSCALL_BASE 57 | # define MISSING_SYSCALL_BASE 0 58 | #endif 59 | 60 | #ifndef __NR_clone3 61 | # define __NR_clone3 (MISSING_SYSCALL_BASE + 435) 62 | #endif 63 | 64 | /* 65 | * The size of clone3's parameter (as of 2021) 66 | */ 67 | #define SIZEOF_STRUCT_CLONE_ARGS ((size_t) 88) 68 | 69 | /* 70 | * An invalid pointer that will cause syscalls to fail with EFAULT 71 | */ 72 | #define WRONG_POINTER ((char *) 1) 73 | 74 | #ifndef PR_GET_CHILD_SUBREAPER 75 | #define PR_GET_CHILD_SUBREAPER 37 76 | #endif 77 | 78 | int 79 | main (int argc, char **argv) 80 | { 81 | int errsv = 0; 82 | int i; 83 | 84 | for (i = 1; i < argc; i++) 85 | { 86 | const char *arg = argv[i]; 87 | 88 | if (strcmp (arg, "print-errno-values") == 0) 89 | { 90 | printf ("EBADF=%d\n", EBADF); 91 | printf ("EFAULT=%d\n", EFAULT); 92 | printf ("ENOENT=%d\n", ENOENT); 93 | printf ("ENOSYS=%d\n", ENOSYS); 94 | printf ("EPERM=%d\n", EPERM); 95 | } 96 | else if (strcmp (arg, "chmod") == 0) 97 | { 98 | /* If not blocked by seccomp, this will fail with EFAULT */ 99 | if (chmod (WRONG_POINTER, 0700) != 0) 100 | { 101 | errsv = errno; 102 | perror (arg); 103 | } 104 | } 105 | else if (strcmp (arg, "chroot") == 0) 106 | { 107 | /* If not blocked by seccomp, this will fail with EFAULT */ 108 | if (chroot (WRONG_POINTER) != 0) 109 | { 110 | errsv = errno; 111 | perror (arg); 112 | } 113 | } 114 | else if (strcmp (arg, "clone3") == 0) 115 | { 116 | /* If not blocked by seccomp, this will fail with EFAULT */ 117 | if (syscall (__NR_clone3, WRONG_POINTER, SIZEOF_STRUCT_CLONE_ARGS) != 0) 118 | { 119 | errsv = errno; 120 | perror (arg); 121 | } 122 | } 123 | else if (strcmp (arg, "ioctl TIOCNOTTY") == 0) 124 | { 125 | /* If not blocked by seccomp, this will fail with EBADF */ 126 | if (ioctl (-1, TIOCNOTTY) != 0) 127 | { 128 | errsv = errno; 129 | perror (arg); 130 | } 131 | } 132 | else if (strcmp (arg, "ioctl TIOCSTI") == 0) 133 | { 134 | /* If not blocked by seccomp, this will fail with EBADF */ 135 | if (ioctl (-1, TIOCSTI, WRONG_POINTER) != 0) 136 | { 137 | errsv = errno; 138 | perror (arg); 139 | } 140 | } 141 | #ifdef __LP64__ 142 | else if (strcmp (arg, "ioctl TIOCSTI CVE-2019-10063") == 0) 143 | { 144 | unsigned long not_TIOCSTI = (0x123UL << 32) | (unsigned long) TIOCSTI; 145 | 146 | /* If not blocked by seccomp, this will fail with EBADF */ 147 | if (syscall (__NR_ioctl, -1, not_TIOCSTI, WRONG_POINTER) != 0) 148 | { 149 | errsv = errno; 150 | perror (arg); 151 | } 152 | } 153 | #endif 154 | else if (strcmp (arg, "listen") == 0) 155 | { 156 | /* If not blocked by seccomp, this will fail with EBADF */ 157 | if (listen (-1, 42) != 0) 158 | { 159 | errsv = errno; 160 | perror (arg); 161 | } 162 | } 163 | else if (strcmp (arg, "prctl") == 0) 164 | { 165 | /* If not blocked by seccomp, this will fail with EFAULT */ 166 | if (prctl (PR_GET_CHILD_SUBREAPER, WRONG_POINTER, 0, 0, 0) != 0) 167 | { 168 | errsv = errno; 169 | perror (arg); 170 | } 171 | } 172 | else 173 | { 174 | fprintf (stderr, "Unsupported syscall \"%s\"\n", arg); 175 | errsv = ENOENT; 176 | } 177 | } 178 | 179 | return errsv; 180 | } 181 | -------------------------------------------------------------------------------- /tests/use-as-subproject/.gitignore: -------------------------------------------------------------------------------- 1 | /_build/ 2 | /subprojects/ 3 | -------------------------------------------------------------------------------- /tests/use-as-subproject/README: -------------------------------------------------------------------------------- 1 | This is a simple example of a project that uses bubblewrap as a 2 | subproject. The intention is that if this project can successfully build 3 | bubblewrap as a subproject, then so could Flatpak. 4 | -------------------------------------------------------------------------------- /tests/use-as-subproject/assert-correct-rpath.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # Copyright 2022 Collabora Ltd. 3 | # SPDX-License-Identifier: LGPL-2.0-or-later 4 | 5 | import subprocess 6 | import sys 7 | 8 | if __name__ == '__main__': 9 | completed = subprocess.run( 10 | ['objdump', '-T', '-x', sys.argv[1]], 11 | stdout=subprocess.PIPE, 12 | ) 13 | stdout = completed.stdout 14 | assert stdout is not None 15 | seen_rpath = False 16 | 17 | for line in stdout.splitlines(): 18 | words = line.strip().split() 19 | 20 | if words and words[0] in (b'RPATH', b'RUNPATH'): 21 | print(line.decode(errors='backslashreplace')) 22 | assert len(words) == 2, words 23 | assert words[1] == b'${ORIGIN}/../lib', words 24 | seen_rpath = True 25 | 26 | assert seen_rpath 27 | -------------------------------------------------------------------------------- /tests/use-as-subproject/config.h: -------------------------------------------------------------------------------- 1 | #error Should not use superproject config.h to compile bubblewrap 2 | -------------------------------------------------------------------------------- /tests/use-as-subproject/dummy-config.h.in: -------------------------------------------------------------------------------- 1 | #error Should not use superproject generated config.h to compile bubblewrap 2 | -------------------------------------------------------------------------------- /tests/use-as-subproject/meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'use-bubblewrap-as-subproject', 3 | 'c', 4 | version : '0', 5 | meson_version : '>=0.49.0', 6 | ) 7 | 8 | configure_file( 9 | output : 'config.h', 10 | input : 'dummy-config.h.in', 11 | configuration : configuration_data(), 12 | ) 13 | 14 | subproject( 15 | 'bubblewrap', 16 | default_options : [ 17 | 'install_rpath=${ORIGIN}/../lib', 18 | 'program_prefix=not-flatpak-', 19 | ], 20 | ) 21 | -------------------------------------------------------------------------------- /uncrustify.cfg: -------------------------------------------------------------------------------- 1 | newlines lf 2 | 3 | input_tab_size 8 4 | output_tab_size 8 5 | 6 | string_escape_char 92 7 | string_escape_char2 0 8 | 9 | # indenting 10 | indent_columns 2 11 | indent_with_tabs 0 12 | indent_align_string True 13 | indent_brace 2 14 | indent_braces false 15 | indent_braces_no_func True 16 | indent_func_call_param false 17 | indent_func_def_param false 18 | indent_func_proto_param false 19 | indent_switch_case 0 20 | indent_case_brace 2 21 | indent_paren_close 1 22 | 23 | # spacing 24 | sp_arith Add 25 | sp_assign Add 26 | sp_enum_assign Add 27 | sp_bool Add 28 | sp_compare Add 29 | sp_inside_paren Remove 30 | sp_inside_fparens Remove 31 | sp_func_def_paren Force 32 | sp_func_proto_paren Force 33 | sp_paren_paren Remove 34 | sp_balance_nested_parens False 35 | sp_paren_brace Remove 36 | sp_before_square Remove 37 | sp_before_squares Remove 38 | sp_inside_square Remove 39 | sp_before_ptr_star Add 40 | sp_between_ptr_star Remove 41 | sp_after_comma Add 42 | sp_before_comma Remove 43 | sp_after_cast Add 44 | sp_sizeof_paren Add 45 | sp_not Remove 46 | sp_inv Remove 47 | sp_addr Remove 48 | sp_member Remove 49 | sp_deref Remove 50 | sp_sign Remove 51 | sp_incdec Remove 52 | sp_attribute_paren remove 53 | sp_macro Force 54 | sp_func_call_paren Force 55 | sp_func_call_user_paren Remove 56 | set func_call_user _ N_ C_ g_autoptr g_auto 57 | sp_brace_typedef add 58 | sp_cond_colon add 59 | sp_cond_question add 60 | sp_defined_paren remove 61 | 62 | # alignment 63 | align_keep_tabs False 64 | align_with_tabs False 65 | align_on_tabstop False 66 | align_number_left True 67 | align_func_params True 68 | align_var_def_span 0 69 | align_var_def_amp_style 1 70 | align_var_def_colon true 71 | align_enum_equ_span 0 72 | align_var_struct_span 2 73 | align_var_def_star_style 2 74 | align_var_def_amp_style 2 75 | align_typedef_span 2 76 | align_typedef_func 0 77 | align_typedef_star_style 2 78 | align_typedef_amp_style 2 79 | 80 | # newlines 81 | nl_assign_leave_one_liners True 82 | nl_enum_leave_one_liners False 83 | nl_func_leave_one_liners False 84 | nl_if_leave_one_liners False 85 | nl_end_of_file Add 86 | nl_assign_brace Remove 87 | nl_func_var_def_blk 1 88 | nl_fcall_brace Add 89 | nl_enum_brace Remove 90 | nl_struct_brace Force 91 | nl_union_brace Force 92 | nl_if_brace Force 93 | nl_brace_else Force 94 | nl_elseif_brace Force 95 | nl_else_brace Add 96 | nl_for_brace Force 97 | nl_while_brace Force 98 | nl_do_brace Force 99 | nl_brace_while Force 100 | nl_switch_brace Force 101 | nl_before_case True 102 | nl_after_case False 103 | nl_func_type_name Force 104 | nl_func_proto_type_name Remove 105 | nl_func_paren Remove 106 | nl_func_decl_start Remove 107 | nl_func_decl_args Force 108 | nl_func_decl_end Remove 109 | nl_fdef_brace Force 110 | nl_after_return False 111 | nl_define_macro False 112 | nl_create_if_one_liner False 113 | nl_create_for_one_liner False 114 | nl_create_while_one_liner False 115 | nl_after_semicolon True 116 | nl_multi_line_cond true 117 | 118 | # mod 119 | # I'd like these to be remove, but that removes brackets in if { if { foo } }, which i dislike 120 | # Not clear what to do about that... 121 | mod_full_brace_for Remove 122 | mod_full_brace_if Remove 123 | mod_full_brace_if_chain True 124 | mod_full_brace_while Remove 125 | mod_full_brace_do Remove 126 | mod_full_brace_nl 3 127 | mod_paren_on_return Remove 128 | 129 | # line splitting 130 | #code_width = 78 131 | ls_for_split_full True 132 | ls_func_split_full True 133 | 134 | # positioning 135 | pos_bool Trail 136 | pos_conditional Trail 137 | -------------------------------------------------------------------------------- /uncrustify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | uncrustify -c uncrustify.cfg --no-backup `git ls-tree --name-only -r HEAD | grep \\\.[ch]$` 3 | -------------------------------------------------------------------------------- /utils.c: -------------------------------------------------------------------------------- 1 | /* bubblewrap 2 | * Copyright (C) 2016 Alexander Larsson 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library. If not, see . 17 | * 18 | */ 19 | #include "config.h" 20 | 21 | #include "utils.h" 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #ifdef HAVE_SELINUX 28 | #include 29 | #endif 30 | 31 | #ifndef HAVE_SELINUX_2_3 32 | /* libselinux older than 2.3 weren't const-correct */ 33 | #define setexeccon(x) setexeccon ((security_context_t) x) 34 | #define setfscreatecon(x) setfscreatecon ((security_context_t) x) 35 | #define security_check_context(x) security_check_context ((security_context_t) x) 36 | #endif 37 | 38 | bool bwrap_level_prefix = false; 39 | 40 | __attribute__((format(printf, 2, 0))) static void 41 | bwrap_logv (int severity, 42 | const char *format, 43 | va_list args, 44 | const char *detail) 45 | { 46 | if (bwrap_level_prefix) 47 | fprintf (stderr, "<%d>", severity); 48 | 49 | fprintf (stderr, "bwrap: "); 50 | vfprintf (stderr, format, args); 51 | 52 | if (detail != NULL) 53 | fprintf (stderr, ": %s", detail); 54 | 55 | fprintf (stderr, "\n"); 56 | } 57 | 58 | void 59 | bwrap_log (int severity, 60 | const char *format, ...) 61 | { 62 | va_list args; 63 | 64 | va_start (args, format); 65 | bwrap_logv (severity, format, args, NULL); 66 | va_end (args); 67 | } 68 | 69 | void 70 | die_with_error (const char *format, ...) 71 | { 72 | va_list args; 73 | int errsv; 74 | 75 | errsv = errno; 76 | 77 | va_start (args, format); 78 | bwrap_logv (LOG_ERR, format, args, strerror (errsv)); 79 | va_end (args); 80 | 81 | exit (1); 82 | } 83 | 84 | void 85 | die_with_mount_error (const char *format, ...) 86 | { 87 | va_list args; 88 | int errsv; 89 | 90 | errsv = errno; 91 | 92 | va_start (args, format); 93 | bwrap_logv (LOG_ERR, format, args, mount_strerror (errsv)); 94 | va_end (args); 95 | 96 | exit (1); 97 | } 98 | 99 | void 100 | die (const char *format, ...) 101 | { 102 | va_list args; 103 | 104 | va_start (args, format); 105 | bwrap_logv (LOG_ERR, format, args, NULL); 106 | va_end (args); 107 | 108 | exit (1); 109 | } 110 | 111 | void 112 | die_unless_label_valid (UNUSED const char *label) 113 | { 114 | #ifdef HAVE_SELINUX 115 | if (is_selinux_enabled () == 1) 116 | { 117 | if (security_check_context (label) < 0) 118 | die_with_error ("invalid label %s", label); 119 | return; 120 | } 121 | #endif 122 | die ("labeling not supported on this system"); 123 | } 124 | 125 | void 126 | die_oom (void) 127 | { 128 | fputs ("Out of memory\n", stderr); 129 | exit (1); 130 | } 131 | 132 | /* Fork, return in child, exiting the previous parent */ 133 | void 134 | fork_intermediate_child (void) 135 | { 136 | int pid = fork (); 137 | if (pid == -1) 138 | die_with_error ("Can't fork for --pidns"); 139 | 140 | /* Parent is an process not needed */ 141 | if (pid != 0) 142 | exit (0); 143 | } 144 | 145 | void * 146 | xmalloc (size_t size) 147 | { 148 | void *res = malloc (size); 149 | 150 | if (res == NULL) 151 | die_oom (); 152 | return res; 153 | } 154 | 155 | void * 156 | xcalloc (size_t nmemb, size_t size) 157 | { 158 | void *res = calloc (nmemb, size); 159 | 160 | if (res == NULL) 161 | die_oom (); 162 | return res; 163 | } 164 | 165 | void * 166 | xrealloc (void *ptr, size_t size) 167 | { 168 | void *res; 169 | 170 | assert (size != 0); 171 | 172 | res = realloc (ptr, size); 173 | 174 | if (res == NULL) 175 | die_oom (); 176 | return res; 177 | } 178 | 179 | char * 180 | xstrdup (const char *str) 181 | { 182 | char *res; 183 | 184 | assert (str != NULL); 185 | 186 | res = strdup (str); 187 | if (res == NULL) 188 | die_oom (); 189 | 190 | return res; 191 | } 192 | 193 | void 194 | strfreev (char **str_array) 195 | { 196 | if (str_array) 197 | { 198 | int i; 199 | 200 | for (i = 0; str_array[i] != NULL; i++) 201 | free (str_array[i]); 202 | 203 | free (str_array); 204 | } 205 | } 206 | 207 | /* Compares if str has a specific path prefix. This differs 208 | from a regular prefix in two ways. First of all there may 209 | be multiple slashes separating the path elements, and 210 | secondly, if a prefix is matched that has to be en entire 211 | path element. For instance /a/prefix matches /a/prefix/foo/bar, 212 | but not /a/prefixfoo/bar. */ 213 | bool 214 | has_path_prefix (const char *str, 215 | const char *prefix) 216 | { 217 | while (true) 218 | { 219 | /* Skip consecutive slashes to reach next path 220 | element */ 221 | while (*str == '/') 222 | str++; 223 | while (*prefix == '/') 224 | prefix++; 225 | 226 | /* No more prefix path elements? Done! */ 227 | if (*prefix == 0) 228 | return true; 229 | 230 | /* Compare path element */ 231 | while (*prefix != 0 && *prefix != '/') 232 | { 233 | if (*str != *prefix) 234 | return false; 235 | str++; 236 | prefix++; 237 | } 238 | 239 | /* Matched prefix path element, 240 | must be entire str path element */ 241 | if (*str != '/' && *str != 0) 242 | return false; 243 | } 244 | } 245 | 246 | bool 247 | path_equal (const char *path1, 248 | const char *path2) 249 | { 250 | while (true) 251 | { 252 | /* Skip consecutive slashes to reach next path 253 | element */ 254 | while (*path1 == '/') 255 | path1++; 256 | while (*path2 == '/') 257 | path2++; 258 | 259 | /* No more prefix path elements? Done! */ 260 | if (*path1 == 0 || *path2 == 0) 261 | return *path1 == 0 && *path2 == 0; 262 | 263 | /* Compare path element */ 264 | while (*path1 != 0 && *path1 != '/') 265 | { 266 | if (*path1 != *path2) 267 | return false; 268 | path1++; 269 | path2++; 270 | } 271 | 272 | /* Matched path1 path element, must be entire path element */ 273 | if (*path2 != '/' && *path2 != 0) 274 | return false; 275 | } 276 | } 277 | 278 | 279 | bool 280 | has_prefix (const char *str, 281 | const char *prefix) 282 | { 283 | return strncmp (str, prefix, strlen (prefix)) == 0; 284 | } 285 | 286 | void 287 | xclearenv (void) 288 | { 289 | if (clearenv () != 0) 290 | die_with_error ("clearenv failed"); 291 | } 292 | 293 | void 294 | xsetenv (const char *name, const char *value, int overwrite) 295 | { 296 | if (setenv (name, value, overwrite)) 297 | die ("setenv failed"); 298 | } 299 | 300 | void 301 | xunsetenv (const char *name) 302 | { 303 | if (unsetenv (name)) 304 | die ("unsetenv failed"); 305 | } 306 | 307 | char * 308 | strconcat (const char *s1, 309 | const char *s2) 310 | { 311 | size_t len = 0; 312 | char *res; 313 | 314 | if (s1) 315 | len += strlen (s1); 316 | if (s2) 317 | len += strlen (s2); 318 | 319 | res = xmalloc (len + 1); 320 | *res = 0; 321 | if (s1) 322 | strcat (res, s1); 323 | if (s2) 324 | strcat (res, s2); 325 | 326 | return res; 327 | } 328 | 329 | char * 330 | strconcat3 (const char *s1, 331 | const char *s2, 332 | const char *s3) 333 | { 334 | size_t len = 0; 335 | char *res; 336 | 337 | if (s1) 338 | len += strlen (s1); 339 | if (s2) 340 | len += strlen (s2); 341 | if (s3) 342 | len += strlen (s3); 343 | 344 | res = xmalloc (len + 1); 345 | *res = 0; 346 | if (s1) 347 | strcat (res, s1); 348 | if (s2) 349 | strcat (res, s2); 350 | if (s3) 351 | strcat (res, s3); 352 | 353 | return res; 354 | } 355 | 356 | char * 357 | xasprintf (const char *format, 358 | ...) 359 | { 360 | char *buffer = NULL; 361 | va_list args; 362 | 363 | va_start (args, format); 364 | if (vasprintf (&buffer, format, args) == -1) 365 | die_oom (); 366 | va_end (args); 367 | 368 | return buffer; 369 | } 370 | 371 | int 372 | fdwalk (int proc_fd, int (*cb)(void *data, 373 | int fd), void *data) 374 | { 375 | int open_max; 376 | int fd; 377 | int dfd; 378 | int res = 0; 379 | DIR *d; 380 | 381 | dfd = TEMP_FAILURE_RETRY (openat (proc_fd, "self/fd", O_DIRECTORY | O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY)); 382 | if (dfd == -1) 383 | return res; 384 | 385 | if ((d = fdopendir (dfd))) 386 | { 387 | struct dirent *de; 388 | 389 | while ((de = readdir (d))) 390 | { 391 | long l; 392 | char *e = NULL; 393 | 394 | if (de->d_name[0] == '.') 395 | continue; 396 | 397 | errno = 0; 398 | l = strtol (de->d_name, &e, 10); 399 | if (errno != 0 || !e || *e) 400 | continue; 401 | 402 | fd = (int) l; 403 | 404 | if ((long) fd != l) 405 | continue; 406 | 407 | if (fd == dirfd (d)) 408 | continue; 409 | 410 | if ((res = cb (data, fd)) != 0) 411 | break; 412 | } 413 | 414 | closedir (d); 415 | return res; 416 | } 417 | 418 | open_max = sysconf (_SC_OPEN_MAX); 419 | 420 | for (fd = 0; fd < open_max; fd++) 421 | if ((res = cb (data, fd)) != 0) 422 | break; 423 | 424 | return res; 425 | } 426 | 427 | /* Sets errno on error (!= 0), ENOSPC on short write */ 428 | int 429 | write_to_fd (int fd, 430 | const char *content, 431 | ssize_t len) 432 | { 433 | ssize_t res; 434 | 435 | while (len > 0) 436 | { 437 | res = write (fd, content, len); 438 | if (res < 0 && errno == EINTR) 439 | continue; 440 | if (res <= 0) 441 | { 442 | if (res == 0) /* Unexpected short write, should not happen when writing to a file */ 443 | errno = ENOSPC; 444 | return -1; 445 | } 446 | len -= res; 447 | content += res; 448 | } 449 | 450 | return 0; 451 | } 452 | 453 | /* Sets errno on error (!= 0), ENOSPC on short write */ 454 | int 455 | write_file_at (int dfd, 456 | const char *path, 457 | const char *content) 458 | { 459 | int fd; 460 | bool res; 461 | int errsv; 462 | 463 | fd = TEMP_FAILURE_RETRY (openat (dfd, path, O_RDWR | O_CLOEXEC, 0)); 464 | if (fd == -1) 465 | return -1; 466 | 467 | res = 0; 468 | if (content) 469 | res = write_to_fd (fd, content, strlen (content)); 470 | 471 | errsv = errno; 472 | close (fd); 473 | errno = errsv; 474 | 475 | return res; 476 | } 477 | 478 | /* Sets errno on error (!= 0), ENOSPC on short write */ 479 | int 480 | create_file (const char *path, 481 | mode_t mode, 482 | const char *content) 483 | { 484 | int fd; 485 | int res; 486 | int errsv; 487 | 488 | fd = TEMP_FAILURE_RETRY (creat (path, mode)); 489 | if (fd == -1) 490 | return -1; 491 | 492 | res = 0; 493 | if (content) 494 | res = write_to_fd (fd, content, strlen (content)); 495 | 496 | errsv = errno; 497 | close (fd); 498 | errno = errsv; 499 | 500 | return res; 501 | } 502 | 503 | int 504 | ensure_file (const char *path, 505 | mode_t mode) 506 | { 507 | struct stat buf; 508 | 509 | /* We check this ahead of time, otherwise 510 | the create file will fail in the read-only 511 | case with EROFS instead of EEXIST. 512 | 513 | We're trying to set up a mount point for a non-directory, so any 514 | non-directory, non-symlink is acceptable - it doesn't necessarily 515 | have to be a regular file. */ 516 | if (stat (path, &buf) == 0 && 517 | !S_ISDIR (buf.st_mode) && 518 | !S_ISLNK (buf.st_mode)) 519 | return 0; 520 | 521 | if (create_file (path, mode, NULL) != 0 && errno != EEXIST) 522 | return -1; 523 | 524 | return 0; 525 | } 526 | 527 | 528 | #define BUFSIZE 8192 529 | /* Sets errno on error (!= 0), ENOSPC on short write */ 530 | int 531 | copy_file_data (int sfd, 532 | int dfd) 533 | { 534 | char buffer[BUFSIZE]; 535 | ssize_t bytes_read; 536 | 537 | while (true) 538 | { 539 | bytes_read = read (sfd, buffer, BUFSIZE); 540 | if (bytes_read == -1) 541 | { 542 | if (errno == EINTR) 543 | continue; 544 | 545 | return -1; 546 | } 547 | 548 | if (bytes_read == 0) 549 | break; 550 | 551 | if (write_to_fd (dfd, buffer, bytes_read) != 0) 552 | return -1; 553 | } 554 | 555 | return 0; 556 | } 557 | 558 | /* Sets errno on error (!= 0), ENOSPC on short write */ 559 | int 560 | copy_file (const char *src_path, 561 | const char *dst_path, 562 | mode_t mode) 563 | { 564 | int sfd; 565 | int dfd; 566 | int res; 567 | int errsv; 568 | 569 | sfd = TEMP_FAILURE_RETRY (open (src_path, O_CLOEXEC | O_RDONLY)); 570 | if (sfd == -1) 571 | return -1; 572 | 573 | dfd = TEMP_FAILURE_RETRY (creat (dst_path, mode)); 574 | if (dfd == -1) 575 | { 576 | errsv = errno; 577 | close (sfd); 578 | errno = errsv; 579 | return -1; 580 | } 581 | 582 | res = copy_file_data (sfd, dfd); 583 | 584 | errsv = errno; 585 | close (sfd); 586 | close (dfd); 587 | errno = errsv; 588 | 589 | return res; 590 | } 591 | 592 | /* Sets errno on error (== NULL), 593 | * Always ensures terminating zero */ 594 | char * 595 | load_file_data (int fd, 596 | size_t *size) 597 | { 598 | cleanup_free char *data = NULL; 599 | ssize_t data_read; 600 | ssize_t data_len; 601 | ssize_t res; 602 | 603 | data_read = 0; 604 | data_len = 4080; 605 | data = xmalloc (data_len); 606 | 607 | do 608 | { 609 | if (data_len == data_read + 1) 610 | { 611 | if (data_len > SSIZE_MAX / 2) 612 | { 613 | errno = EFBIG; 614 | return NULL; 615 | } 616 | 617 | data_len *= 2; 618 | data = xrealloc (data, data_len); 619 | } 620 | 621 | do 622 | res = read (fd, data + data_read, data_len - data_read - 1); 623 | while (res < 0 && errno == EINTR); 624 | 625 | if (res < 0) 626 | return NULL; 627 | 628 | data_read += res; 629 | } 630 | while (res > 0); 631 | 632 | data[data_read] = 0; 633 | 634 | if (size) 635 | *size = (size_t) data_read; 636 | 637 | return steal_pointer (&data); 638 | } 639 | 640 | /* Sets errno on error (== NULL), 641 | * Always ensures terminating zero */ 642 | char * 643 | load_file_at (int dfd, 644 | const char *path) 645 | { 646 | int fd; 647 | char *data; 648 | int errsv; 649 | 650 | fd = TEMP_FAILURE_RETRY (openat (dfd, path, O_CLOEXEC | O_RDONLY)); 651 | if (fd == -1) 652 | return NULL; 653 | 654 | data = load_file_data (fd, NULL); 655 | 656 | errsv = errno; 657 | close (fd); 658 | errno = errsv; 659 | 660 | return data; 661 | } 662 | 663 | /* Sets errno on error (< 0) */ 664 | int 665 | get_file_mode (const char *pathname) 666 | { 667 | struct stat buf; 668 | 669 | if (stat (pathname, &buf) != 0) 670 | return -1; 671 | 672 | return buf.st_mode & S_IFMT; 673 | } 674 | 675 | int 676 | ensure_dir (const char *path, 677 | mode_t mode) 678 | { 679 | struct stat buf; 680 | 681 | /* We check this ahead of time, otherwise 682 | the mkdir call can fail in the read-only 683 | case with EROFS instead of EEXIST on some 684 | filesystems (such as NFS) */ 685 | if (stat (path, &buf) == 0) 686 | { 687 | if (!S_ISDIR (buf.st_mode)) 688 | { 689 | errno = ENOTDIR; 690 | return -1; 691 | } 692 | 693 | return 0; 694 | } 695 | 696 | if (mkdir (path, mode) == -1 && errno != EEXIST) 697 | return -1; 698 | 699 | return 0; 700 | } 701 | 702 | 703 | /* Sets errno on error (!= 0) */ 704 | int 705 | mkdir_with_parents (const char *pathname, 706 | mode_t mode, 707 | bool create_last) 708 | { 709 | cleanup_free char *fn = NULL; 710 | char *p; 711 | 712 | if (pathname == NULL || *pathname == '\0') 713 | { 714 | errno = EINVAL; 715 | return -1; 716 | } 717 | 718 | fn = xstrdup (pathname); 719 | 720 | p = fn; 721 | while (*p == '/') 722 | p++; 723 | 724 | do 725 | { 726 | while (*p && *p != '/') 727 | p++; 728 | 729 | if (!*p) 730 | p = NULL; 731 | else 732 | *p = '\0'; 733 | 734 | if (!create_last && p == NULL) 735 | break; 736 | 737 | if (ensure_dir (fn, mode) != 0) 738 | return -1; 739 | 740 | if (p) 741 | { 742 | *p++ = '/'; 743 | while (*p && *p == '/') 744 | p++; 745 | } 746 | } 747 | while (p); 748 | 749 | return 0; 750 | } 751 | 752 | /* Send an ucred with current pid/uid/gid over a socket, it can be 753 | read back with read_pid_from_socket(), and then the kernel has 754 | translated it between namespaces as needed. */ 755 | void 756 | send_pid_on_socket (int sockfd) 757 | { 758 | char buf[1] = { 0 }; 759 | struct msghdr msg = {}; 760 | struct iovec iov = { buf, sizeof (buf) }; 761 | const ssize_t control_len_snd = CMSG_SPACE(sizeof(struct ucred)); 762 | _Alignas(struct cmsghdr) char control_buf_snd[control_len_snd]; 763 | struct cmsghdr *cmsg; 764 | struct ucred cred; 765 | 766 | msg.msg_iov = &iov; 767 | msg.msg_iovlen = 1; 768 | msg.msg_control = control_buf_snd; 769 | msg.msg_controllen = control_len_snd; 770 | 771 | cmsg = CMSG_FIRSTHDR(&msg); 772 | cmsg->cmsg_level = SOL_SOCKET; 773 | cmsg->cmsg_type = SCM_CREDENTIALS; 774 | cmsg->cmsg_len = CMSG_LEN(sizeof(struct ucred)); 775 | 776 | cred.pid = getpid (); 777 | cred.uid = geteuid (); 778 | cred.gid = getegid (); 779 | memcpy (CMSG_DATA (cmsg), &cred, sizeof (cred)); 780 | 781 | if (TEMP_FAILURE_RETRY (sendmsg (sockfd, &msg, 0)) < 0) 782 | die_with_error ("Can't send pid"); 783 | } 784 | 785 | void 786 | create_pid_socketpair (int sockets[2]) 787 | { 788 | int enable = 1; 789 | 790 | if (socketpair (AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, sockets) != 0) 791 | die_with_error ("Can't create intermediate pids socket"); 792 | 793 | if (setsockopt (sockets[0], SOL_SOCKET, SO_PASSCRED, &enable, sizeof (enable)) < 0) 794 | die_with_error ("Can't set SO_PASSCRED"); 795 | } 796 | 797 | int 798 | read_pid_from_socket (int sockfd) 799 | { 800 | char recv_buf[1] = { 0 }; 801 | struct msghdr msg = {}; 802 | struct iovec iov = { recv_buf, sizeof (recv_buf) }; 803 | const ssize_t control_len_rcv = CMSG_SPACE(sizeof(struct ucred)); 804 | _Alignas(struct cmsghdr) char control_buf_rcv[control_len_rcv]; 805 | struct cmsghdr* cmsg; 806 | 807 | msg.msg_iov = &iov; 808 | msg.msg_iovlen = 1; 809 | msg.msg_control = control_buf_rcv; 810 | msg.msg_controllen = control_len_rcv; 811 | 812 | if (TEMP_FAILURE_RETRY (recvmsg (sockfd, &msg, 0)) < 0) 813 | die_with_error ("Can't read pid from socket"); 814 | 815 | if (msg.msg_controllen <= 0) 816 | die ("Unexpected short read from pid socket"); 817 | 818 | for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) 819 | { 820 | const unsigned payload_len = cmsg->cmsg_len - CMSG_LEN(0); 821 | if (cmsg->cmsg_level == SOL_SOCKET && 822 | cmsg->cmsg_type == SCM_CREDENTIALS && 823 | payload_len == sizeof(struct ucred)) 824 | { 825 | struct ucred cred; 826 | 827 | memcpy (&cred, CMSG_DATA (cmsg), sizeof (cred)); 828 | return cred.pid; 829 | } 830 | } 831 | die ("No pid returned on socket"); 832 | } 833 | 834 | /* Sets errno on error (== NULL), 835 | * Always ensures terminating zero */ 836 | char * 837 | readlink_malloc (const char *pathname) 838 | { 839 | size_t size = 50; 840 | ssize_t n; 841 | cleanup_free char *value = NULL; 842 | 843 | do 844 | { 845 | if (size > SIZE_MAX / 2) 846 | die ("Symbolic link target pathname too long"); 847 | size *= 2; 848 | value = xrealloc (value, size); 849 | n = readlink (pathname, value, size - 1); 850 | if (n < 0) 851 | return NULL; 852 | } 853 | while (size - 2 < (size_t)n); 854 | 855 | value[n] = 0; 856 | return steal_pointer (&value); 857 | } 858 | 859 | char * 860 | get_oldroot_path (const char *path) 861 | { 862 | while (*path == '/') 863 | path++; 864 | return strconcat ("/oldroot/", path); 865 | } 866 | 867 | char * 868 | get_newroot_path (const char *path) 869 | { 870 | while (*path == '/') 871 | path++; 872 | return strconcat ("/newroot/", path); 873 | } 874 | 875 | int 876 | raw_clone (unsigned long flags, 877 | void *child_stack) 878 | { 879 | #if defined(__s390__) || defined(__CRIS__) 880 | /* On s390 and cris the order of the first and second arguments 881 | * of the raw clone() system call is reversed. */ 882 | return (int) syscall (__NR_clone, child_stack, flags); 883 | #else 884 | return (int) syscall (__NR_clone, flags, child_stack); 885 | #endif 886 | } 887 | 888 | int 889 | pivot_root (const char * new_root, const char * put_old) 890 | { 891 | #ifdef __NR_pivot_root 892 | return syscall (__NR_pivot_root, new_root, put_old); 893 | #else 894 | errno = ENOSYS; 895 | return -1; 896 | #endif 897 | } 898 | 899 | char * 900 | label_mount (const char *opt, UNUSED const char *mount_label) 901 | { 902 | #ifdef HAVE_SELINUX 903 | if (mount_label) 904 | { 905 | if (opt) 906 | return xasprintf ("%s,context=\"%s\"", opt, mount_label); 907 | else 908 | return xasprintf ("context=\"%s\"", mount_label); 909 | } 910 | #endif 911 | if (opt) 912 | return xstrdup (opt); 913 | return NULL; 914 | } 915 | 916 | int 917 | label_create_file (UNUSED const char *file_label) 918 | { 919 | #ifdef HAVE_SELINUX 920 | if (is_selinux_enabled () > 0 && file_label) 921 | return setfscreatecon (file_label); 922 | #endif 923 | return 0; 924 | } 925 | 926 | int 927 | label_exec (UNUSED const char *exec_label) 928 | { 929 | #ifdef HAVE_SELINUX 930 | if (is_selinux_enabled () > 0 && exec_label) 931 | return setexeccon (exec_label); 932 | #endif 933 | return 0; 934 | } 935 | 936 | /* 937 | * Like strerror(), but specialized for a failed mount(2) call. 938 | */ 939 | const char * 940 | mount_strerror (int errsv) 941 | { 942 | switch (errsv) 943 | { 944 | case ENOSPC: 945 | /* "No space left on device" misleads users into thinking there 946 | * is some sort of disk-space problem, but mount(2) uses that 947 | * errno value to mean something more like "limit exceeded". */ 948 | return ("Limit exceeded (ENOSPC). " 949 | "(Hint: Check that /proc/sys/fs/mount-max is sufficient, " 950 | "typically 100000)"); 951 | 952 | default: 953 | return strerror (errsv); 954 | } 955 | } 956 | 957 | /* 958 | * Return a + b if it would not overflow. 959 | * Die with an "out of memory" error if it would. 960 | */ 961 | static size_t 962 | xadd (size_t a, size_t b) 963 | { 964 | #if defined(__GNUC__) && __GNUC__ >= 5 965 | size_t result; 966 | if (__builtin_add_overflow (a, b, &result)) 967 | die_oom (); 968 | return result; 969 | #else 970 | if (a > SIZE_MAX - b) 971 | die_oom (); 972 | 973 | return a + b; 974 | #endif 975 | } 976 | 977 | /* 978 | * Return a * b if it would not overflow. 979 | * Die with an "out of memory" error if it would. 980 | */ 981 | static size_t 982 | xmul (size_t a, size_t b) 983 | { 984 | #if defined(__GNUC__) && __GNUC__ >= 5 985 | size_t result; 986 | if (__builtin_mul_overflow (a, b, &result)) 987 | die_oom (); 988 | return result; 989 | #else 990 | if (b != 0 && a > SIZE_MAX / b) 991 | die_oom (); 992 | 993 | return a * b; 994 | #endif 995 | } 996 | 997 | void 998 | strappend (StringBuilder *dest, const char *src) 999 | { 1000 | size_t len = strlen (src); 1001 | size_t new_offset = xadd (dest->offset, len); 1002 | 1003 | if (new_offset >= dest->size) 1004 | { 1005 | dest->size = xmul (xadd (new_offset, 1), 2); 1006 | dest->str = xrealloc (dest->str, dest->size); 1007 | } 1008 | 1009 | /* Preserves the invariant that dest->str is always null-terminated, even 1010 | * though the offset is positioned at the null byte for the next write. 1011 | */ 1012 | strncpy (dest->str + dest->offset, src, len + 1); 1013 | dest->offset = new_offset; 1014 | } 1015 | 1016 | __attribute__((format (printf, 2, 3))) 1017 | void 1018 | strappendf (StringBuilder *dest, const char *fmt, ...) 1019 | { 1020 | va_list args; 1021 | int len; 1022 | size_t new_offset; 1023 | 1024 | va_start (args, fmt); 1025 | len = vsnprintf (dest->str + dest->offset, dest->size - dest->offset, fmt, args); 1026 | va_end (args); 1027 | if (len < 0) 1028 | die_with_error ("vsnprintf"); 1029 | new_offset = xadd (dest->offset, len); 1030 | if (new_offset >= dest->size) 1031 | { 1032 | dest->size = xmul (xadd (new_offset, 1), 2); 1033 | dest->str = xrealloc (dest->str, dest->size); 1034 | va_start (args, fmt); 1035 | len = vsnprintf (dest->str + dest->offset, dest->size - dest->offset, fmt, args); 1036 | va_end (args); 1037 | if (len < 0) 1038 | die_with_error ("vsnprintf"); 1039 | } 1040 | 1041 | dest->offset = new_offset; 1042 | } 1043 | 1044 | void 1045 | strappend_escape_for_mount_options (StringBuilder *dest, const char *src) 1046 | { 1047 | bool unescaped = true; 1048 | 1049 | for (;;) 1050 | { 1051 | if (dest->offset == dest->size) 1052 | { 1053 | dest->size = MAX (64, xmul (dest->size, 2)); 1054 | dest->str = xrealloc (dest->str, dest->size); 1055 | } 1056 | switch (*src) 1057 | { 1058 | case '\0': 1059 | dest->str[dest->offset] = '\0'; 1060 | return; 1061 | 1062 | case '\\': 1063 | case ',': 1064 | case ':': 1065 | if (unescaped) 1066 | { 1067 | dest->str[dest->offset++] = '\\'; 1068 | unescaped = false; 1069 | continue; 1070 | } 1071 | /* else fall through */ 1072 | 1073 | default: 1074 | dest->str[dest->offset++] = *src; 1075 | unescaped = true; 1076 | break; 1077 | } 1078 | src++; 1079 | } 1080 | } 1081 | -------------------------------------------------------------------------------- /utils.h: -------------------------------------------------------------------------------- 1 | /* bubblewrap 2 | * Copyright (C) 2016 Alexander Larsson 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library. If not, see . 17 | * 18 | */ 19 | 20 | #pragma once 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | #if 0 37 | #define debug(...) bwrap_log (LOG_DEBUG, __VA_ARGS__) 38 | #else 39 | #define debug(...) 40 | #endif 41 | 42 | #define UNUSED __attribute__((__unused__)) 43 | 44 | #define N_ELEMENTS(arr) (sizeof (arr) / sizeof ((arr)[0])) 45 | 46 | #ifndef TEMP_FAILURE_RETRY 47 | #define TEMP_FAILURE_RETRY(expression) \ 48 | (__extension__ \ 49 | ({ long int __result; \ 50 | do __result = (long int) (expression); \ 51 | while (__result == -1L && errno == EINTR); \ 52 | __result; })) 53 | #endif 54 | 55 | #define PIPE_READ_END 0 56 | #define PIPE_WRITE_END 1 57 | 58 | #ifndef PR_SET_CHILD_SUBREAPER 59 | #define PR_SET_CHILD_SUBREAPER 36 60 | #endif 61 | 62 | extern bool bwrap_level_prefix; 63 | 64 | void bwrap_log (int severity, 65 | const char *format, 66 | ...) __attribute__((format (printf, 2, 3))); 67 | #define warn(...) bwrap_log (LOG_WARNING, __VA_ARGS__) 68 | 69 | void die_with_error (const char *format, 70 | ...) __attribute__((__noreturn__)) __attribute__((format (printf, 1, 2))); 71 | void die_with_mount_error (const char *format, 72 | ...) __attribute__((__noreturn__)) __attribute__((format (printf, 1, 2))); 73 | void die (const char *format, 74 | ...) __attribute__((__noreturn__)) __attribute__((format (printf, 1, 2))); 75 | void die_oom (void) __attribute__((__noreturn__)); 76 | void die_unless_label_valid (const char *label); 77 | 78 | void fork_intermediate_child (void); 79 | 80 | void *xmalloc (size_t size); 81 | void *xcalloc (size_t nmemb, size_t size); 82 | void *xrealloc (void *ptr, 83 | size_t size); 84 | char *xstrdup (const char *str); 85 | void strfreev (char **str_array); 86 | void xclearenv (void); 87 | void xsetenv (const char *name, 88 | const char *value, 89 | int overwrite); 90 | void xunsetenv (const char *name); 91 | char *strconcat (const char *s1, 92 | const char *s2); 93 | char *strconcat3 (const char *s1, 94 | const char *s2, 95 | const char *s3); 96 | char * xasprintf (const char *format, 97 | ...) __attribute__((format (printf, 1, 2))); 98 | bool has_prefix (const char *str, 99 | const char *prefix); 100 | bool has_path_prefix (const char *str, 101 | const char *prefix); 102 | bool path_equal (const char *path1, 103 | const char *path2); 104 | int fdwalk (int proc_fd, 105 | int (*cb)(void *data, 106 | int fd), 107 | void *data); 108 | char *load_file_data (int fd, 109 | size_t *size); 110 | char *load_file_at (int dirfd, 111 | const char *path); 112 | int write_file_at (int dirfd, 113 | const char *path, 114 | const char *content); 115 | int write_to_fd (int fd, 116 | const char *content, 117 | ssize_t len); 118 | int copy_file_data (int sfd, 119 | int dfd); 120 | int copy_file (const char *src_path, 121 | const char *dst_path, 122 | mode_t mode); 123 | int create_file (const char *path, 124 | mode_t mode, 125 | const char *content); 126 | int ensure_file (const char *path, 127 | mode_t mode); 128 | int ensure_dir (const char *path, 129 | mode_t mode); 130 | int get_file_mode (const char *pathname); 131 | int mkdir_with_parents (const char *pathname, 132 | mode_t mode, 133 | bool create_last); 134 | void create_pid_socketpair (int sockets[2]); 135 | void send_pid_on_socket (int socket); 136 | int read_pid_from_socket (int socket); 137 | char *get_oldroot_path (const char *path); 138 | char *get_newroot_path (const char *path); 139 | char *readlink_malloc (const char *pathname); 140 | 141 | /* syscall wrappers */ 142 | int raw_clone (unsigned long flags, 143 | void *child_stack); 144 | int pivot_root (const char *new_root, 145 | const char *put_old); 146 | char *label_mount (const char *opt, 147 | const char *mount_label); 148 | int label_exec (const char *exec_label); 149 | int label_create_file (const char *file_label); 150 | 151 | const char *mount_strerror (int errsv); 152 | 153 | static inline void 154 | cleanup_freep (void *p) 155 | { 156 | void **pp = (void **) p; 157 | 158 | if (*pp) 159 | free (*pp); 160 | } 161 | 162 | static inline void 163 | cleanup_strvp (void *p) 164 | { 165 | void **pp = (void **) p; 166 | 167 | strfreev (*pp); 168 | } 169 | 170 | static inline void 171 | cleanup_fdp (int *fdp) 172 | { 173 | int fd; 174 | 175 | assert (fdp); 176 | 177 | fd = *fdp; 178 | if (fd != -1) 179 | (void) close (fd); 180 | } 181 | 182 | #define cleanup_free __attribute__((cleanup (cleanup_freep))) 183 | #define cleanup_fd __attribute__((cleanup (cleanup_fdp))) 184 | #define cleanup_strv __attribute__((cleanup (cleanup_strvp))) 185 | 186 | static inline void * 187 | steal_pointer (void *pp) 188 | { 189 | void **ptr = (void **) pp; 190 | void *ref; 191 | 192 | ref = *ptr; 193 | *ptr = NULL; 194 | 195 | return ref; 196 | } 197 | 198 | /* type safety */ 199 | #define steal_pointer(pp) \ 200 | (0 ? (*(pp)) : (steal_pointer) (pp)) 201 | 202 | typedef struct _StringBuilder StringBuilder; 203 | 204 | struct _StringBuilder 205 | { 206 | char * str; 207 | size_t size; 208 | size_t offset; 209 | }; 210 | 211 | void strappend (StringBuilder *dest, 212 | const char *src); 213 | void strappendf (StringBuilder *dest, 214 | const char *fmt, 215 | ...); 216 | void strappend_escape_for_mount_options (StringBuilder *dest, 217 | const char *src); 218 | --------------------------------------------------------------------------------