├── .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 | <one line to give the library's name and a brief idea of what it does.>
453 | Copyright (C) <year> <name of author>
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 | <signature of Ty Coon>, 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 `<stdbool.h>` 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 <https://github.com/containers/bubblewrap/releases>
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 | 
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 <http://www.gnu.org/licenses/>.
17 | *
18 | */
19 |
20 | #include "config.h"
21 |
22 | #include <sys/mount.h>
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 <http://www.gnu.org/licenses/>.
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 <<EOF
13 | Usage: see source code
14 | EOF
15 | exit "${1-2}"
16 | }
17 |
18 | opt_clang=
19 |
20 | getopt_temp="help"
21 | getopt_temp="$getopt_temp,clang"
22 |
23 | getopt_temp="$(getopt -o '' --long "${getopt_temp}" -n "$0" -- "$@")"
24 | eval set -- "$getopt_temp"
25 | unset getopt_temp
26 |
27 | while true; do
28 | case "$1" in
29 | (--clang)
30 | clang=yes
31 | shift
32 | ;;
33 |
34 | (--help)
35 | usage 0
36 | # not reached
37 | ;;
38 |
39 | (--)
40 | shift
41 | break
42 | ;;
43 |
44 | (*)
45 | echo 'Error parsing options' >&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<<EOF
62 | [Application]
63 | name=org.gnome.Weather
64 | runtime=runtime/org.gnome.Platform/x86_64/master
65 | EOF
66 |
--------------------------------------------------------------------------------
/demos/flatpak.bpf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/containers/bubblewrap/9ca3b05ec787acfb4b17bed37db5719fa777834f/demos/flatpak.bpf
--------------------------------------------------------------------------------
/demos/userns-block-fd.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import os, select, subprocess, sys, json
4 |
5 | pipe_info = os.pipe()
6 | userns_block = os.pipe()
7 |
8 | pid = os.fork()
9 |
10 | if pid != 0:
11 | os.close(pipe_info[1])
12 | os.close(userns_block[0])
13 |
14 | select.select([pipe_info[0]], [], [])
15 |
16 | data = json.load(os.fdopen(pipe_info[0]))
17 | child_pid = str(data['child-pid'])
18 |
19 | subprocess.call(["newuidmap", child_pid, "0", str(os.getuid()), "1"])
20 | subprocess.call(["newgidmap", child_pid, "0", str(os.getgid()), "1"])
21 |
22 | os.write(userns_block[1], b'1')
23 | else:
24 | os.close(pipe_info[0])
25 | os.close(userns_block[1])
26 |
27 | os.set_inheritable(pipe_info[1], True)
28 | os.set_inheritable(userns_block[0], True)
29 |
30 | args = ["bwrap",
31 | "bwrap",
32 | "--unshare-all",
33 | "--unshare-user",
34 | "--userns-block-fd", "%i" % userns_block[0],
35 | "--info-fd", "%i" % pipe_info[1],
36 | "--bind", "/", "/",
37 | "cat", "/proc/self/uid_map"]
38 |
39 | os.execlp(*args)
40 |
--------------------------------------------------------------------------------
/meson.build:
--------------------------------------------------------------------------------
1 | project(
2 | 'bubblewrap',
3 | 'c',
4 | version : '0.11.0',
5 | meson_version : '>=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 <http://www.gnu.org/licenses/>.
17 | *
18 | */
19 |
20 | #include "config.h"
21 |
22 | #include <arpa/inet.h>
23 | #include <net/if.h>
24 | #include <netinet/in.h>
25 | #include <linux/loop.h>
26 | #include <linux/netlink.h>
27 | #include <linux/rtnetlink.h>
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 <http://www.gnu.org/licenses/>.
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 <walters@verbum.org>
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)#39; | 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 <walters@verbum.org>
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: =#39;
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#39; caps.test; then
204 | assert_not_file_has_content caps.test '^Current: =.*cap_kill.*+eip#39;
205 | assert_not_file_has_content caps.test '^Current: =.*cap_fowner.*+eip#39;
206 | assert_file_has_content caps.test '^Current: =.*cap_net_bind_service.*+eip#39;
207 | else
208 | assert_file_has_content caps.test '^Current: =eip.*cap_kill.*-eip#39;
209 | assert_file_has_content caps.test '^Current: =eip.*cap_fowner.*-eip#39;
210 | assert_not_file_has_content caps.test '^Current: =.*cap_net_bind_service.*-eip#39;
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 <<EOF
218 | #!/usr/bin/env python3
219 | import struct,fcntl,sys
220 | path = sys.argv[1]
221 | if sys.argv[2] == 'wait':
222 | locktype = fcntl.F_SETLKW
223 | else:
224 | locktype = fcntl.F_SETLK
225 | lockdata = struct.pack("hhqqhh", fcntl.F_WRLCK, 0, 0, 0, 0, 0)
226 | fd=open(sys.argv[1], 'a')
227 | try:
228 | fcntl.fcntl(fd.fileno(), locktype, lockdata)
229 | except IOError as e:
230 | sys.exit(1)
231 | sys.exit(0)
232 | EOF
233 | chmod a+x lockf-n.py
234 | touch lock
235 |
236 | for die_with_parent_argv in "--die-with-parent" "--die-with-parent --unshare-pid"; do
237 | # We have to loop here, because bwrap doesn't wait for the lock if
238 | # another process is holding it. If we're unlucky, lockf-n.py will
239 | # be holding it.
240 | bash -c "while true; do $RUN ${die_with_parent_argv} --lock-file $(pwd)/lock sleep 1h; done" &
241 | childshellpid=$!
242 |
243 | # Wait for lock to be taken (yes hacky)
244 | for x in $(seq 10); do
245 | if ./lockf-n.py ./lock nowait; then
246 | sleep 1
247 | else
248 | break
249 | fi
250 | done
251 | if ./lockf-n.py ./lock nowait; then
252 | assert_not_reached "timed out waiting for lock"
253 | fi
254 |
255 | # Kill the shell, which should kill bwrap (and the sleep)
256 | kill -9 ${childshellpid}
257 | # Lock file should be unlocked
258 | ./lockf-n.py ./lock wait
259 | ok "die with parent ${die_with_parent_argv}"
260 | done
261 |
262 | printf '%s--dir\0/tmp/hello/world\0' '' > 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<test.args 4<test.args2 5<test.args3
266 | ok "we can parse arguments from a fd"
267 |
268 | mkdir bin
269 | echo "#!/bin/sh" > 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#39;
372 | command stat -c '%a' new-dir-set-perms > new-dir-permissions
373 | assert_file_has_content new-dir-permissions '^1741#39;
374 | command stat -c '%a' dir-already-existed > dir-permissions
375 | assert_file_has_content dir-permissions '^710#39;
376 | command stat -c '%a' dir-already-existed2 > dir-permissions
377 | assert_file_has_content dir-permissions '^754#39;
378 | command stat -c '%a' dir-chmod > dir-permissions
379 | assert_file_has_content dir-permissions '^1755#39;
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#39;
412 | command stat -c '%a' parent-of-1777 > dir-permissions
413 | assert_file_has_content dir-permissions '^755#39;
414 | command stat -c '%a' parent-of-0755 > dir-permissions
415 | assert_file_has_content dir-permissions '^755#39;
416 | command stat -c '%a' parent-of-0644 > dir-permissions
417 | assert_file_has_content dir-permissions '^755#39;
418 | command stat -c '%a' parent-of-0750 > dir-permissions
419 | assert_file_has_content dir-permissions '^750#39;
420 | command stat -c '%a' parent-of-0710 > dir-permissions
421 | assert_file_has_content dir-permissions '^750#39;
422 | command stat -c '%a' parent-of-0720 > dir-permissions
423 | assert_file_has_content dir-permissions '^750#39;
424 | command stat -c '%a' parent-of-0640 > dir-permissions
425 | assert_file_has_content dir-permissions '^750#39;
426 | command stat -c '%a' parent-of-0700 > dir-permissions
427 | assert_file_has_content dir-permissions '^700#39;
428 | command stat -c '%a' parent-of-0600 > dir-permissions
429 | assert_file_has_content dir-permissions '^700#39;
430 | command stat -c '%a' parent-of-0705 > dir-permissions
431 | assert_file_has_content dir-permissions '^705#39;
432 | command stat -c '%a' parent-of-0604 > dir-permissions
433 | assert_file_has_content dir-permissions '^705#39;
434 | command stat -c '%a' parent-of-0000 > dir-permissions
435 | assert_file_has_content dir-permissions '^700#39;
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#39;
447 | $RUN \
448 | --tmpfs "$(pwd -P)" \
449 | stat -c '%a' "$(pwd -P)" > dir-permissions
450 | assert_file_has_content dir-permissions '^755#39;
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#39;
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#39;
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#39;
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#39;
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#39;
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#39;
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#39;
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#39;
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#39;
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#39;
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#39;
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#39;
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#39;
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#39;
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#39;
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#39;
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#39;
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#39;
619 | assert_file_has_content upper/b '^5#39;
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#39;
630 | assert_file_has_content 'lower ":,\'/a '^1#39;
631 | assert_file_has_content 'upper ":,\'/a '^2#39;
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#39;
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#39;
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#39;
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#39;
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#39;
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#39;
654 | assert_file_has_content lower1/b '^2#39;
655 | assert_file_has_content lower2/b '^3#39;
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#39;
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#39;
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#39;
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 <http://www.gnu.org/licenses/>.
18 | */
19 |
20 | #include <stdarg.h>
21 | #include <stdint.h>
22 | #include <stdio.h>
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 <errno.h>
16 | #include <stdio.h>
17 | #include <string.h>
18 | #include <unistd.h>
19 | #include <sys/ioctl.h>
20 | #include <sys/prctl.h>
21 | #include <sys/socket.h>
22 | #include <sys/syscall.h>
23 | #include <sys/stat.h>
24 | #include <sys/types.h>
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 <http://www.gnu.org/licenses/>.
17 | *
18 | */
19 | #include "config.h"
20 |
21 | #include "utils.h"
22 | #include <limits.h>
23 | #include <stdint.h>
24 | #include <sys/syscall.h>
25 | #include <sys/socket.h>
26 | #include <sys/param.h>
27 | #ifdef HAVE_SELINUX
28 | #include <selinux/selinux.h>
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 <http://www.gnu.org/licenses/>.
17 | *
18 | */
19 |
20 | #pragma once
21 |
22 | #include <assert.h>
23 | #include <dirent.h>
24 | #include <errno.h>
25 | #include <fcntl.h>
26 | #include <stdarg.h>
27 | #include <stdbool.h>
28 | #include <stdio.h>
29 | #include <stdlib.h>
30 | #include <string.h>
31 | #include <syslog.h>
32 | #include <unistd.h>
33 | #include <sys/types.h>
34 | #include <sys/stat.h>
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 |
--------------------------------------------------------------------------------