├── .gitignore ├── .vscode ├── .gitignore └── settings.json.in ├── AUTHORS ├── CODEOWNERS ├── CONTRIBUTING.md ├── CONTRIBUTORS ├── Cargo.toml ├── INSTALL.md ├── LICENSE ├── Makefile.in ├── NEWS.md ├── README.md ├── admin ├── lint │ ├── checks.go │ └── lint.go ├── make-linux-pkg.sh ├── make-macos-pkg.sh ├── org.bazelbuild.sandboxfs.setup-osxfuse.plist ├── pre-commit ├── setup-osxfuse.sh ├── travis-build.sh └── travis-install.sh ├── build.rs ├── configure ├── go.mod ├── integration ├── cli_test.go ├── doc.go ├── layout_test.go ├── main_test.go ├── nesting_test.go ├── options_test.go ├── profiling_test.go ├── read_only_test.go ├── read_write_test.go ├── reconfiguration_test.go ├── signal_test.go └── utils │ ├── checks.go │ ├── config.go │ ├── doc.go │ ├── exec.go │ ├── time_darwin.go │ ├── time_linux.go │ ├── unmount_darwin.go │ ├── unmount_linux.go │ ├── user.go │ ├── xattr_darwin.go │ └── xattr_linux.go ├── man └── sandboxfs.1 └── src ├── concurrent.rs ├── errors.rs ├── lib.rs ├── main.rs ├── nodes ├── caches.rs ├── conv.rs ├── dir.rs ├── file.rs ├── mod.rs └── symlink.rs ├── profiling.rs ├── reconfig.rs └── testutils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | .*.orig 2 | .*.rej 3 | .*.swp 4 | .*~ 5 | /.gopath 6 | /.gopath-tools 7 | /bazel-* 8 | Cargo.lock 9 | Makefile 10 | go.sum 11 | target 12 | -------------------------------------------------------------------------------- /.vscode/.gitignore: -------------------------------------------------------------------------------- 1 | settings.json 2 | -------------------------------------------------------------------------------- /.vscode/settings.json.in: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": true, 3 | "editor.rulers": [80], 4 | "editor.tabSize": 2, 5 | "editor.wordWrapColumn": 80, 6 | 7 | "files.associations": { 8 | "sandboxfs.1": "troff", 9 | "settings.json.in": "json" 10 | }, 11 | "files.exclude": { 12 | "**/.DS_Store": true, 13 | "**/.git": true, 14 | ".gopath-tools/**": true, 15 | ".gopath/**": true, 16 | "bazel-*": true, 17 | "settings.json": true 18 | }, 19 | "files.insertFinalNewline": true, 20 | "files.trimTrailingWhitespace": true, 21 | 22 | "go.formatOnSave": true, 23 | "go.gopath": "__GOPATH__", 24 | "go.goroot": "__GOROOT__", 25 | "go.toolsGopath": "__TOOLS_GOPATH__", 26 | "go.useLanguageServer": true, 27 | 28 | "rust.cfg_test": true, 29 | "rust.clippy_preference": "on", 30 | 31 | "[go]": { 32 | "editor.insertSpaces": false, 33 | "editor.rulers": [100], 34 | "editor.tabSize": 8, 35 | "editor.wordWrapColumn": 100 36 | }, 37 | 38 | "[makefile]": { 39 | "editor.insertSpaces": false, 40 | "editor.tabSize": 8 41 | }, 42 | 43 | "[markdown]": { 44 | "editor.quickSuggestions": false 45 | }, 46 | 47 | "markdownlint.config": { 48 | "MD007": { 49 | "indent": 4 50 | }, 51 | "MD025": { 52 | "level": 7 53 | }, 54 | "MD026": { 55 | "punctuation": ".,:;'" 56 | }, 57 | "MD030": { 58 | "ol_single": 2, 59 | "ol_multi": 2, 60 | "ul_single": 3, 61 | "ul_multi": 3 62 | } 63 | }, 64 | 65 | "[rust]": { 66 | "editor.rulers": [100], 67 | "editor.wordWrapColumn": 100 68 | }, 69 | 70 | "[troff]": { 71 | "editor.quickSuggestions": false 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of sandboxfs authors for copyright purposes. 2 | # 3 | # This file is distinct from the CONTRIBUTORS files; see the latter for 4 | # an explanation. 5 | # 6 | # Names are sorted alphabetically and should be added to this file as: 7 | # 8 | # * Name 9 | # * Organization 10 | 11 | * Google Inc. 12 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Admin 2 | * @jmmv 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Want to contribute? Great! First, read this page in its entirety. 4 | 5 | ## Before you contribute 6 | 7 | Before we can use your code, you must sign the [Google Individual Contributor 8 | License Agreement](https://cla.developers.google.com/about/google-individual) 9 | (CLA), which you can do online. 10 | 11 | The CLA is necessary mainly because you own the copyright to your changes, even 12 | after your contribution becomes part of our codebase, so we need your 13 | permission to use and distribute your code. We also need to be sure of various 14 | other things: for instance that you'll tell us if you know that your code 15 | infringes on other people's patents. You don't have to sign the CLA until 16 | after you've submitted your code for review and a member has approved it, but 17 | you must do it before we can put your code into our codebase. 18 | 19 | Contributions made by corporations are covered by a different agreement than 20 | the one above, the [Software Grant and Corporate Contributor License 21 | Agreement](https://cla.developers.google.com/about/google-corporate). 22 | 23 | Before you start working on a larger contribution, you should get in touch with 24 | us first through the issue tracker with your idea so that we can help out and 25 | possibly guide you. Coordinating up front makes it much easier to avoid 26 | frustration later on. 27 | 28 | ## Project setup 29 | 30 | In order to contribute to sandboxfs, you *must* use the Bazel build system, as 31 | this integrates with a variety of tools that you will need during development. 32 | Read the [installation instructions](INSTALL.md) for details on how to get 33 | started. 34 | 35 | Once you have Bazel installed, you *must* run the `./configure` script to 36 | prepare your source tree for development. In its simplest form, this will 37 | install necessary Git hooks into the current workspace. Failure to do so will 38 | potentially result in commits that are not up to the standards that we expect 39 | and delay your code reviews. 40 | 41 | ## IDE support 42 | 43 | sandboxfs' build scripts integrate well with the [Visual Studio Code 44 | (VSCode)](https://code.visualstudio.com/) editor. 45 | 46 | To enable support for VSCode, run `./configure --enable-vscode`. This will 47 | generate a `settings.json` file for the project that points to the right 48 | locations of the Go tools and will also create a fake `GOPATH` that the tools 49 | can consume. 50 | 51 | ## Code formatting and linting 52 | 53 | At commit time, our pre-commit script will verify that any changes you have 54 | made comply with the expected coding style. If there are any problems, you 55 | will see a report on the command-line with the specifics. 56 | 57 | If you want to run the style check by hand, you can invoke it with the 58 | `make lint` command. 59 | 60 | ## Code reviews 61 | 62 | All submissions, including submissions by project members, require review. 63 | We use GitHub pull requests for this purpose. 64 | 65 | Be aware that the copy of sandboxfs in GitHub is not *yet* primary: all 66 | changes submitted via GitHub bugs or pull requests will be manually applied 67 | to the Google-internal tree, reviewed there, and then reexported to GitHub 68 | (which means your commit IDs will change, but attribution should not). Our 69 | goal is to make GitHub the primary copy, but we are not there yet! 70 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This is the list of people who have agreed to one of the CLAs and can 2 | # contribute patches to the sandboxfs project. 3 | # 4 | # The AUTHORS file lists the copyright holders; this file lists people. 5 | # For example: Google employees are listed here but not in AUTHORS 6 | # because Google holds the copyright. 7 | # 8 | # See the CONTRIBUTING.md file for more details on the CLA. 9 | # 10 | # Names are sorted by last name and should be added as: 11 | # 12 | # * Name 13 | 14 | * Pallav Agarwal 15 | * Julio Merino 16 | * Phillip Wollermann 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Julio Merino "] 3 | categories = ["filesystems"] 4 | description = "A virtual file system for sandboxing" 5 | homepage = "https://github.com/bazelbuild/sandboxfs" 6 | keywords = ["bazel", "filesystem", "fuse", "sandboxing"] 7 | license = "Apache-2.0" 8 | name = "sandboxfs" 9 | readme = "README.md" 10 | repository = "https://github.com/bazelbuild/sandboxfs" 11 | version = "0.2.0" 12 | 13 | [badges] 14 | travis-ci = { repository = "bazelbuild/sandboxfs", branch = "master" } 15 | 16 | [features] 17 | default = [] 18 | profiling = ["cpuprofiler"] 19 | 20 | [dependencies] 21 | cpuprofiler = { version = "0.0", optional = true } 22 | env_logger = "0.5" 23 | failure = "~0.1.2" 24 | fuse = "0.3" 25 | getopts = "0.2" 26 | log = "0.4" 27 | nix = "0.12" 28 | num_cpus = "1.11" 29 | serde = "1.0" 30 | serde_derive = "1.0" 31 | serde_json = "1.0" 32 | signal-hook = "0.1" 33 | threadpool = "1.0" 34 | time = "0.1" 35 | xattr = "0.2.2" 36 | 37 | [build-dependencies] 38 | pkg-config = "0.3" 39 | 40 | [dev-dependencies] 41 | tempfile = "3" 42 | users = "0.9" 43 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installation instructions 2 | 3 | ## Using the macOS installer 4 | 5 | 1. [Download and install OSXFUSE](https://osxfuse.github.io/). 6 | 7 | 1. Download the `sandboxfs---macos.pkg` file attached to the 8 | latest release in the 9 | [releases page](https://github.com/bazelbuild/sandboxfs/releases). 10 | 11 | 1. Double-click the downloaded file and follow the instructions. 12 | 13 | Should you want to uninstall sandboxfs at any point, you can run 14 | `/usr/local/share/sandboxfs/uninstall.sh` to cleanly remove all installed 15 | files. 16 | 17 | ## Using the generic Linux pre-built binaries 18 | 19 | If your Linux distribution does not provide sandboxfs packages on its own, 20 | you can try using the prebuilt versions we supply. These assume that you 21 | have certain versions of shared libraries in the right location, so your 22 | mileage may vary when attempting to use these. If the binaries do not 23 | work, you'll have to build sandboxfs from the sources as described later on. 24 | 25 | 1. Install FUSE. The specific steps will depend on your distribution but 26 | here are some common examples: 27 | 28 | * Debian, Ubuntu: `apt install libfuse2`. 29 | * Fedora: `dnf install fuse-libs`. 30 | 31 | 1. Download the `sandboxfs---linux-.tgz` file attached 32 | to the latest release in the 33 | [releases page](https://github.com/bazelbuild/sandboxfs/releases). 34 | 35 | 1. Extract the downloaded file in the prefix where you want sandboxfs to 36 | be available. The most common location for a system-wide installation 37 | is `/usr/local` so do: 38 | 39 | ``` 40 | tar xzv -C /usr/local -f .../path/to/downloaded.tgz 41 | ``` 42 | 43 | You can also install sandboxfs under your home directory if you do not 44 | have administrator privileges, for example under `~/local`. 45 | 46 | 1. Verify that the binary can start by running it at least once. If it 47 | does start (because it can find all required shared libraries), you can 48 | be fairly confident that it will work. 49 | 50 | Should you want to uninstall sandboxfs at any point, you can run 51 | `.../share/sandboxfs/uninstall.sh` from the prefix in which you unpacked the 52 | pre-built binaries to cleanly remove all installed files. 53 | 54 | ## From crates.io 55 | 56 | 1. [Download and install Rust](https://www.rust-lang.org/). If you already 57 | had it installed, make sure you are on a new-enough version by running 58 | `rustup update`. 59 | 60 | 1. Install FUSE and pkg-config. The specific steps will depend on your 61 | system but here are some common examples: 62 | 63 | * Debian, Ubuntu: `apt install libfuse-dev pkg-config`. 64 | * Fedora: `dnf install fuse-devel pkgconf-pkg-config`. 65 | * macOS: Visit the [OSXFUSE](https://osxfuse.github.io/) homepage and 66 | try `brew install pkg-config`. 67 | 68 | 1. Run `cargo install sandboxfs`. 69 | 70 | ## From a GitHub checkout 71 | 72 | 1. [Download and install Rust](https://www.rust-lang.org/). If you already 73 | had it installed, make sure you are on a new-enough version by running 74 | `rustup update`. 75 | 76 | 1. Download and install FUSE for your system. On Linux this will vary 77 | on a distribution basis, and on macOS you can [install 78 | OSXFUSE](https://osxfuse.github.io/). 79 | 80 | 1. Download and install `pkg-config` or `pkgconf`. 81 | 82 | 1. Run `./configure` to generate the scripts that will allow installation. 83 | 84 | 1. The default installation path is `/usr/local` but you can customize 85 | it to any other location by passing the flag `--prefix=/usr/local`. 86 | 87 | 1. The build scripts will use `cargo` from your path but you can set 88 | a different location by passing the `--cargo=/path/to/cargo` flag. 89 | 90 | 1. The build scripts will use `go` from your path but you can set a 91 | different location by passing the `--goroot=/path/to/goroot` flag. 92 | You can also use the magic `none` value to disable Go usage, but 93 | this will prevent running the integration tests or the code linter. 94 | 95 | 1. Run `make release` to download all required dependencies and build the 96 | final binary. 97 | 98 | 1. You could also run `cargo build --release`. This package is 99 | intended to work just fine with the standard Cargo toolchain. 100 | 101 | 1. Run `make install` to put the built binary and all supporting files 102 | in place. 103 | 104 | * You will (most likely) need superuser permissions to install 105 | under `/usr/local`, so run the previous command with `sudo`. 106 | 107 | ## Profiling support 108 | 109 | sandboxfs has optional support for the gperftools profiling tools. If you have 110 | that package installed, you can pass `--features=profiling` to the `configure` 111 | script and sandboxfs's `--cpu_profile` flag will become functional. 112 | 113 | ## macOS only: Enable "allow other" support in OSXFUSE 114 | 115 | In order to run system binaries within a sandboxfs mount point (which is 116 | the primary goal of using sandboxfs), you must enable OSXFUSE's "allow 117 | other" support; otherwise, necessary core macOS security services [will deny 118 | executions](https://jmmv.dev/2017/10/fighting-execs-sandboxfs-macos.html). 119 | 120 | To do this, run the following for a one-time change: 121 | 122 | sudo sysctl -w vfs.generic.osxfuse.tunables.allow_other=1 123 | 124 | Note that you cannot do this via `/etc/sysctl.conf` because the OSXFUSE 125 | kernel extension has not yet been loaded when this file is parsed. 126 | 127 | The macOS installer configures your system to do this automatically at 128 | every boot by using a launch agent. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile.in: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy 5 | # of the License at: 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | .PHONY: all 16 | all: Makefile debug 17 | 18 | CARGO_FLAGS = --features "$(FEATURES)" 19 | 20 | # TODO(jmmv): Should automatically reinvoke configure... but this is difficult 21 | # because we need to remember the flags originally passed by the user, and 22 | # we need to tell make to reload the Makefile somehow. 23 | Makefile: configure Makefile.in 24 | @echo "Makefile out of date; rerun ./configure with desired args" 25 | @false 26 | 27 | @IS_BMAKE@RUST_SRCS != find src -name "*.rs" 28 | @IS_GNUMAKE@RUST_SRCS = $(shell find src -name "*.rs") 29 | target/debug/sandboxfs: Cargo.toml $(RUST_SRCS) 30 | $(CARGO) build $(CARGO_FLAGS) 31 | 32 | .PHONY: debug 33 | debug: target/debug/sandboxfs 34 | 35 | .PHONY: release 36 | release: target/release/sandboxfs 37 | 38 | target/release/sandboxfs: Cargo.toml $(RUST_SRCS) 39 | $(CARGO) build $(CARGO_FLAGS) --release 40 | 41 | .PHONY: check 42 | check: check-unit check-integration 43 | 44 | check-unit: Cargo.toml $(RUST_SRCS) 45 | RUST_BACKTRACE=full $(CARGO) test $(CARGO_FLAGS) --verbose 46 | 47 | SANDBOXFS_BINARY = $$(pwd)/target/debug/sandboxfs 48 | 49 | @IS_BMAKE@GO_SRCS != find integration -name "*.go" 50 | @IS_GNUMAKE@GO_SRCS = $(shell find integration -name "*.go") 51 | check-integration: target/debug/sandboxfs $(GO_SRCS) 52 | @if [ -n "$(GOROOT)" ]; then \ 53 | set -x; \ 54 | GOPATH=$(GOPATH) GOROOT=$(GOROOT) $(GOROOT)/bin/go test \ 55 | -v -timeout=600s \ 56 | github.com/bazelbuild/sandboxfs/integration \ 57 | -features="$(FEATURES)" \ 58 | -sandboxfs_binary="$(SANDBOXFS_BINARY)" \ 59 | $(CHECK_INTEGRATION_FLAGS); \ 60 | else \ 61 | echo "WARNING: Go not enabled; integration tests not run"; \ 62 | fi 63 | 64 | .PHONY: lint 65 | lint: 66 | $(CARGO) clippy $(CARGO_FLAGS) --all-features --all-targets \ 67 | -- -D warnings 68 | @if [ -n "$(GOROOT)" ]; then \ 69 | set -x; \ 70 | PATH=$(GOPATH)/bin:$${PATH} GOPATH=$(GOPATH) GOROOT=$(GOROOT) \ 71 | $(GOROOT)/bin/go run \ 72 | github.com/bazelbuild/sandboxfs/admin/lint $(LINT_FILES); \ 73 | else \ 74 | echo "WARNING: Go not enabled; linter not run"; \ 75 | fi 76 | 77 | .PHONY: install 78 | install: target/release/sandboxfs 79 | install -m 0755 -d $(DESTDIR)$(PREFIX)/bin 80 | install -m 0755 -c target/release/sandboxfs \ 81 | $(DESTDIR)$(PREFIX)/bin/sandboxfs 82 | install -m 0755 -d $(DESTDIR)$(PREFIX)/share/man/man1 83 | install -m 0644 -c man/sandboxfs.1 $(DESTDIR)$(PREFIX)/share/man/man1 84 | install -m 0755 -d $(DESTDIR)$(PREFIX)/share/doc/sandboxfs 85 | install -m 0644 -c AUTHORS CONTRIBUTING.md CONTRIBUTORS LICENSE \ 86 | NEWS.md README.md $(DESTDIR)$(PREFIX)/share/doc/sandboxfs 87 | 88 | .PHONY: clean 89 | clean: 90 | rm -rf $(CLEANFILES) 91 | 92 | .PHONY: distclean 93 | distclean: clean 94 | rm -rf $(DISTCLEANFILES) 95 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # Major changes between releases 2 | 3 | ## Changes in version X.Y.Z 4 | 5 | **STILL UNDER DEVELOPMENT; NOT RELEASED YET.** 6 | 7 | * Issue #111: Optimized map operations, cutting down their CPU time by 8 | about 15%. 9 | 10 | ## Changes in version 0.2.0 11 | 12 | **Released on 2020-04-20.** 13 | 14 | * The reconfiguration protocol has completely changed in order to support 15 | more efficient reconfigurations. If you are using sandboxfs with Bazel, 16 | you will need to upgrade to Bazel 3.0.0 to use this version. See below for 17 | more details on how the protocol has changed. 18 | 19 | * Made sandboxfs process reconfiguration requests in parallel, which has a 20 | significant performance impact when those requests are large. 21 | 22 | * Fixed a bug where writes on a file descriptor that had been duplicated and 23 | closed did not update the file size, resulting in bad data being returned 24 | on future reads. 25 | 26 | * Fixed timestamp updates so that the `birthtime` rolls back to an older 27 | `mtime` to mimic BSD semantics. 28 | 29 | * Fixed hardlink counts so that they are zero for handles that point to 30 | deleted files or directories. 31 | 32 | * Added support for extended attributes. Must be explicitly enabled by 33 | passing the `--xattrs` option. 34 | 35 | * Added support to change the timestamps of a symlink on systems that have 36 | this feature. 37 | 38 | * Disabled the path-based node cache by default and added a `--node_cache` 39 | flag to reenable it. This fixes crashes when running Java within a 40 | sandboxfs instance where the Java toolchain is mapped under multiple 41 | locations and the first mapped location vanishes. See [The OSXFUSE, hard 42 | links, and dladdr 43 | puzzle](https://jmmv.dev/2020/01/osxfuse-hardlinks-dladdr.html) for 44 | details. 45 | 46 | The following are the highlights of the changes to the reconfiguration protocol 47 | in this release. You can read the full specification in the `sandboxfs(1)` 48 | manual page: 49 | 50 | * Use JSON streams for both the requests and the responses, instead of 51 | the previous ad-hoc line-oriented protocol. 52 | 53 | * Each map and unmap request carries a list of mappings to map and 54 | unmap, respectively, along with the "root" path where all those mappings 55 | start. This is to allow sandboxfs to process the requests more 56 | efficiently. 57 | 58 | * Each request contains a tag, which is then propagated to the response 59 | for that request. This is to allow sandboxfs to process requests in 60 | parallel. 61 | 62 | * Work at the level of sandboxes, not paths, where a sandbox is defined 63 | as a top-level directory with a collection of mappings beneath it. 64 | 65 | This essentially makes reconfigurations less powerful than they were, but 66 | also makes them infinitely simpler to understand and manage. Furthermore, 67 | this lines up better with the needs of Bazel, our primary customer, and 68 | with sandboxfs' own name. 69 | 70 | * Take prefix-encoded paths to minimize the size of the reconfiguration 71 | requests. This has shown to significantly reduce the CPU consumption of 72 | both sandboxfs and Bazel during a build, as the size of the reconfiguration 73 | messages is drastically smaller. 74 | 75 | * Accept short aliases for all fields, thus further minimizing the size 76 | of reconfiguration requests, and also to accept omitting optional fields. 77 | 78 | ## Changes in version 0.1.1 79 | 80 | **Released on 2019-10-24.** 81 | 82 | * Fixed the definition of `--input` and `--output` to require an argument, 83 | which makes `--foo bar` and `--foo=bar` equivalent. This can be thought to 84 | break backwards compatibility but, in reality, it does not. The previous 85 | behavior was just broken: specifying `--foo bar` would cause `bar` to be 86 | treated as an argument and `--foo` to use its default value, which meant 87 | that these two flags would be ignored when supplied under this syntax. 88 | 89 | * Fixed `--input` and `--output` to handle stdin and stdout correctly when 90 | running e.g. under `sudo`. 91 | 92 | * Make create operations honor the UID and GID of the caller user instead of 93 | inheriting the permissions of whoever was running sandboxfs. Only has an 94 | effect when using `--allow=other` or `--allow=root`. 95 | 96 | ## Changes in version 0.1.0 97 | 98 | **Released on 2019-02-05.** 99 | 100 | This is the first formal release of the sandboxfs project. 101 | 102 | **WARNING:** The interaction points with sandboxfs are subject to change at this 103 | point. In particular, the command-line interface and the data format used to 104 | reconfigure sandboxfs while it's running *will* most certainly change. 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sandboxfs: A virtual file system for sandboxing 2 | 3 | sandboxfs is a **FUSE file system** that exposes a combination of multiple 4 | files and directories from the host's file system in the form of a virtual 5 | tree with an arbitrary layout. You can think of a sandbox as an arbitrary 6 | **view into the host's file system** with different access privileges per 7 | directory. 8 | 9 | sandboxfs is **designed to allow running commands** with limited access to 10 | the file system by using the virtual tree as their new root, and to do so 11 | consistently across a variety of platforms. 12 | 13 | sandboxfs is **licensed under the [Apache 2.0 license](LICENSE)** and is 14 | not an official Google product. 15 | 16 | ## Releases 17 | 18 | The latest version of sandboxfs is 0.2.0 and was released on 2020-04-20. 19 | 20 | See the [installation instructions](INSTALL.md) for details on how to build 21 | and install sandboxfs. 22 | 23 | See the [release notes](NEWS.md) file for more details. 24 | 25 | ## Usage 26 | 27 | sandboxfs is fully documented in the `sandboxfs(1)` manual page, which is 28 | located in the [`man/sandboxfs.1`](man/sandboxfs.1) file. You can view a 29 | rendered version of this manual page using the following command after 30 | cloning the tree: 31 | 32 | man ./man/sandboxfs.1 33 | 34 | ## Contributing 35 | 36 | If you'd like to contribute to sandboxfs, there is plenty of work to be 37 | done! Please make sure to read our [contribution guidelines](CONTRIBUTING.md) 38 | to learn about some important prerequisite steps. 39 | -------------------------------------------------------------------------------- /admin/lint/checks.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bufio" 19 | "bytes" 20 | "fmt" 21 | "io" 22 | "log" 23 | "os" 24 | "os/exec" 25 | "path/filepath" 26 | "regexp" 27 | "strings" 28 | ) 29 | 30 | // grep checks whether the given file's contents match the pattern. 31 | func grep(pattern string, file string) (bool, error) { 32 | input, err := os.OpenFile(file, os.O_RDONLY, 0) 33 | if err != nil { 34 | return false, fmt.Errorf("failed to open %s for read: %v", file, err) 35 | } 36 | defer input.Close() 37 | 38 | matched, err := regexp.MatchReader(pattern, bufio.NewReader(input)) 39 | if err != nil { 40 | return false, fmt.Errorf("failed to search for %s in %s: %v", pattern, file, err) 41 | } 42 | 43 | return matched, nil 44 | } 45 | 46 | // checkLicense checks if the given file contains the necessary license information and returns an 47 | // error if this is not true or if the check cannot be performed. 48 | func checkLicense(workspaceDir string, file string) error { 49 | for _, pattern := range []string{ 50 | `Copyright.*Google`, 51 | `Apache License.*2.0`, 52 | } { 53 | matched, err := grep(pattern, file) 54 | if err != nil { 55 | return fmt.Errorf("license check failed for %s: %v", file, err) 56 | } 57 | if !matched { 58 | return fmt.Errorf("license check failed for %s: %s not found", file, pattern) 59 | } 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // checkLineLength checks if the given file contains any lines longer than the given maximum and, if 66 | // it does, returns an error. 67 | func checkLineLength(workspaceDir string, file string, max int) error { 68 | input, err := os.OpenFile(file, os.O_RDONLY, 0) 69 | if err != nil { 70 | return fmt.Errorf("failed to open %s for read: %v", file, err) 71 | } 72 | defer input.Close() 73 | 74 | reader := bufio.NewReader(input) 75 | lineNo := 1 76 | done := false 77 | for !done { 78 | line, err := reader.ReadString('\n') 79 | if err == io.EOF { 80 | done = true 81 | // Fall through to process the last line in case it's not empty (when the 82 | // file didn't end with a newline). 83 | } else if err != nil { 84 | return fmt.Errorf("line length check failed for %s: %v", file, err) 85 | } 86 | line = strings.TrimRight(line, "\n\r") 87 | if len(line) > max { 88 | return fmt.Errorf("line length check failed for %s: line %d exceeds length %d with %d characters", file, lineNo, max, len(line)) 89 | } 90 | lineNo++ 91 | } 92 | 93 | return nil 94 | } 95 | 96 | // checkNoTabs checks if the given file contains any tabs as indentation and, if it does, returns 97 | // an error. 98 | func checkNoTabs(workspaceDir string, file string) error { 99 | input, err := os.OpenFile(file, os.O_RDONLY, 0) 100 | if err != nil { 101 | return fmt.Errorf("failed to open %s for read: %v", file, err) 102 | } 103 | defer input.Close() 104 | 105 | preg := regexp.MustCompile(`^ *\t`) 106 | 107 | reader := bufio.NewReader(input) 108 | lineNo := 1 109 | done := false 110 | for !done { 111 | line, err := reader.ReadString('\n') 112 | if err == io.EOF { 113 | done = true 114 | // Fall through to process the last line in case it's not empty (when the 115 | // file didn't end with a newline). 116 | } else if err != nil { 117 | return fmt.Errorf("no tabs check failed for %s: %v", file, err) 118 | } 119 | if preg.MatchString(line) { 120 | return fmt.Errorf("no tabs check failed for %s: indentation tabs found at line %d", file, lineNo) 121 | } 122 | lineNo++ 123 | } 124 | 125 | return nil 126 | } 127 | 128 | // captureErrorsFromStdout configures the given non-started "cmd" to save its stdout into "output" 129 | // and to print stderr to this process' stderr. 130 | func captureErrorsFromStdout(cmd *exec.Cmd, output *bytes.Buffer) { 131 | cmd.Stdout = output 132 | cmd.Stderr = os.Stderr 133 | } 134 | 135 | // captureErrorsFromStderr configures the given non-started "cmd" to save its stderr into "output" 136 | // and to silence its stdout. 137 | func captureErrorsFromStderr(cmd *exec.Cmd, output *bytes.Buffer) { 138 | cmd.Stdout = nil 139 | cmd.Stderr = output 140 | } 141 | 142 | // runLinter runs a "linting" helper binary that prints diagnostics to some output and whose exit 143 | // status is always true. "captureErrors" takes a lambda to configure the command to save its 144 | // diagnostics to the given buffer, and is used to account for tools that print messages to either 145 | // stdout or stderr. The remaining arguments indicate the full command line to run, including the 146 | // path to the tool as the first argument. The file to check is expected to appear as the last 147 | // argument. 148 | func runLinter(captureErrors func(*exec.Cmd, *bytes.Buffer), toolName string, arg ...string) error { 149 | file := arg[len(arg)-1] 150 | 151 | var output bytes.Buffer 152 | cmd := exec.Command(toolName, arg...) 153 | captureErrors(cmd, &output) 154 | err := cmd.Run() 155 | if err != nil { 156 | return fmt.Errorf("%s check failed for %s: %v", toolName, file, err) 157 | } 158 | if output.Len() > 0 { 159 | fmt.Printf("%s does not pass %s:\n", file, toolName) 160 | fmt.Println(output.String()) 161 | return fmt.Errorf("%s check failed for %s: not compliant", toolName, file) 162 | } 163 | return nil 164 | } 165 | 166 | // checkGoFmt checks if the given file is formatted according to gofmt and, if not, prints a diff 167 | // detailing what's wrong with the file to stdout and returns an error. 168 | func checkGofmt(workspaceDir string, file string) error { 169 | return runLinter(captureErrorsFromStdout, "gofmt", "-d", "-e", "-s", file) 170 | } 171 | 172 | // checkGoLint checks if the given file passes golint checks and, if not, prints diagnostic messages 173 | // to stdout and returns an error. 174 | func checkGolint(workspaceDir string, file string) error { 175 | // Lower confidence levels raise a per-file warning to remind about having a package-level 176 | // docstring... but the warning is issued blindly without checking for the existing of this 177 | // docstring in other packages. 178 | minConfidenceFlag := "-min_confidence=0.3" 179 | 180 | return runLinter(captureErrorsFromStdout, "golint", minConfidenceFlag, file) 181 | } 182 | 183 | // checkManpage checks if the given manual page contains any formatting errors by attempting to 184 | // render it. The output of the rendering is ignored and any errors are printed to stdout, 185 | // returning an error. 186 | func checkManpage(workspaceDir string, file string) error { 187 | return runLinter(captureErrorsFromStderr, "man", file) 188 | } 189 | 190 | // checkAll runs all possible checks on a file. Returns true if all checks pass, and false 191 | // otherwise. Error details are dumped to stderr. 192 | func checkAll(workspaceDir string, file string) bool { 193 | isBuildFile := filepath.Base(file) == "Makefile.in" 194 | 195 | // If a file starts with an upper-case letter, assume it's supporting package documentation 196 | // (all those files in the root directory) and avoid linting it. 197 | isDocumentation := mustMatch(`^[A-Z]`, filepath.Base(file)) && !isBuildFile 198 | 199 | log.Printf("Linting file %s", file) 200 | ok := true 201 | 202 | runCheck := func(checker func(string, string) error, file string) { 203 | if err := checker(workspaceDir, file); err != nil { 204 | fmt.Fprintf(os.Stderr, "%s: %v\n", file, err) 205 | ok = false 206 | } 207 | } 208 | 209 | checkLineLength80 := func(workspaceDir string, file string) error { 210 | return checkLineLength(workspaceDir, file, 80) 211 | } 212 | checkLineLength100 := func(workspaceDir string, file string) error { 213 | return checkLineLength(workspaceDir, file, 100) 214 | } 215 | 216 | if !isDocumentation && filepath.Base(file) != "settings.json.in" { 217 | runCheck(checkLicense, file) 218 | } 219 | 220 | if filepath.Ext(file) == ".go" { 221 | runCheck(checkGofmt, file) 222 | runCheck(checkGolint, file) 223 | } else if filepath.Ext(file) == ".rs" { 224 | runCheck(checkNoTabs, file) 225 | runCheck(checkLineLength100, file) 226 | } else if mustMatch("^\\.[0-9]$", filepath.Ext(file)) { 227 | runCheck(checkManpage, file) 228 | runCheck(checkLineLength80, file) 229 | } else if !isBuildFile { 230 | runCheck(checkNoTabs, file) 231 | runCheck(checkLineLength80, file) 232 | } 233 | 234 | return ok 235 | } 236 | 237 | // mustMatch returns true if the given regular expression matches the string. The regular 238 | // expression is assumed to be valid. 239 | func mustMatch(pattern string, str string) bool { 240 | matched, err := regexp.MatchString(pattern, str) 241 | if err != nil { 242 | panic("invalid regexp") 243 | } 244 | return matched 245 | } 246 | -------------------------------------------------------------------------------- /admin/lint/lint.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | // The lint binary ensures the source tree is correctly formatted. 16 | package main 17 | 18 | import ( 19 | "flag" 20 | "fmt" 21 | "io/ioutil" 22 | "log" 23 | "os" 24 | "path/filepath" 25 | "strings" 26 | ) 27 | 28 | // isBlacklisted returns true if the given filename should not be linted. The candidate must be 29 | // given as a relative path to the workspace directory. 30 | func isBlacklisted(candidate string) bool { 31 | // Skip hidden files as we don't need to run checks on them. (This is not strictly 32 | // true, but it's simpler this way for now and the risk is low given that the hidden 33 | // files we have are trivial.) 34 | if strings.HasPrefix(candidate, ".") { 35 | return true 36 | } 37 | 38 | // Skip the Rust build directory. 39 | if strings.HasPrefix(candidate, "target/") { 40 | return true 41 | } 42 | 43 | base := filepath.Base(candidate) 44 | ext := filepath.Ext(candidate) 45 | 46 | // Only worry about non-generated files. 47 | if base == "Cargo.lock" || base == "Makefile" || base == "go.mod" || base == "go.sum" { 48 | return true 49 | } 50 | 51 | // Skip plist files, which even though are manually written, they have quite a few 52 | // exceptions. 53 | if ext == ".plist" { 54 | return true 55 | } 56 | 57 | return false 58 | } 59 | 60 | // collectFiles scans the given directory recursively and returns the paths to all regular files 61 | // within it as relative paths to the given directory. 62 | func collectFiles(dir string) ([]string, error) { 63 | files := make([]string, 0) 64 | 65 | collector := func(path string, info os.FileInfo, err error) error { 66 | if err != nil { 67 | return err 68 | } 69 | 70 | relative, err := filepath.Rel(dir, path) 71 | if err != nil { 72 | panic(fmt.Sprintf("%s is not within %s but it should have been", path, dir)) 73 | } 74 | if info.Mode()&os.ModeType == 0 { 75 | files = append(files, relative) 76 | } 77 | 78 | return err 79 | } 80 | 81 | return files, filepath.Walk(dir, collector) 82 | } 83 | 84 | func main() { 85 | verbose := flag.Bool("verbose", false, "Enables extra logging") 86 | workspace := flag.String("workspace", ".", "Path to the directory where the source tree lives; used to find source files (symlinks are followed) and to resolve relative paths to sources") 87 | flag.Parse() 88 | 89 | if *verbose { 90 | log.SetOutput(os.Stderr) 91 | } else { 92 | log.SetOutput(ioutil.Discard) 93 | } 94 | 95 | var relFiles []string 96 | if len(flag.Args()) == 0 { 97 | log.Printf("Searching for source files in %s", *workspace) 98 | var err error 99 | relFiles, err = collectFiles(*workspace) 100 | if err != nil { 101 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 102 | os.Exit(1) 103 | } 104 | } else { 105 | for _, file := range flag.Args() { 106 | if filepath.IsAbs(file) { 107 | fmt.Fprintf(os.Stderr, "ERROR: Explicitly-provided file names must be relative; %s was not\n", file) 108 | os.Exit(1) 109 | } 110 | } 111 | relFiles = flag.Args() 112 | } 113 | 114 | files := make([]string, 0, len(relFiles)) 115 | for _, file := range relFiles { 116 | if isBlacklisted(file) { 117 | log.Printf("Skipping linting of %s because it is blacklisted", file) 118 | } else { 119 | files = append(files, filepath.Join(*workspace, file)) 120 | } 121 | } 122 | 123 | failed := false 124 | for _, file := range files { 125 | if !checkAll(*workspace, file) { 126 | failed = true 127 | } 128 | } 129 | if failed { 130 | os.Exit(1) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /admin/make-linux-pkg.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy 6 | # of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | # Builds a self-contained package for Linux. 17 | # 18 | # This assumes the FUSE development libraries and pkg-config are installed. 19 | # 20 | # The package generated by this script is built with a lot of caveats: we 21 | # assume that the required dynamic libraries will be available in the right 22 | # locations on the target machine, and that the binary that we build is 23 | # relocatable and can be installed on any location on the machine (so that 24 | # the user is not forced to install sandboxfs under /usr/local in case they 25 | # don't have root permissions). 26 | # 27 | # All arguments given to this script are delegated to "configure". 28 | 29 | 30 | # Directory name of the running script. 31 | DirName="$(dirname "${0}")" 32 | 33 | 34 | # Base name of the running script. 35 | ProgName="${0##*/}" 36 | 37 | 38 | # Prints the given error message to stderr and exits. 39 | # 40 | # \param ... The message to print. 41 | err() { 42 | echo "${ProgName}: E: $*" 1>&2 43 | exit 1 44 | } 45 | 46 | 47 | # Prints the given informational message to stderr. 48 | # 49 | # \param ... The message to print. 50 | info() { 51 | echo "${ProgName}: I: $*" 1>&2 52 | } 53 | 54 | 55 | # Modifies the fresh sandboxfs installation for our packaging needs. 56 | # 57 | # \param root Path to the new file system root used to build the package. 58 | configure_root() { 59 | local root="${1}"; shift 60 | 61 | mkdir -p "${root}/libexec/sandboxfs" 62 | cat >"${root}/libexec/sandboxfs/uninstall.sh" </dev/null || true 75 | fi 76 | done 77 | EOF 78 | chmod +x "${root}/libexec/sandboxfs/uninstall.sh" 79 | 80 | mkdir -p "${root}/share/sandboxfs" 81 | touch "${root}/share/sandboxfs/manifest" 82 | ( cd "${root}" && find . | sort >>"${root}/share/sandboxfs/manifest" ) 83 | } 84 | 85 | 86 | # Program's entry point. 87 | main() { 88 | [ "$(uname -s)" = Linux ] || err "This script is for Linux only" 89 | [ -x "${DirName}/../configure" ] || err "configure not found; make" \ 90 | "sure to run this from a cloned repository" 91 | 92 | local tempdir 93 | tempdir="$(mktemp -d "${TMPDIR:-/tmp}/${ProgName}.XXXXXX" 2>/dev/null)" \ 94 | || err "Failed to create temporary directory" 95 | trap "rm -rf '${tempdir}'" EXIT 96 | 97 | info "Cloning fresh copy of the source tree" 98 | git clone "${DirName}/.." "${tempdir}/src" 99 | 100 | info "Building and installing into temporary root" 101 | ( 102 | set -e 103 | cd "${tempdir}/src" 104 | ./configure --goroot=none --prefix=/ "${@}" 105 | make release 106 | make install DESTDIR="${tempdir}/root" 107 | ) || err "Build failed" 108 | 109 | info "Preparing temporary root for packaging" 110 | configure_root "${tempdir}/root" 111 | 112 | local version="$(grep ^version "${tempdir}/src/Cargo.toml" \ 113 | | cut -d '"' -f 2)" 114 | local revision="$(date +%Y%m%d)" 115 | local pkgversion="${version}-${revision}" 116 | local pkgfile="sandboxfs-${pkgversion}-linux-$(uname -m).tgz" 117 | 118 | info "Building package ${pkgfile}" 119 | ( cd "${tempdir}/root" && find . ) | sed 's,^,MANIFEST: ,' 120 | tar cz -C "${tempdir}/root" -f "${pkgfile}" . 121 | } 122 | 123 | 124 | main "${@}" 125 | -------------------------------------------------------------------------------- /admin/make-macos-pkg.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy 6 | # of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | # Builds a self-installer package for macOS. 17 | # 18 | # This assumes Cargo, OSXFUSE, and pkg-config are installed. 19 | # 20 | # All arguments given to this script are delegated to "configure". 21 | 22 | 23 | # Directory name of the running script. 24 | DirName="$(dirname "${0}")" 25 | 26 | 27 | # Base name of the running script. 28 | ProgName="${0##*/}" 29 | 30 | 31 | # Prints the given error message to stderr and exits. 32 | # 33 | # \param ... The message to print. 34 | err() { 35 | echo "${ProgName}: E: $*" 1>&2 36 | exit 1 37 | } 38 | 39 | 40 | # Prints the given informational message to stderr. 41 | # 42 | # \param ... The message to print. 43 | info() { 44 | echo "${ProgName}: I: $*" 1>&2 45 | } 46 | 47 | 48 | # Creates the package scripts. 49 | # 50 | # \param dir Directory in which to store the scripts. 51 | write_scripts() { 52 | local dir="${1}"; shift 53 | 54 | mkdir -p "${dir}" 55 | cat >"${dir}/postinstall" <"${root}/etc/paths.d/sandboxfs" <"${root}/usr/local/libexec/sandboxfs/uninstall.sh" </dev/null || true 94 | fi 95 | done 96 | EOF 97 | chmod +x "${root}/usr/local/libexec/sandboxfs/uninstall.sh" 98 | 99 | mkdir -p "${root}/usr/local/share/sandboxfs" 100 | touch "${root}/usr/local/share/sandboxfs/manifest" 101 | ( cd "${root}" && \ 102 | find . | sort >>"${root}/usr/local/share/sandboxfs/manifest" ) 103 | } 104 | 105 | 106 | # Program's entry point. 107 | main() { 108 | [ "$(uname -s)" = Darwin ] || err "This script is for macOS only" 109 | [ -x "${DirName}/../configure" ] || err "configure not found; make" \ 110 | "sure to run this from a cloned repository" 111 | 112 | local tempdir 113 | tempdir="$(mktemp -d "${TMPDIR:-/tmp}/${ProgName}.XXXXXX" 2>/dev/null)" \ 114 | || err "Failed to create temporary directory" 115 | trap "rm -rf '${tempdir}'" EXIT 116 | 117 | export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig # For OSXFUSE. 118 | 119 | info "Cloning fresh copy of the source tree" 120 | git clone "${DirName}/.." "${tempdir}/src" 121 | 122 | info "Building and installing into temporary root" 123 | ( 124 | set -e 125 | cd "${tempdir}/src" 126 | ./configure --goroot=none --prefix=/usr/local "${@}" 127 | make release 128 | make install DESTDIR="${tempdir}/root" 129 | ) || err "Build failed" 130 | 131 | info "Preparing temporary root for packaging" 132 | configure_root "${tempdir}/root" 133 | 134 | local version="$(grep ^version "${tempdir}/src/Cargo.toml" \ 135 | | cut -d '"' -f 2)" 136 | local revision="$(date +%Y%m%d)" 137 | local pkgversion="${version}-${revision}" 138 | local pkgfile="sandboxfs-${pkgversion}-macos.pkg" 139 | 140 | info "Building package ${pkgfile}" 141 | write_scripts "${tempdir}/scripts" 142 | ( cd "${tempdir}/root" && find . ) | sed 's,^,MANIFEST: ,' 143 | pkgbuild \ 144 | --identifier com.github.bazelbuild.sandboxfs \ 145 | --root "${tempdir}/root" \ 146 | --scripts "${tempdir}/scripts" \ 147 | --version "${pkgversion}" \ 148 | "${pkgfile}" 149 | } 150 | 151 | 152 | main "${@}" 153 | -------------------------------------------------------------------------------- /admin/org.bazelbuild.sandboxfs.setup-osxfuse.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | org.bazelbuild.sandboxfs.setup-osxfuse 7 | ProgramArguments 8 | 9 | /usr/local/libexec/sandboxfs/setup-osxfuse.sh 10 | 11 | ProcessType 12 | Background 13 | RunAtLoad 14 | 15 | LaunchOnlyOnce 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /admin/pre-commit: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # Copyright 2016 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy 6 | # of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | exec 1>&2 17 | 18 | # Obtain the list of files from the index (i.e. the files to be committed). 19 | # From the list, resolve renames to their target. 20 | index_files="$(git status --porcelain --untracked-files=no \ 21 | | grep '^[MARC]' | cut -c 4- | sed -e 's,^.* -> ,,' | paste -s -)" 22 | 23 | if [ -n "${index_files}" ]; then 24 | echo "Linting changed files..." 25 | make lint LINT_FILES="${index_files}" || exit 1 26 | fi 27 | -------------------------------------------------------------------------------- /admin/setup-osxfuse.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy 6 | # of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | # Startup script to load OSXFUSE and enable "-o allow_other". 17 | # 18 | # sandboxfs requires this setting in order for binaries mapped within the 19 | # sandbox to work. See the following for more details: 20 | # http://julio.meroh.net/2017/10/fighting-execs-sandboxfs-macos.html 21 | # 22 | # We use a startup script instead of an entry in /etc/sysctl.conf because 23 | # it's easier to manage installation and deinstallation, and because we 24 | # must first ensure OSXFUSE is loaded in order to change its configuration. 25 | 26 | /Library/Filesystems/osxfuse.fs/Contents/Resources/load_osxfuse 27 | /usr/sbin/sysctl -w vfs.generic.osxfuse.tunables.allow_other=1 28 | -------------------------------------------------------------------------------- /admin/travis-build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Copyright 2017 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy 6 | # of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | set -e -u -x 17 | 18 | # Default to no features to avoid cluttering .travis.yml. 19 | : "${FEATURES:=}" 20 | 21 | # Default to no flags to avoid cluttering .travis.yml. 22 | : "${FLAGS:=}" 23 | 24 | rootenv=() 25 | rootenv+=(PATH="${PATH}") 26 | [ "${DO-unset}" = unset ] || rootenv+=(DO="${DO}") 27 | [ "${GOPATH-unset}" = unset ] || rootenv+=(GOPATH="${GOPATH}") 28 | [ "${GOROOT-unset}" = unset ] || rootenv+=(GOROOT="${GOROOT}") 29 | readonly rootenv 30 | 31 | # Verifies that Bazel (our primary customer) integrates well with sandboxfs. 32 | # This is a simple smoke test that builds Bazel with itself: there is no 33 | # guarantee that more complex builds wouldn't fail due to sandboxfs bugs. 34 | do_bazel() { 35 | ./configure --cargo="${HOME}/.cargo/bin/cargo" --features="${FEATURES}" \ 36 | --goroot=none 37 | make release 38 | ( cd bazel && bazel \ 39 | build \ 40 | --experimental_use_sandboxfs \ 41 | --experimental_sandboxfs_path="$(pwd)/../target/release/sandboxfs" \ 42 | --spawn_strategy=sandboxed \ 43 | //src:bazel ) 44 | ./bazel/bazel-bin/src/bazel help 45 | } 46 | 47 | # Verifies that the "make install" procedure works and respects both the 48 | # user-supplied prefix and destdir. 49 | do_install() { 50 | ./configure --cargo="${HOME}/.cargo/bin/cargo" --features="${FEATURES}" \ 51 | --goroot=none --prefix="/opt/sandboxfs" 52 | make release 53 | make install DESTDIR="$(pwd)/destdir" 54 | test -x destdir/opt/sandboxfs/bin/sandboxfs 55 | test -e destdir/opt/sandboxfs/share/man/man1/sandboxfs.1 56 | test -e destdir/opt/sandboxfs/share/doc/sandboxfs/README.md 57 | } 58 | 59 | # Ensures that the source tree is sane according to our coding style. 60 | do_lint() { 61 | ./configure --cargo="${HOME}/.cargo/bin/cargo" --features="${FEATURES}" 62 | make lint 63 | } 64 | 65 | # Builds the Linux binary distribution and verifies that it works. 66 | do_linux_pkg() { 67 | ./admin/make-linux-pkg.sh --cargo="${HOME}/.cargo/bin/cargo" 68 | local pkg="$(echo sandboxfs-*.*.*-????????-linux-*.tgz)" 69 | 70 | sudo find /usr/local >before.list 71 | sudo tar xzv -C /usr/local -f "${pkg}" 72 | /usr/local/bin/sandboxfs --version 73 | 74 | sudo /usr/local/libexec/sandboxfs/uninstall.sh 75 | sudo find /usr/local >after.list 76 | if ! cmp -s before.list after.list; then 77 | echo "Files left behind after installation:" 78 | diff -u before.list after.list 79 | false 80 | fi 81 | } 82 | 83 | # Builds the macOS installer and verifies that it works. 84 | do_macos_pkg() { 85 | ./admin/make-macos-pkg.sh --cargo="${HOME}/.cargo/bin/cargo" 86 | local pkg="$(echo sandboxfs-*.*.*-????????-macos.pkg)" 87 | 88 | sudo find /Library /etc /usr/local >before.list 89 | sudo sysctl -w vfs.generic.osxfuse.tunables.allow_other=0 90 | 91 | sudo installer -pkg "${pkg}" -target / 92 | test "$(sysctl -n vfs.generic.osxfuse.tunables.allow_other)" -eq 1 93 | /usr/local/bin/sandboxfs --version 94 | 95 | sudo /usr/local/libexec/sandboxfs/uninstall.sh 96 | sudo find /Library /etc /usr/local >after.list 97 | if ! cmp -s before.list after.list; then 98 | echo "Files left behind after installation:" 99 | diff -u before.list after.list 100 | false 101 | fi 102 | } 103 | 104 | # Ensures that we can build a publishable crate and that it is sane. 105 | do_package() { 106 | # Intentionally avoids ./configure to certify that the code is buildable 107 | # directly from Cargo. 108 | "${HOME}/.cargo/bin/cargo" publish --dry-run 109 | } 110 | 111 | # Runs sandboxfs' unit and integration tests. 112 | do_test() { 113 | ./configure --cargo="${HOME}/.cargo/bin/cargo" --features="${FEATURES}" 114 | make debug 115 | 116 | local sandboxfs_binary="$(pwd)/target/debug/sandboxfs" 117 | if [ -n "${FLAGS}" ]; then 118 | cat >sandboxfs-wrapper.sh <&2 173 | exit 1 174 | ;; 175 | esac 176 | -------------------------------------------------------------------------------- /admin/travis-install.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # Copyright 2017 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy 6 | # of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | set -e -u 17 | 18 | # Default to no features to avoid cluttering .travis.yml. 19 | : "${FEATURES:=}" 20 | 21 | install_bazel() { 22 | local osname 23 | case "${TRAVIS_OS_NAME}" in 24 | osx) osname=darwin ;; 25 | *) osname="${TRAVIS_OS_NAME}" ;; 26 | esac 27 | local tag=3.0.0 28 | local github="https://github.com/bazelbuild/bazel/releases/download/${tag}" 29 | local url="${github}/bazel-${tag}-${osname}-x86_64" 30 | mkdir -p ~/bin 31 | wget -O ~/bin/bazel "${url}" 32 | chmod +x ~/bin/bazel 33 | 34 | git clone https://github.com/bazelbuild/bazel.git 35 | cd bazel 36 | git pull --tags 37 | git checkout "${tag}" 38 | bazel fetch //src:bazel 39 | cd - 40 | } 41 | 42 | install_fuse() { 43 | case "${TRAVIS_OS_NAME}" in 44 | linux) 45 | sudo apt-get update 46 | sudo apt-get install -qq fuse libfuse-dev pkg-config 47 | 48 | sudo /bin/sh -c 'echo user_allow_other >>/etc/fuse.conf' 49 | sudo chmod 644 /etc/fuse.conf 50 | ;; 51 | 52 | osx) 53 | brew update 54 | brew cask install osxfuse 55 | 56 | sudo /Library/Filesystems/osxfuse.fs/Contents/Resources/load_osxfuse 57 | sudo sysctl -w vfs.generic.osxfuse.tunables.allow_other=1 58 | ;; 59 | 60 | *) 61 | echo "Don't know how to install FUSE for OS ${TRAVIS_OS_NAME}" 1>&2 62 | exit 1 63 | ;; 64 | esac 65 | } 66 | 67 | install_gperftools() { 68 | case "${TRAVIS_OS_NAME}" in 69 | linux) 70 | # Assume install_fuse has already run, which updates the packages 71 | # repository and also installs pkg-config. 72 | sudo apt-get install -qq libgoogle-perftools-dev 73 | ;; 74 | 75 | *) 76 | echo "Don't know how to install gperftools for OS ${TRAVIS_OS_NAME}" 1>&2 77 | exit 1 78 | ;; 79 | esac 80 | } 81 | 82 | install_rust() { 83 | # We need to manually install Rust because we can only specify a single 84 | # language in .travis.yml, and that language is Go for now. 85 | curl https://sh.rustup.rs -sSf | sh -s -- -y 86 | PATH="${HOME}/.cargo/bin:${PATH}" 87 | } 88 | 89 | case "${DO}" in 90 | bazel) 91 | install_bazel 92 | install_fuse 93 | install_rust 94 | ;; 95 | 96 | install|linux_pkg|macos_pkg|package|test) 97 | install_fuse 98 | install_rust 99 | if [ "${FEATURES}" = profiling ]; then 100 | install_gperftools 101 | fi 102 | ;; 103 | 104 | lint) 105 | install_fuse # Needed by Clippy to build the fuse Rust dependency. 106 | install_rust 107 | rustup component add clippy-preview 108 | ;; 109 | esac 110 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | extern crate pkg_config; 16 | 17 | use std::env; 18 | 19 | /// Configures the crate to link against `lib_name`. 20 | /// 21 | /// The library is searched via the pkg-config file provided in `pc_name`, which provides us 22 | /// accurate information on how to find the library to link to. 23 | /// 24 | /// However, for libraries that do not provide a pkg-config file, `fallback` can be set to true to 25 | /// just rely on the linker's search path to find it. This is not accurate but is better than just 26 | /// failing to build. 27 | #[allow(unused)] 28 | fn find_library(pc_name: &str, lib_name: &str, fallback: bool) { 29 | match pkg_config::Config::new().atleast_version("2.0").probe(pc_name) { 30 | Ok(_) => (), 31 | Err(_) => if fallback { println!("cargo:rustc-link-lib={}", lib_name) }, 32 | }; 33 | } 34 | 35 | fn main () { 36 | // We are running on Travis, which pins us to an old macOS version that does not have 37 | // utimensat. Apply a workaround so we can test most of sandboxfs. 38 | // TODO(https://github.com/bazelbuild/sandboxfs/issues/46): Remove this hack. 39 | match env::var_os("DO") { 40 | Some(_) => { 41 | #[cfg(target_os = "macos")] 42 | println!("cargo:rustc-cfg=have_utimensat=\"0\""); 43 | #[cfg(not(target_os = "macos"))] 44 | println!("cargo:rustc-cfg=have_utimensat=\"1\""); 45 | }, 46 | None => println!("cargo:rustc-cfg=have_utimensat=\"1\""), 47 | } 48 | 49 | // Look for the libraries required by our cpuprofiler dependency. Such dependency should do 50 | // this on its own but it doesn't yet. Given that we just need this during linking, we can 51 | // cheat and do it ourselves. 52 | // 53 | // Note that older versions of gperftools (the package providing libprofiler) did not ship a 54 | // pkg-config file, so we must fall back to using the linker's path. 55 | // 56 | // TODO(https://github.com/AtheMathmo/cpuprofiler/pull/10): Remove this in favor of upstream 57 | // doing the right thing when this PR is accepted a new cpuprofiler version is released. 58 | // TODO(https://github.com/dignifiedquire/rust-gperftools/pull/1): Remove this in favor of 59 | // upstream doing the right thing when this PR is accepted and switch to rust-gperftools instead 60 | // (which has the added benefit of providing heap profiling). 61 | #[cfg(feature = "profiling")] find_library("libprofiler", "profiler", true); 62 | } 63 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # Copyright 2017 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy 6 | # of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | set -e 17 | 18 | readonly PROGNAME="${0##*/}" 19 | readonly SRCDIR="$(cd "$(dirname "${0}")" && pwd -P)" 20 | 21 | # Settings that end up in the Makefile. 22 | CARGO=cargo 23 | CLEANFILES=target 24 | FEATURES= 25 | GOPATH="$(pwd)/.gopath" 26 | GOROOT= 27 | IS_BMAKE= 28 | IS_GNUMAKE= 29 | PREFIX= 30 | DISTCLEANFILES="Makefile go.sum ${GOPATH}" 31 | 32 | # List of variables exposed to the Makefile. 33 | MK_VARS="CARGO CLEANFILES DISTCLEANFILES FEATURES GOPATH GOROOT PREFIX" 34 | 35 | # List of variables replaced in the Makefile. 36 | MK_SUBSTS="IS_BMAKE IS_GNUMAKE" 37 | 38 | err() { 39 | echo "${PROGNAME}: E: ${@}" 1>&2 40 | exit 1 41 | } 42 | 43 | info() { 44 | echo "${PROGNAME}: I: ${@}" 1>&2 45 | } 46 | 47 | find_prog() { 48 | local prog="${1}"; shift 49 | 50 | local oldifs="${IFS}" 51 | IFS=: 52 | set -- ${PATH} 53 | IFS="${oldifs}" 54 | 55 | while [ ${#} -gt 0 ]; do 56 | if [ -x "${1}/${prog}" ]; then 57 | echo "${1}/${prog}" 58 | return 0 59 | else 60 | shift 61 | fi 62 | done 63 | return 1 64 | } 65 | 66 | find_progs() { 67 | while [ ${#} -gt 0 ]; do 68 | if find_prog "${1}"; then 69 | return 0 70 | else 71 | shift 72 | fi 73 | done 74 | return 1 75 | } 76 | 77 | setup_cargo() { 78 | local user_override="${1}"; shift 79 | 80 | if [ -n "${user_override}" ]; then 81 | [ -e "${user_override}" ] || err "cargo not found in" \ 82 | "${user_override}; bogus argument to --cargo?" 83 | CARGO="${user_override}" 84 | else 85 | local cargo="$(find_progs cargo)" 86 | [ -n "${cargo}" ] || err "Cannot find cargo in path; pass" \ 87 | "--cargo=/path/to/cargo to configure" 88 | CARGO="${cargo}" 89 | fi 90 | 91 | info "Using Cargo from: ${CARGO}" 92 | } 93 | 94 | # Installs git hooks into the git directory provided in git_dir. 95 | setup_git() { 96 | local git_dir="${1}"; shift 97 | 98 | cd "${git_dir}/hooks" 99 | for hook in ../../admin/pre-commit; do 100 | info "Installing git hook ${hook##*/}" 101 | ln -s -f "${hook}" . 102 | done 103 | cd - >/dev/null 2>&1 104 | } 105 | 106 | setup_go() { 107 | local user_override="${1}"; shift 108 | 109 | if [ -n "${user_override}" ]; then 110 | [ "${user_override}" = none ] || GOROOT="${user_override}" 111 | else 112 | local go="$(find_progs go)" 113 | [ -n "${go}" ] && GOROOT="$(dirname "$(dirname "${go}")")" 114 | fi 115 | 116 | if [ -z "${GOROOT}" ]; then 117 | info "Go not found; cannot run integration tests" 118 | else 119 | info "Using Go with GOROOT: ${GOROOT}" 120 | GOPATH="${GOPATH}" "${GOROOT}/bin/go" mod download 121 | GOPATH="${GOPATH}" "${GOROOT}/bin/go" install golang.org/x/lint/golint 122 | fi 123 | } 124 | 125 | setup_make() { 126 | IS_BMAKE=# 127 | IS_GNUMAKE=# 128 | if make --version >/dev/null 2>&1; then 129 | info "make is GNU Make" 130 | IS_GNUMAKE= 131 | else 132 | info "make is bmake" 133 | IS_BMAKE= 134 | fi 135 | } 136 | 137 | setup_prefix() { 138 | local prefix="${1:-/usr/local}"; shift 139 | 140 | info "Installation prefix is ${prefix}" 141 | PREFIX="${prefix}" 142 | } 143 | 144 | setup_vscode() { 145 | { 146 | echo '// AUTOMATICALLY GENERATED!!!' 147 | echo '// EDIT settings.json.in INSTEAD' 148 | sed \ 149 | -e "s,__GOPATH__,${GOPATH},g" \ 150 | -e "s,__GOROOT__,${GOROOT},g" \ 151 | -e "s,__TOOLS_GOPATH__,$(pwd)/.gopath-tools,g" \ 152 | .vscode/settings.json.in 153 | } >.vscode/settings.json 154 | } 155 | 156 | generate_makefile() { 157 | local src="${1}"; shift 158 | local dest="${1}"; shift 159 | 160 | info "Generating ${dest}" 161 | echo "# AUTOMATICALLY GENERATED; DO NOT EDIT!" >"${dest}.tmp" 162 | for var in ${MK_VARS}; do 163 | local value 164 | eval "value=\"\$${var}\"" 165 | echo "${var} = ${value}" >>"${dest}.tmp" 166 | done 167 | local substs= 168 | for var in ${MK_SUBSTS}; do 169 | local value 170 | eval "value=\"\$${var}\"" 171 | substs="${substs} -e s,@${var}@,${value},g" 172 | done 173 | sed ${substs} "${src}" >>"${dest}.tmp" 174 | mv "${dest}.tmp" "${dest}" 175 | } 176 | 177 | main() { 178 | cd "${SRCDIR}" 179 | 180 | local cargo= 181 | local goroot= 182 | local prefix= 183 | for arg in "${@}"; do 184 | case "${arg}" in 185 | --cargo=*) cargo="${arg#*=}" ;; 186 | --features=*) FEATURES="${arg#*=}" ;; 187 | --goroot=*) goroot="${arg#*=}" ;; 188 | --prefix=*) prefix="${arg#*=}" ;; 189 | *) err "Unknown argument ${arg}" ;; 190 | esac 191 | done 192 | 193 | setup_cargo "${cargo}" 194 | [ -d .git ] && setup_git .git 195 | setup_go "${goroot}" 196 | setup_make 197 | setup_prefix "${prefix}" 198 | setup_vscode 199 | 200 | generate_makefile Makefile.in Makefile 201 | } 202 | 203 | main "${@}" 204 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bazelbuild/sandboxfs 2 | 3 | go 1.12 4 | 5 | require ( 6 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b 7 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect 8 | golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 9 | ) 10 | -------------------------------------------------------------------------------- /integration/cli_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package integration 16 | 17 | import ( 18 | "fmt" 19 | "runtime" 20 | "testing" 21 | 22 | "github.com/bazelbuild/sandboxfs/integration/utils" 23 | ) 24 | 25 | var ( 26 | // versionPattern contains a pattern to match the output of sandboxfs --version. 27 | versionPattern = `sandboxfs [0-9]+\.[0-9]+` 28 | ) 29 | 30 | func TestCli_Help(t *testing.T) { 31 | wantStdout := fmt.Sprintf(`Usage: sandboxfs [options] MOUNT_POINT 32 | 33 | Options: 34 | --allow other|root|self 35 | specifies who should have access to the file system 36 | (default: self) 37 | --cpu_profile PATH enables CPU profiling and writes a profile to the 38 | given path 39 | --help prints usage information and exits 40 | --input PATH where to read reconfiguration data from (- for stdin) 41 | --mapping TYPE:PATH:UNDERLYING_PATH 42 | type and locations of a mapping 43 | --node_cache enables the path-based node cache (known broken) 44 | --output PATH where to write the reconfiguration status to (- for 45 | stdout) 46 | --reconfig_threads COUNT 47 | number of reconfiguration threads (default: %d) 48 | --ttl TIMEs how long the kernel is allowed to keep file metadata 49 | (default: 60s) 50 | --version prints version information and exits 51 | --xattrs enables support for extended attributes 52 | `, runtime.NumCPU()) 53 | 54 | stdout, stderr, err := utils.RunAndWait(0, "--help") 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | if wantStdout != stdout { 59 | t.Errorf("Got %s; want stdout to match %s", stdout, wantStdout) 60 | } 61 | if len(stderr) > 0 { 62 | t.Errorf("Got %s; want stderr to be empty", stderr) 63 | } 64 | } 65 | func TestCli_Version(t *testing.T) { 66 | stdout, stderr, err := utils.RunAndWait(0, "--version") 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | if !utils.MatchesRegexp(versionPattern, stdout) { 71 | t.Errorf("Got %s; want stdout to match %s", stdout, versionPattern) 72 | } 73 | if len(stderr) > 0 { 74 | t.Errorf("Got %s; want stderr to be empty", stderr) 75 | } 76 | } 77 | 78 | func TestCli_ExclusiveFlagsPriority(t *testing.T) { 79 | testData := []struct { 80 | name string 81 | 82 | args []string 83 | wantExitStatus int 84 | wantStdout string 85 | wantStderr string 86 | }{ 87 | { 88 | "BogusFlagsWinOverEverything", 89 | []string{"--version", "--help", "--foo"}, 90 | 2, 91 | "", 92 | "Unrecognized option.*'foo'", 93 | }, 94 | { 95 | "BogusHFlagWinsOverEverything", 96 | []string{"--version", "--help", "-h"}, 97 | 2, 98 | "", 99 | "Unrecognized option.*'h'", 100 | }, 101 | { 102 | "HelpWinsOverValidArgs", 103 | []string{"--version", "--allow=self", "--help", "/mnt"}, 104 | 0, 105 | "Usage:", 106 | "", 107 | }, 108 | { 109 | "VersionWinsOverValidArgsButHelp", 110 | []string{"--allow=other", "--version", "/mnt"}, 111 | 0, 112 | versionPattern, 113 | "", 114 | }, 115 | } 116 | for _, d := range testData { 117 | t.Run(d.name, func(t *testing.T) { 118 | stdout, stderr, err := utils.RunAndWait(d.wantExitStatus, d.args...) 119 | if err != nil { 120 | t.Fatal(err) 121 | } 122 | if len(d.wantStdout) == 0 && len(stdout) > 0 { 123 | t.Errorf("Got %s; want stdout to be empty", stdout) 124 | } else if len(d.wantStdout) > 0 && !utils.MatchesRegexp(d.wantStdout, stdout) { 125 | t.Errorf("Got %s; want stdout to match %s", stdout, d.wantStdout) 126 | } 127 | if len(d.wantStderr) == 0 && len(stderr) > 0 { 128 | t.Errorf("Got %s; want stderr to be empty", stderr) 129 | } else if len(d.wantStderr) > 0 && !utils.MatchesRegexp(d.wantStderr, stderr) { 130 | t.Errorf("Got %s; want stderr to match %s", stderr, d.wantStderr) 131 | } 132 | }) 133 | } 134 | } 135 | 136 | func TestCli_Syntax(t *testing.T) { 137 | testData := []struct { 138 | name string 139 | 140 | args []string 141 | wantStderr string 142 | }{ 143 | { 144 | "InvalidFlag", 145 | []string{"--foo"}, 146 | "Unrecognized option.*'foo'", 147 | }, 148 | { 149 | "InvalidHFlag", 150 | []string{"-h"}, 151 | "Unrecognized option.*'h'", 152 | }, 153 | { 154 | "NoArguments", 155 | []string{}, 156 | "invalid number of arguments", 157 | }, 158 | { 159 | "TooManyArguments", 160 | []string{"mount-point", "extra"}, 161 | "invalid number of arguments", 162 | }, 163 | { 164 | "InvalidFlagWinsOverHelp", 165 | []string{"--invalid_flag", "--help"}, 166 | "Unrecognized option.*'invalid_flag'", 167 | }, 168 | // TODO(jmmv): For consistency with all previous tests, an invalid number of 169 | // arguments should win over --help, but it currently does not. 170 | // { 171 | // "InvalidArgumentsWinOverHelp", 172 | // []string{"--help", "foo"}, 173 | // "invalid number of arguments", 174 | // }, 175 | { 176 | "MappingMissingTarget", 177 | []string{"--mapping=ro:/foo"}, 178 | `bad mapping ro:/foo: expected three colon-separated fields`, 179 | }, 180 | { 181 | "MappingRelativeTarget", 182 | []string{"--mapping=rw:/:relative/path"}, 183 | `bad mapping rw:/:relative/path: path "relative/path" is not absolute`, 184 | }, 185 | { 186 | "MappingBadType", 187 | []string{"--mapping=row:/foo:/bar"}, 188 | `bad mapping row:/foo:/bar: type was row but should be ro or rw`, 189 | }, 190 | { 191 | "ReconfigThreadsBadValue", 192 | []string{"--reconfig_threads=-1"}, 193 | `invalid thread count -1: .*invalid digit`, 194 | }, 195 | } 196 | for _, d := range testData { 197 | t.Run(d.name, func(t *testing.T) { 198 | stdout, stderr, err := utils.RunAndWait(2, d.args...) 199 | if err != nil { 200 | t.Fatal(err) 201 | } 202 | if len(stdout) > 0 { 203 | t.Errorf("Got %s; want stdout to be empty", stdout) 204 | } 205 | if !utils.MatchesRegexp(d.wantStderr, stderr) { 206 | t.Errorf("Got %s; want stderr to match %s", stderr, d.wantStderr) 207 | } 208 | if !utils.MatchesRegexp("--help", stderr) { 209 | t.Errorf("Got %s; want --help mention in stderr", stderr) 210 | } 211 | }) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /integration/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | // Package integration contains integration tests for sandboxfs. 16 | // 17 | // All the tests in this package are designed to execute a sandboxfs binary and treat it as a black 18 | // box. The binary to be tested is indicated by the SANDBOXFS environment variable, which must be 19 | // set by the user at startup time. 20 | package integration 21 | -------------------------------------------------------------------------------- /integration/layout_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package integration 16 | 17 | import ( 18 | "io/ioutil" 19 | "os" 20 | "path/filepath" 21 | "testing" 22 | 23 | "github.com/bazelbuild/sandboxfs/integration/utils" 24 | ) 25 | 26 | func TestLayout_MountPointDoesNotExist(t *testing.T) { 27 | tempDir, err := ioutil.TempDir("", "test") 28 | if err != nil { 29 | t.Fatalf("Failed to create temporary directory: %v", err) 30 | } 31 | defer os.RemoveAll(tempDir) 32 | 33 | mountPoint := filepath.Join(tempDir, "non-existent") 34 | wantStderr := "Failed to mount " + mountPoint + ".*No such" 35 | 36 | stdout, stderr, err := utils.RunAndWait(1, "--mapping=ro:/:"+tempDir, mountPoint) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | if len(stdout) > 0 { 41 | t.Errorf("Got %s; want stdout to be empty", stdout) 42 | } 43 | if !utils.MatchesRegexp(wantStderr, stderr) { 44 | t.Errorf("Got %s; want stderr to match %s", stderr, wantStderr) 45 | } 46 | } 47 | 48 | func TestLayout_RootMustBeDirectory(t *testing.T) { 49 | tempDir, err := ioutil.TempDir("", "test") 50 | if err != nil { 51 | t.Fatalf("Failed to create temporary directory: %v", err) 52 | } 53 | defer os.RemoveAll(tempDir) 54 | 55 | file := filepath.Join(tempDir, "file") 56 | utils.MustWriteFile(t, file, 0644, "") 57 | 58 | wantStderr := "Failed to map root: .*" + file + ".* not a directory" 59 | 60 | stdout, stderr, err := utils.RunAndWait(1, "--mapping=ro:/:"+file, "irrelevant-mount-point") 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | if len(stdout) > 0 { 65 | t.Errorf("Got %s; want stdout to be empty", stdout) 66 | } 67 | if !utils.MatchesRegexp(wantStderr, stderr) { 68 | t.Errorf("Got %s; want stderr to match %s", stderr, wantStderr) 69 | } 70 | } 71 | 72 | func TestLayout_TargetDoesNotExist(t *testing.T) { 73 | wantStderr := "Failed to map root: stat failed .*/non-existent" 74 | 75 | stdout, stderr, err := utils.RunAndWait(1, "--mapping=ro:/:/non-existent", "irrelevant-mount-point") 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | if len(stdout) > 0 { 80 | t.Errorf("Got %s; want stdout to be empty", stdout) 81 | } 82 | if !utils.MatchesRegexp(wantStderr, stderr) { 83 | t.Errorf("Got %s; want stderr to match %s", stderr, wantStderr) 84 | } 85 | } 86 | 87 | func TestLayout_DuplicateMapping(t *testing.T) { 88 | tempDir, err := ioutil.TempDir("", "test") 89 | if err != nil { 90 | t.Fatalf("Failed to create temporary directory: %v", err) 91 | } 92 | defer os.RemoveAll(tempDir) 93 | 94 | wantStderr := "Cannot map .*'/a/a .* Already mapped\n" 95 | 96 | path1 := filepath.Join(tempDir, "1") 97 | utils.MustWriteFile(t, path1, 0644, "") 98 | path2 := filepath.Join(tempDir, "2") 99 | utils.MustWriteFile(t, path2, 0644, "") 100 | 101 | stdout, stderr, err := utils.RunAndWait(1, "--mapping=ro:/:"+tempDir, "--mapping=ro:/a/a:"+path1, "--mapping=ro:/a/b:"+tempDir, "--mapping=ro:/a/a:"+path2, "irrelevant-mount-point") 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | if len(stdout) > 0 { 106 | t.Errorf("Got %s; want stdout to be empty", stdout) 107 | } 108 | if !utils.MatchesRegexp(wantStderr, stderr) { 109 | t.Errorf("Got %s; want stderr to match %s", stderr, wantStderr) 110 | } 111 | } 112 | 113 | func TestLayout_TargetIsScaffoldDirectory(t *testing.T) { 114 | tempDir, err := ioutil.TempDir("", "test") 115 | if err != nil { 116 | t.Fatalf("Failed to create temporary directory: %v", err) 117 | } 118 | defer os.RemoveAll(tempDir) 119 | 120 | file := filepath.Join(tempDir, "file") 121 | utils.MustWriteFile(t, file, 0644, "") 122 | 123 | wantStderr := "Cannot map .*'/a .* Already mapped" 124 | 125 | stdout, stderr, err := utils.RunAndWait(1, "--mapping=ro:/a/b/c:"+tempDir, "--mapping=ro:/a:"+file, "irrelevant-mount-point") 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | if len(stdout) > 0 { 130 | t.Errorf("Got %s; want stdout to be empty", stdout) 131 | } 132 | if !utils.MatchesRegexp(wantStderr, stderr) { 133 | t.Errorf("Got %s; want stderr to match %s", stderr, wantStderr) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /integration/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package integration 16 | 17 | import ( 18 | "flag" 19 | "log" 20 | "os" 21 | "testing" 22 | 23 | "github.com/bazelbuild/sandboxfs/integration/utils" 24 | ) 25 | 26 | var ( 27 | features = flag.String("features", "", "Whitespace-separated list of features enabled during the build") 28 | releaseBuild = flag.Bool("release_build", true, "Whether the tested binary was built for release or not") 29 | sandboxfsBinary = flag.String("sandboxfs_binary", "", "Path to the sandboxfs binary to test; cannot be empty and must point to an existent binary") 30 | unprivilegedUser = flag.String("unprivileged_user", "", "Username of the system user to use for tests that require non-root permissions; can be empty, in which case those tests are skipped") 31 | ) 32 | 33 | func TestMain(m *testing.M) { 34 | flag.Parse() 35 | if len(*sandboxfsBinary) == 0 { 36 | log.Fatalf("--sandboxfs_binary must be provided") 37 | } 38 | if err := utils.SetConfigFromFlags(*features, *releaseBuild, *sandboxfsBinary, *unprivilegedUser); err != nil { 39 | log.Fatalf("invalid flags configuration: %v", err) 40 | } 41 | 42 | os.Exit(m.Run()) 43 | } 44 | -------------------------------------------------------------------------------- /integration/nesting_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package integration 16 | 17 | import ( 18 | "os" 19 | "path/filepath" 20 | "reflect" 21 | "syscall" 22 | "testing" 23 | 24 | "github.com/bazelbuild/sandboxfs/integration/utils" 25 | ) 26 | 27 | func TestNesting_ScaffoldIntermediateComponents(t *testing.T) { 28 | state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%", "--mapping=ro:/1/2/3/4/5:%ROOT%/subdir") 29 | defer state.TearDown(t) 30 | 31 | utils.MustWriteFile(t, state.RootPath("subdir", "file"), 0644, "some contents") 32 | 33 | if err := utils.DirEquals(state.RootPath("subdir"), state.MountPath("1/2/3/4/5")); err != nil { 34 | t.Error(err) 35 | } 36 | if err := utils.FileEquals(state.MountPath("1/2/3/4/5", "file"), "some contents"); err != nil { 37 | t.Error(err) 38 | } 39 | 40 | utils.MustMkdirAll(t, state.TempPath("golden/1/2/3/4/5"), 0755) 41 | for _, dir := range []string{"1/2/3/4", "1/2/3", "1/2", "1"} { 42 | goldenDir := state.TempPath("golden", dir) 43 | if err := os.Chmod(goldenDir, 0555); err != nil { 44 | t.Errorf("Failed to set golden dir permissions to 0555 to match scaffold dir expectations: %v", err) 45 | } 46 | defer os.Chmod(goldenDir, 0755) // To allow cleanup in tearDown to succeed. 47 | 48 | scaffoldDir := state.MountPath(dir) 49 | if err := utils.DirEquals(goldenDir, scaffoldDir); err != nil { 50 | t.Error(err) 51 | } 52 | } 53 | } 54 | 55 | func TestNesting_ScaffoldIntermediateComponentsAreImmutable(t *testing.T) { 56 | // Scaffold directories have mode 0555 to signal that they are read-only. The mode alone 57 | // prevents unprivileged users from writing to those directories, but the mode has no effect 58 | // on root accesses. Therefore, run the test as root to bypass permission checks and 59 | // attempt real writes. 60 | root := utils.RequireRoot(t, "Requires root privileges to write to directories with mode 0555") 61 | 62 | state := utils.MountSetupWithUser(t, root, "--mapping=ro:/:%ROOT%", "--mapping=rw:/1/2/3:%ROOT%/subdir") 63 | defer state.TearDown(t) 64 | 65 | for _, dir := range []string{"1/foo", "1/2/foo"} { 66 | err := os.Mkdir(state.MountPath(dir), 0755) 67 | pathErr, ok := err.(*os.PathError) 68 | if !ok || pathErr.Err != syscall.EPERM { 69 | t.Errorf("Want Mkdir to fail inside scaffold directory %s with %v; got %v (%v)", dir, syscall.EPERM, err, reflect.TypeOf(err)) 70 | } 71 | } 72 | if err := os.Mkdir(state.MountPath("1/2/3/foo"), 0755); err != nil { 73 | t.Errorf("Want Mkdir to succeed inside non-scaffold directory; got %v", err) 74 | } 75 | } 76 | 77 | func TestNesting_ReadWriteWithinReadOnly(t *testing.T) { 78 | state := utils.MountSetup(t, "--mapping=rw:/:%ROOT%", "--mapping=ro:/ro:%ROOT%/one/two", "--mapping=rw:/ro/rw:%ROOT%") 79 | defer state.TearDown(t) 80 | 81 | if err := os.MkdirAll(state.MountPath("ro/hello"), 0755); err == nil { 82 | t.Errorf("Mkdir succeeded in read-only mapping") 83 | } 84 | if err := os.MkdirAll(state.MountPath("ro/rw/hello"), 0755); err != nil { 85 | t.Errorf("Mkdir failed in read-write mapping: %v", err) 86 | } 87 | } 88 | 89 | func TestNesting_SameTarget(t *testing.T) { 90 | state := utils.MountSetup(t, "--node_cache", "--mapping=ro:/:%ROOT%", "--mapping=rw:/dir1:%ROOT%/same", "--mapping=rw:/dir2/dir3/dir4:%ROOT%/same") 91 | defer state.TearDown(t) 92 | 93 | utils.MustWriteFile(t, state.MountPath("dir1/file"), 0644, "old contents") 94 | utils.MustWriteFile(t, state.MountPath("dir2/dir3/dir4/file"), 0644, "new contents") 95 | 96 | externalDir := state.RootPath("same") 97 | if err := utils.FileEquals(filepath.Join(externalDir, "file"), "new contents"); err != nil { 98 | t.Error(err) 99 | } 100 | for _, dir := range []string{"/dir1", "/dir2/dir3/dir4"} { 101 | internalDir := state.MountPath(dir) 102 | if err := utils.DirEquals(externalDir, internalDir); err != nil { 103 | t.Error(err) 104 | } 105 | } 106 | 107 | // We share the same internal representation for different mappings that point to the same 108 | // underlying file, which means that we can assume content changes through a mapping will be 109 | // reflected on the other mapping. This is independent of how the kernel caches work or 110 | // when content invalidations happen. 111 | if err := utils.FileEquals(state.MountPath("dir1/file"), "new contents"); err != nil { 112 | t.Error(err) 113 | } 114 | if err := utils.FileEquals(state.MountPath("dir2/dir3/dir4/file"), "new contents"); err != nil { 115 | t.Error(err) 116 | } 117 | } 118 | 119 | func TestNesting_PreserveSymlinks(t *testing.T) { 120 | state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%", "--mapping=ro:/dir1/dir2:%ROOT%") 121 | defer state.TearDown(t) 122 | 123 | utils.MustWriteFile(t, state.RootPath("file"), 0644, "file in root directory") 124 | utils.MustMkdirAll(t, state.RootPath("dir"), 0755) 125 | if err := os.Symlink("..", state.RootPath("dir/up")); err != nil { 126 | t.Fatalf("Failed to create test symlink: %v", err) 127 | } 128 | 129 | if err := utils.FileEquals(state.MountPath("dir/up/file"), "file in root directory"); err != nil { 130 | t.Error(err) 131 | } 132 | if err := utils.FileEquals(state.MountPath("dir1/dir2/dir/up/file"), "file in root directory"); err != nil { 133 | t.Error(err) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /integration/options_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package integration 16 | 17 | import ( 18 | "io/ioutil" 19 | "os" 20 | "runtime" 21 | "testing" 22 | 23 | "github.com/bazelbuild/sandboxfs/integration/utils" 24 | ) 25 | 26 | func TestOptions_Allow(t *testing.T) { 27 | root := utils.RequireRoot(t, "Requires root privileges to spawn sandboxfs under different users") 28 | 29 | user := utils.GetConfig().UnprivilegedUser 30 | if user == nil { 31 | t.Skipf("unprivileged user not set; must contain the name of an unprivileged user with FUSE access") 32 | } 33 | t.Logf("Using primary unprivileged user: %v", user) 34 | 35 | other, err := utils.LookupUserOtherThan(root.Username, user.Username) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | t.Logf("Using secondary unprivileged user: %v", other) 40 | 41 | var allowRootWorks bool 42 | switch runtime.GOOS { 43 | case "darwin": 44 | allowRootWorks = true 45 | case "linux": 46 | allowRootWorks = false 47 | default: 48 | t.Fatalf("Don't know how this test behaves in this platform") 49 | } 50 | 51 | testData := []struct { 52 | name string 53 | 54 | allowFlag string 55 | wantMountOk bool 56 | okUsers []*utils.UnixUser 57 | notOkUsers []*utils.UnixUser 58 | }{ 59 | {"Default", "", true, []*utils.UnixUser{user}, []*utils.UnixUser{root, other}}, 60 | {"Other", "--allow=other", true, []*utils.UnixUser{user, other, root}, []*utils.UnixUser{}}, 61 | {"Root", "--allow=root", allowRootWorks, []*utils.UnixUser{user, root}, []*utils.UnixUser{other}}, 62 | {"Self", "--allow=self", true, []*utils.UnixUser{user}, []*utils.UnixUser{root, other}}, 63 | } 64 | for _, d := range testData { 65 | t.Run(d.name, func(t *testing.T) { 66 | tempDir, err := ioutil.TempDir("", "test") 67 | if err != nil { 68 | t.Fatalf("Failed to create temporary directory: %v", err) 69 | } 70 | defer os.RemoveAll(tempDir) 71 | 72 | if !d.wantMountOk { 73 | _, stderr, err := utils.RunAndWait(1, d.allowFlag, "--mapping=ro:/:/", tempDir) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | if !utils.MatchesRegexp("known.*broken", stderr) { 78 | t.Errorf("Want error message to mention known brokenness; got %v", stderr) 79 | } 80 | return 81 | } 82 | 83 | args := []string{"--mapping=ro:/:%ROOT%"} 84 | if d.allowFlag != "" { 85 | args = append(args, d.allowFlag) 86 | } 87 | 88 | state := utils.MountSetupWithUser(t, user, args...) 89 | defer state.TearDown(t) 90 | 91 | utils.MustWriteFile(t, state.RootPath("file"), 0444, "") 92 | file := state.MountPath("file") 93 | 94 | for _, user := range d.okUsers { 95 | if err := utils.FileExistsAsUser(file, user); err != nil { 96 | t.Errorf("Failed to access mount point as user %s: %v", user.Username, err) 97 | } 98 | } 99 | 100 | for _, user := range d.notOkUsers { 101 | if err := utils.FileExistsAsUser(file, user); err == nil { 102 | t.Errorf("Was able to access mount point as user %s; want error", user.Username) 103 | } 104 | } 105 | }) 106 | } 107 | } 108 | 109 | func TestOptions_Syntax(t *testing.T) { 110 | testData := []struct { 111 | name string 112 | 113 | args []string 114 | wantStderr string 115 | }{ 116 | {"AllowBadValue", []string{"--allow=foo"}, "foo.*must be one of.*other"}, 117 | } 118 | for _, d := range testData { 119 | t.Run(d.name, func(t *testing.T) { 120 | stdout, stderr, err := utils.RunAndWait(2, d.args...) 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | if len(stdout) > 0 { 125 | t.Errorf("Got %s; want stdout to be empty", stdout) 126 | } 127 | if !utils.MatchesRegexp(d.wantStderr, stderr) { 128 | t.Errorf("Got %s; want stderr to match %s", stderr, d.wantStderr) 129 | } 130 | if !utils.MatchesRegexp("--help", stderr) { 131 | t.Errorf("Got %s; want --help mention in stderr", stderr) 132 | } 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /integration/profiling_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package integration 16 | 17 | import ( 18 | "io/ioutil" 19 | "os" 20 | "path/filepath" 21 | "testing" 22 | 23 | "github.com/bazelbuild/sandboxfs/integration/utils" 24 | ) 25 | 26 | func TestProfiling_OptionalSupport(t *testing.T) { 27 | tempDir, err := ioutil.TempDir("", "test") 28 | if err != nil { 29 | t.Fatalf("Failed to create temporary directory: %v", err) 30 | } 31 | defer os.RemoveAll(tempDir) 32 | 33 | profile := filepath.Join(tempDir, "cpu.prof") 34 | arg := "--cpu_profile=" + profile 35 | 36 | if _, ok := utils.GetConfig().Features["profiling"]; ok { 37 | state := utils.MountSetup(t, arg, "--mapping=ro:/:%ROOT%") 38 | // Explicitly stop sandboxfs (which is different to what most other tests do). 39 | // We need to do this here to cause the profiles to be written to disk. 40 | state.TearDown(t) 41 | 42 | // Check if the profile exists and is not empty. We cannot do much more complex 43 | // verifications here, but ensuring the file is not empty is sufficient to verify 44 | // that the profiles were actually written during termination. 45 | stat, err := os.Lstat(profile) 46 | if err != nil { 47 | t.Fatalf("Cannot find expected profile %s", profile) 48 | } 49 | if stat.Size() == 0 { 50 | t.Errorf("Expected profile %s is empty", profile) 51 | } 52 | } else { 53 | _, stderr, err := utils.RunAndWait(1, arg, filepath.Join(tempDir, "root")) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | wantStderr := "Failed to start CPU profile.*feature not enabled" 58 | if !utils.MatchesRegexp(wantStderr, stderr) { 59 | t.Errorf("Got %s; want stderr to match %s", stderr, wantStderr) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /integration/signal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package integration 16 | 17 | import ( 18 | "bufio" 19 | "bytes" 20 | "fmt" 21 | "io" 22 | "os" 23 | "syscall" 24 | "testing" 25 | "time" 26 | 27 | "github.com/bazelbuild/sandboxfs/integration/utils" 28 | ) 29 | 30 | // checkSignalHandled verifies that the given sandboxfs process exited with an error on receipt of 31 | // a signal and that the mount point was truly unmounted. 32 | func checkSignalHandled(state *utils.MountState) error { 33 | if err := state.Cmd.Wait(); err == nil { 34 | return fmt.Errorf("wait of sandboxfs returned nil, want an error") 35 | } 36 | if state.Cmd.ProcessState.Success() { 37 | return fmt.Errorf("exit status of sandboxfs returned success, want an error") 38 | } 39 | 40 | if err := utils.Unmount(state.MountPath()); err == nil { 41 | return fmt.Errorf("mount point should have been released during signal handling but wasn't") 42 | } 43 | 44 | state.Cmd = nil // Tell state.TearDown that we cleaned the mount point ourselves. 45 | return nil 46 | } 47 | 48 | func TestSignal_RaceBetweenSignalSetupAndMount(t *testing.T) { 49 | // This is a race-condition test: we run the same test multiple times, each increasing the 50 | // time it takes for us to kill the subprocess. The numbers here proved to be sufficient 51 | // during development to exercise various bugs, and with machines getting faster, they 52 | // should continue to be good. 53 | ok := true 54 | for delayMs := 2; ok && delayMs < 200; delayMs += 2 { 55 | ok = t.Run(fmt.Sprintf("Delay%v", delayMs), func(t *testing.T) { 56 | state := utils.MountSetup(t) 57 | defer state.TearDown(t) 58 | 59 | time.Sleep(time.Duration(delayMs) * time.Millisecond) 60 | 61 | if err := state.Cmd.Process.Signal(os.Interrupt); err != nil { 62 | t.Fatalf("Failed to deliver signal to sandboxfs process: %v", err) 63 | } 64 | if err := checkSignalHandled(state); err != nil { 65 | t.Fatal(err) 66 | } 67 | }) 68 | } 69 | } 70 | 71 | func TestSignal_UnmountWhenCaught(t *testing.T) { 72 | for _, signal := range []os.Signal{syscall.SIGHUP, os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM} { 73 | t.Run(signal.String(), func(t *testing.T) { 74 | stderr := new(bytes.Buffer) 75 | 76 | state := utils.MountSetupWithOutputs(t, nil, stderr, "--mapping=ro:/:%ROOT%") 77 | defer state.TearDown(t) 78 | 79 | utils.MustWriteFile(t, state.RootPath("a"), 0644, "") 80 | if _, err := os.Lstat(state.MountPath("a")); os.IsNotExist(err) { 81 | t.Fatalf("Failed to create test file within file system: %v", err) 82 | } 83 | 84 | if err := state.Cmd.Process.Signal(signal); err != nil { 85 | t.Fatalf("Failed to deliver signal to sandboxfs process: %v", err) 86 | } 87 | if err := checkSignalHandled(state); err != nil { 88 | t.Fatal(err) 89 | } 90 | if !utils.MatchesRegexp(fmt.Sprintf("Caught signal %d", signal), stderr.String()) { 91 | t.Errorf("Termination error message does not mention signal number; got %v", stderr) 92 | } 93 | 94 | if _, err := os.Lstat(state.MountPath("a")); os.IsExist(err) { 95 | t.Fatalf("File system not unmounted; test file still exists in mount point") 96 | } 97 | }) 98 | } 99 | } 100 | 101 | func TestSignal_QueuedWhileInUse(t *testing.T) { 102 | stderrReader, stderrWriter := io.Pipe() 103 | defer stderrReader.Close() 104 | defer stderrWriter.Close() 105 | stderr := bufio.NewScanner(stderrReader) 106 | 107 | state := utils.MountSetupWithOutputs(t, nil, stderrWriter, "--mapping=rw:/:%ROOT%") 108 | defer state.TearDown(t) 109 | 110 | // Create a file under the root directory and open it via the mount point to keep the file 111 | // system busy. 112 | utils.MustWriteFile(t, state.RootPath("file"), 0644, "file contents") 113 | file, err := os.Open(state.MountPath("file")) 114 | if err != nil { 115 | t.Fatalf("Failed to open test file: %v", err) 116 | } 117 | defer file.Close() 118 | 119 | // Send signal. The mount point is busy because we hold an open file descriptor. While the 120 | // file system will receive the signal, it will not be able to exit cleanly. Instead, we 121 | // expect it to continue running until we release the resources, at which point the signal 122 | // should be processed. 123 | if err := state.Cmd.Process.Signal(os.Interrupt); err != nil { 124 | t.Fatalf("Failed to deliver signal to sandboxfs process: %v", err) 125 | } 126 | 127 | // Wait until sandboxfs has acknowledged the first receipt of the signal, but continue 128 | // consuming stderr output in the background to prevent stalling sandboxfs due to a full 129 | // pipe. 130 | received := make(chan bool) 131 | go func() { 132 | notified := false 133 | for { 134 | if !stderr.Scan() { 135 | break 136 | } 137 | os.Stderr.WriteString(stderr.Text() + "\n") 138 | if utils.MatchesRegexp("Unmounting.*failed.*will retry", stderr.Text()) { 139 | if !notified { 140 | received <- true 141 | notified = true 142 | } 143 | // Continue running so that any additional contents to stderr are 144 | // consumed. Otherwise, the sandboxfs could stall if the stderr 145 | // pipe's buffer filled up. 146 | } 147 | } 148 | }() 149 | _ = <-received 150 | t.Logf("sandboxfs saw the signal delivery; continuing test") 151 | 152 | // Now that we know that sandboxfs has seen the signal, verify that it continues to work 153 | // successfully. 154 | if err := utils.FileEquals(state.MountPath("file"), "file contents"); err != nil { 155 | t.Fatalf("Failed to verify file contents using handle opened before signal delivery: %v", err) 156 | } 157 | if err := os.Mkdir(state.MountPath("dir"), 0755); err != nil { 158 | t.Fatalf("Mkdir failed after signal reception; sandboxfs may have exited: %v", err) 159 | } 160 | 161 | // Release the open file. This should cause sandboxfs to terminate within a limited amount 162 | // of time, so ensure it exited as expected. 163 | file.Close() 164 | if err := checkSignalHandled(state); err != nil { 165 | t.Fatal(err) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /integration/utils/checks.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "fmt" 19 | "io/ioutil" 20 | "os" 21 | "os/exec" 22 | "reflect" 23 | "regexp" 24 | "sort" 25 | "testing" 26 | ) 27 | 28 | // TODO(jmmv): All functions in this file should use t.Helper(), but we must first be ready to 29 | // switch to Go 1.9 externally. 30 | 31 | // MustMkdirAll wraps os.MkdirAll and immediately fails the test case on failure. 32 | // This is purely syntactic sugar to keep test setup short and concise. 33 | func MustMkdirAll(t *testing.T, path string, perm os.FileMode) { 34 | t.Helper() 35 | 36 | if err := os.MkdirAll(path, perm); err != nil { 37 | t.Fatalf("Failed to create directory %s: %v", path, err) 38 | } 39 | } 40 | 41 | // MustSymlink wraps os.Symlink and immediately fails the test case on failure. 42 | // This is purely syntactic sugar to keep test setup short and concise. 43 | // 44 | // Note that, compared to the other *OrFatal operations, this one does not take file permissions 45 | // into account because Linux does not have an lchmod(2) system call, nor Go offers a mechanism to 46 | // call it on the systems that support it. 47 | func MustSymlink(t *testing.T, target string, path string) { 48 | t.Helper() 49 | 50 | if err := os.Symlink(target, path); err != nil { 51 | t.Fatalf("Failed to create symlink %s: %v", path, err) 52 | } 53 | } 54 | 55 | // MustWriteFile wraps ioutil.WriteFile and immediately fails the test case on failure. 56 | // This is purely syntactic sugar to keep test setup short and concise. 57 | func MustWriteFile(t *testing.T, path string, perm os.FileMode, contents string) { 58 | t.Helper() 59 | 60 | if err := ioutil.WriteFile(path, []byte(contents), perm); err != nil { 61 | t.Fatalf("Failed to create file %s: %v", path, err) 62 | } 63 | } 64 | 65 | // RequireRoot checks if the test is running as root and skips the test with the given reason 66 | // otherwise. 67 | func RequireRoot(t *testing.T, skipReason string) *UnixUser { 68 | t.Helper() 69 | 70 | if os.Getuid() != 0 { 71 | t.Skipf(skipReason) 72 | } 73 | root, err := LookupUID(os.Getuid()) 74 | if err != nil { 75 | t.Fatalf("Failed to get details about root user: %v", err) 76 | } 77 | t.Logf("Running test as: %v", root) 78 | return root 79 | } 80 | 81 | // DirEquals checks if the contents of two directories are the same. The equality check is based 82 | // on the directory entry names and their modes. 83 | func DirEquals(path1 string, path2 string) error { 84 | names := make([]map[string]os.FileMode, 2) 85 | for i, path := range []string{path1, path2} { 86 | dirents, err := ioutil.ReadDir(path) 87 | if err != nil { 88 | return fmt.Errorf("failed to read contents of directory %s: %v", path, err) 89 | } 90 | names[i] = make(map[string]os.FileMode, len(dirents)) 91 | for _, dirent := range dirents { 92 | names[i][dirent.Name()] = dirent.Mode() 93 | } 94 | } 95 | if !reflect.DeepEqual(names[0], names[1]) { 96 | return fmt.Errorf("contents of directory %s do not match %s; got %v, want %v", path1, path2, names[1], names[0]) 97 | } 98 | return nil 99 | } 100 | 101 | // DirEntryNamesEqual checks if the names of the entries in the given directory match the expected 102 | // names in the given slice. The list of expected entries needs to be sorted alphabetically. 103 | func DirEntryNamesEqual(path string, wantNames []string) error { 104 | dirents, err := ioutil.ReadDir(path) 105 | if err != nil { 106 | return fmt.Errorf("failed to read contents of directory %s: %v", path, err) 107 | } 108 | 109 | var names []string 110 | for _, dirent := range dirents { 111 | names = append(names, dirent.Name()) 112 | } 113 | sort.Strings(names) 114 | 115 | if !reflect.DeepEqual(names, wantNames) { 116 | return fmt.Errorf("got entries %v for directory %s; want %v", names, path, wantNames) 117 | } 118 | 119 | return nil 120 | } 121 | 122 | // FileEquals checks if a file matches the expected contents. 123 | func FileEquals(path string, wantContents string) error { 124 | contents, err := ioutil.ReadFile(path) 125 | if err != nil { 126 | return err 127 | } 128 | if string(contents) != wantContents { 129 | return fmt.Errorf("file %s doesn't match expected contents: got '%s', want '%s'", path, contents, wantContents) 130 | } 131 | return nil 132 | } 133 | 134 | // runAs starts the given command as the given user. 135 | func runAs(user *UnixUser, arg ...string) error { 136 | cmd := exec.Command(arg[0], arg[1:]...) 137 | cmd.Stdout = os.Stdout 138 | cmd.Stderr = os.Stderr 139 | SetCredential(cmd, user) 140 | return cmd.Run() 141 | } 142 | 143 | // runAsSilent starts the given command as the given user and suppresses stdout/stderr output. 144 | func runAsSilent(user *UnixUser, arg ...string) error { 145 | cmd := exec.Command(arg[0], arg[1:]...) 146 | SetCredential(cmd, user) 147 | return cmd.Run() 148 | } 149 | 150 | // CreateFileAsUser creates the given file, running the operation as the given user. 151 | func CreateFileAsUser(path string, user *UnixUser) error { 152 | return runAs(user, "touch", path) 153 | } 154 | 155 | // MkdirAsUser creates the given directory, running the operation as the given user. 156 | func MkdirAsUser(path string, user *UnixUser) error { 157 | return runAs(user, "mkdir", path) 158 | } 159 | 160 | // MkfifoAsUser creates the given named pipe, running the operation as the given user. 161 | func MkfifoAsUser(path string, user *UnixUser) error { 162 | return runAs(user, "mkfifo", path) 163 | } 164 | 165 | // MoveAsUser moves the given file, running the operation as the given user. 166 | func MoveAsUser(source string, target string, user *UnixUser) error { 167 | return runAs(user, "mv", source, target) 168 | } 169 | 170 | // SymlinkAsUser creates the given symlink, running the operation as the given user. 171 | func SymlinkAsUser(target string, path string, user *UnixUser) error { 172 | return runAs(user, "ln", "-s", target, path) 173 | } 174 | 175 | // FileExistsAsUser checks if the given path is accessible by the given user. The user may be nil, 176 | // in which case the current user is assumed. 177 | func FileExistsAsUser(path string, user *UnixUser) error { 178 | // We cannot do the access test in-process by switching the effective UID/GID because we 179 | // must fully drop privileges to the given user. If we dropped privileges, we wouldn't be 180 | // able to restore them to root's for the remainder of the test. 181 | // 182 | // Also, using os.Lstat or running "test -e" is insufficient: FUSE still allows root to 183 | // traverse the file system (that is, to resolve nodes and even stat them) even if root has 184 | // not been granted access through the allow_other/allow_root options. Therefore, we must 185 | // read file contents to really validate the access control. Note that this might be a bug 186 | // in OSXFUSE. 187 | return runAsSilent(user, "cat", path) 188 | } 189 | 190 | // MatchesRegexp returns true if the given string s matches the pattern. 191 | func MatchesRegexp(pattern string, s string) bool { 192 | match, err := regexp.MatchString(pattern, s) 193 | if err != nil { 194 | // This function is intended to be used exclusively from tests, and as such we know 195 | // that the given pattern must be valid. If it's not, we've got a bug in the code 196 | // that must be fixed: there is no point in returning this as an error. 197 | panic(fmt.Sprintf("invalid regexp %s: %v; this is a bug in the test code", pattern, err)) 198 | } 199 | return match 200 | } 201 | -------------------------------------------------------------------------------- /integration/utils/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "fmt" 19 | "path/filepath" 20 | "strings" 21 | ) 22 | 23 | // Config represents the configuration for the integration tests as provided in the command line. 24 | type Config struct { 25 | // Features contains the set of features enabled during the build. This represents a set 26 | // and therefore the values in the map are meaningless. 27 | Features map[string]bool 28 | 29 | // ReleaseBinary is true if the binary provided in SandboxfsBinary was built in release 30 | // mode, false otherwise. 31 | ReleaseBinary bool 32 | 33 | // SandboxfsBinary contains the absolute path to the sandboxfs binary to test. 34 | SandboxfsBinary string 35 | 36 | // UnprivilegedUser is a non-root user to use when the integration tests are run as root, 37 | // by those tests that require dropping privileges. May be nil, in which case those tests 38 | // are skipped. 39 | UnprivilegedUser *UnixUser 40 | } 41 | 42 | // globalConfig contains the singleton instance of the configuration. This must be initialized at 43 | // test program startup time with the SetConfigFromFlags function and can later be queried at will 44 | // by any test. 45 | var globalConfig *Config 46 | 47 | // SetConfigFromFlags initializes the test configuration based on the raw values provided by the 48 | // user on the command line. Returns an error if any of those values is incorrect. 49 | func SetConfigFromFlags(rawFeatures string, releaseBinary bool, rawSandboxfsBinary string, unprivilegedUserName string) error { 50 | if globalConfig != nil { 51 | panic("SetConfigFromFlags can only be called once") 52 | } 53 | 54 | features := make(map[string]bool) 55 | for _, feature := range strings.Split(rawFeatures, " ") { 56 | features[feature] = true 57 | } 58 | 59 | sandboxfsBinary, err := filepath.Abs(rawSandboxfsBinary) 60 | if err != nil { 61 | return fmt.Errorf("cannot make %s absolute: %v", rawSandboxfsBinary, err) 62 | } 63 | 64 | var unprivilegedUser *UnixUser 65 | if unprivilegedUserName != "" { 66 | unprivilegedUser, err = LookupUser(unprivilegedUserName) 67 | if err != nil { 68 | return fmt.Errorf("invalid unprivileged user setting %s: %v", unprivilegedUserName, err) 69 | } 70 | } 71 | 72 | globalConfig = &Config{ 73 | Features: features, 74 | ReleaseBinary: releaseBinary, 75 | SandboxfsBinary: sandboxfsBinary, 76 | UnprivilegedUser: unprivilegedUser, 77 | } 78 | return nil 79 | } 80 | 81 | // GetConfig returns the singleon instance of the test configuration. 82 | func GetConfig() *Config { 83 | if globalConfig == nil { 84 | panic("GetConfig should have been called from main but was not yet") 85 | } 86 | 87 | return globalConfig 88 | } 89 | -------------------------------------------------------------------------------- /integration/utils/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | // Package utils provides support code for the sandboxfs integration tests. 16 | package utils 17 | -------------------------------------------------------------------------------- /integration/utils/time_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "os" 19 | "syscall" 20 | "time" 21 | ) 22 | 23 | // ZeroBtime indicates that the given timestamp could not be queried. 24 | var ZeroBtime = time.Date(0, 0, 0, 0, 0, 0, 0, time.Local) 25 | 26 | // Atime obtains the access time from a system-specific stat structure. 27 | func Atime(s *syscall.Stat_t) time.Time { 28 | return time.Unix(int64(s.Atimespec.Sec), int64(s.Atimespec.Nsec)) 29 | } 30 | 31 | // Btime obtains the birth time from a file. 32 | func Btime(path string) (time.Time, error) { 33 | fileInfo, err := os.Lstat(path) 34 | if err != nil { 35 | return ZeroBtime, err 36 | } 37 | 38 | s := fileInfo.Sys().(*syscall.Stat_t) 39 | return time.Unix(int64(s.Birthtimespec.Sec), int64(s.Birthtimespec.Nsec)), nil 40 | } 41 | 42 | // Ctime obtains the inode change time from a system-specific stat structure. 43 | func Ctime(s *syscall.Stat_t) time.Time { 44 | return time.Unix(int64(s.Ctimespec.Sec), int64(s.Ctimespec.Nsec)) 45 | } 46 | 47 | // Mtime obtains the modification time from a system-specific stat structure. 48 | func Mtime(s *syscall.Stat_t) time.Time { 49 | return time.Unix(int64(s.Mtimespec.Sec), int64(s.Mtimespec.Nsec)) 50 | } 51 | -------------------------------------------------------------------------------- /integration/utils/time_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "syscall" 19 | "time" 20 | 21 | "golang.org/x/sys/unix" 22 | ) 23 | 24 | // ZeroBtime indicates that the given timestamp could not be queried. 25 | var ZeroBtime = time.Date(0, 0, 0, 0, 0, 0, 0, time.Local) 26 | 27 | // Atime obtains the access time from a system-specific stat structure. 28 | func Atime(s *syscall.Stat_t) time.Time { 29 | return time.Unix(int64(s.Atim.Sec), int64(s.Atim.Nsec)) 30 | } 31 | 32 | // Btime obtains the birth time from a system-specific stat structure. 33 | func Btime(path string) (time.Time, error) { 34 | var stx unix.Statx_t 35 | if err := unix.Statx(unix.AT_FDCWD, path, unix.AT_SYMLINK_NOFOLLOW, unix.STATX_BTIME, &stx); err != nil { 36 | return ZeroBtime, err 37 | } 38 | if stx.Mask&unix.STATX_BTIME == 0 { 39 | return ZeroBtime, nil 40 | } 41 | return time.Unix(int64(stx.Btime.Sec), int64(stx.Btime.Nsec)), nil 42 | } 43 | 44 | // Ctime obtains the inode change time from a system-specific stat structure. 45 | func Ctime(s *syscall.Stat_t) time.Time { 46 | return time.Unix(int64(s.Ctim.Sec), int64(s.Ctim.Nsec)) 47 | } 48 | 49 | // Mtime obtains the modification time from a system-specific stat structure. 50 | func Mtime(s *syscall.Stat_t) time.Time { 51 | return time.Unix(int64(s.Mtim.Sec), int64(s.Mtim.Nsec)) 52 | } 53 | -------------------------------------------------------------------------------- /integration/utils/unmount_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "os/exec" 21 | ) 22 | 23 | // Unmount unmounts the given file system. 24 | func Unmount(path string) error { 25 | cmd := exec.Command("umount", path) 26 | cmd.Stdout = os.Stdout 27 | cmd.Stderr = os.Stderr 28 | if err := cmd.Run(); err != nil { 29 | return fmt.Errorf("exec of umount %s failed: %v", path, err) 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /integration/utils/unmount_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "os/exec" 21 | ) 22 | 23 | // Unmount unmounts the given file system. 24 | func Unmount(path string) error { 25 | cmd := exec.Command("fusermount", "-u", path) 26 | cmd.Stdout = os.Stdout 27 | cmd.Stderr = os.Stderr 28 | if err := cmd.Run(); err != nil { 29 | return fmt.Errorf("exec of fusermount -u %s failed: %v", path, err) 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /integration/utils/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "os/exec" 21 | "os/user" 22 | "strconv" 23 | "syscall" 24 | 25 | "golang.org/x/sys/unix" 26 | ) 27 | 28 | // UnixUser represents a Unix user account. This differs from the standard user.User in that the 29 | // values of the fields here are in integer form as is customary in Unix. 30 | type UnixUser struct { 31 | // Username is the login name. 32 | Username string 33 | // UID is the numeric user identifier. 34 | UID int 35 | // GID is the numeric group identifier. 36 | GID int 37 | // Groups is the list of secondary groups this user belongs to. 38 | Groups []int 39 | } 40 | 41 | // String formats the user details for display. 42 | func (u *UnixUser) String() string { 43 | return fmt.Sprintf("username=%s, uid=%d, gid=%d, groups=%v", u.Username, u.UID, u.GID, u.Groups) 44 | } 45 | 46 | // ToCredential converts the user details to a credential usable by exec.Cmd. 47 | func (u *UnixUser) ToCredential() *syscall.Credential { 48 | groups := make([]uint32, len(u.Groups)) 49 | for i, gid := range u.Groups { 50 | groups[i] = uint32(gid) 51 | } 52 | return &syscall.Credential{ 53 | Uid: uint32(u.UID), 54 | Gid: uint32(u.GID), 55 | Groups: groups, 56 | } 57 | } 58 | 59 | // toUnixUser converts a generic user.User object into a Unix-specific user. 60 | func toUnixUser(user *user.User) (*UnixUser, error) { 61 | uid, err := strconv.Atoi(user.Uid) 62 | if err != nil { 63 | return nil, fmt.Errorf("invalid uid %s for user %s: %v", user.Uid, user.Username, err) 64 | } 65 | 66 | gid, err := strconv.Atoi(user.Gid) 67 | if err != nil { 68 | return nil, fmt.Errorf("invalid gid %s for user %s: %v", user.Gid, user.Username, err) 69 | } 70 | 71 | strGroups, err := user.GroupIds() 72 | if err != nil { 73 | return nil, fmt.Errorf("cannot get groups for %s: %v", user.Username, err) 74 | } 75 | groups := make([]int, len(strGroups)) 76 | for i, name := range strGroups { 77 | groups[i], err = strconv.Atoi(name) 78 | if err != nil { 79 | return nil, fmt.Errorf("invalid secondary gid %s for user %s: %v", name, user.Username, err) 80 | } 81 | } 82 | 83 | return &UnixUser{ 84 | Username: user.Username, 85 | UID: uid, 86 | GID: gid, 87 | Groups: groups, 88 | }, nil 89 | } 90 | 91 | // WriteErrorForUnwritableNode returns the expected error for operations that cannot succeed on 92 | // unwritable nodes. 93 | // 94 | // Unwritable nodes have read-only permissions. Because we use default_permissions when mounting 95 | // the daemon, the kernel performs access checks on its own based on those. For unprivileged users, 96 | // the kernel denies access and returns EACCES without even calling the FUSE handler for the desired 97 | // write operation. However, when running as root, the kernel bypasses this check and ends up 98 | // calling the operation, which then fails with EPERM. This function computes the expected error 99 | // code based on this logic. 100 | func WriteErrorForUnwritableNode() error { 101 | if os.Getuid() == 0 { 102 | return unix.EPERM 103 | } 104 | return unix.EACCES 105 | } 106 | 107 | // LookupUser looks up a user by username. 108 | func LookupUser(name string) (*UnixUser, error) { 109 | generic, err := user.Lookup(name) 110 | if err != nil { 111 | return nil, fmt.Errorf("cannot find user %s: %v", name, err) 112 | } 113 | return toUnixUser(generic) 114 | } 115 | 116 | // LookupUID looks up a user by UID. 117 | func LookupUID(uid int) (*UnixUser, error) { 118 | generic, err := user.LookupId(fmt.Sprintf("%d", uid)) 119 | if err != nil { 120 | return nil, fmt.Errorf("cannot find user %d: %v", uid, err) 121 | } 122 | return toUnixUser(generic) 123 | } 124 | 125 | // LookupUserOtherThan searches for a user whose username is different than all given ones. 126 | func LookupUserOtherThan(username ...string) (*UnixUser, error) { 127 | // Testing a bunch of low-numbered UIDs should be sufficient because most Unix systems, 128 | // if not all, have system accounts immediately after 0. 129 | var other *UnixUser 130 | for i := 1; i < 100; i++ { 131 | var err error 132 | other, err = LookupUID(i) 133 | if err != nil { 134 | continue 135 | } 136 | 137 | for _, name := range username { 138 | if other.Username == name { 139 | continue 140 | } 141 | } 142 | break 143 | } 144 | if other == nil { 145 | return nil, fmt.Errorf("cannot find an unprivileged user other than %v", username) 146 | } 147 | return other, nil 148 | } 149 | 150 | // SetCredential updates the spawn attributes of the given command to execute such command under the 151 | // credentials of the given user. 152 | // 153 | // For the simplicity of the caller, the attributes are not modified if the given user is nil or if 154 | // the given user matches the current user. 155 | func SetCredential(cmd *exec.Cmd, user *UnixUser) { 156 | if user == nil || user.UID == os.Getuid() { 157 | return 158 | } 159 | 160 | if cmd.SysProcAttr == nil { 161 | cmd.SysProcAttr = &syscall.SysProcAttr{} 162 | } 163 | if cmd.SysProcAttr.Credential != nil { 164 | // This function is intended to be used exclusively from tests, and as such we 165 | // expect the given cmd object to not have credentials set. If that were the case, 166 | // it'd indicate a bug in the code that must be fixed: there is no point in 167 | // returning this as an error. 168 | panic("SetCredential invoked on a cmd object that already includes user credentials") 169 | } 170 | cmd.SysProcAttr.Credential = user.ToCredential() 171 | cmd.SysProcAttr.Credential.NoSetGroups = true 172 | } 173 | -------------------------------------------------------------------------------- /integration/utils/xattr_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "golang.org/x/sys/unix" 19 | ) 20 | 21 | // MissingXattrErr is the errno we expect on a getxattr call for a missing extended attribute. 22 | var MissingXattrErr = unix.ENOATTR 23 | -------------------------------------------------------------------------------- /integration/utils/xattr_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "golang.org/x/sys/unix" 19 | ) 20 | 21 | // MissingXattrErr is the errno we expect on a getxattr call for a missing extended attribute. 22 | var MissingXattrErr = unix.ENODATA 23 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | use failure::Error; 16 | use nix::errno::Errno; 17 | use std::io; 18 | use std::path::PathBuf; 19 | 20 | /// Type that represents an error understood by the kernel. 21 | #[derive(Debug, Fail)] 22 | #[fail(display = "errno={}", errno)] 23 | pub struct KernelError { 24 | errno: Errno, 25 | } 26 | 27 | impl KernelError { 28 | /// Constructs a new error given a raw errno code. 29 | pub fn from_errno(errno: Errno) -> KernelError { 30 | KernelError { errno } 31 | } 32 | 33 | /// Obtains the errno code contained in this error as an integer. 34 | pub fn errno_as_i32(&self) -> i32 { 35 | self.errno as i32 36 | } 37 | } 38 | 39 | impl From for KernelError { 40 | fn from(e: io::Error) -> Self { 41 | match e.raw_os_error() { 42 | Some(errno) => KernelError::from_errno(Errno::from_i32(errno)), 43 | None => { 44 | warn!("Got io::Error without an errno; propagating as EIO: {}", e); 45 | KernelError::from_errno(Errno::EIO) 46 | }, 47 | } 48 | } 49 | } 50 | 51 | impl From for KernelError { 52 | fn from(e: nix::Error) -> Self { 53 | match e { 54 | nix::Error::Sys(errno) => KernelError::from_errno(errno), 55 | _ => { 56 | warn!("Got nix::Error without an errno; propagating as EIO: {}", e); 57 | KernelError::from_errno(Errno::EIO) 58 | } 59 | } 60 | } 61 | } 62 | 63 | /// An error indicating that a mapping specification (coming from the command line or from a 64 | /// reconfiguration operation) is invalid. 65 | #[derive(Debug, Eq, Fail, PartialEq)] 66 | pub enum MappingError { 67 | /// A path was required to be absolute but wasn't. 68 | #[fail(display = "path {:?} is not absolute", path)] 69 | PathNotAbsolute { 70 | /// The invalid path. 71 | path: PathBuf, 72 | }, 73 | 74 | /// A path contains non-normalized components (like ".."). 75 | #[fail(display = "path {:?} is not normalized", path)] 76 | PathNotNormalized { 77 | /// The invalid path. 78 | path: PathBuf, 79 | }, 80 | } 81 | 82 | /// Flattens all causes of an error into a single string. 83 | pub fn flatten_causes(err: &Error) -> String { 84 | err.iter_chain().fold(String::new(), |flattened, cause| { 85 | let flattened = if flattened.is_empty() { 86 | flattened 87 | } else { 88 | flattened + ": " 89 | }; 90 | flattened + &format!("{}", cause) 91 | }) 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use super::*; 97 | 98 | #[test] 99 | fn flatten_causes_one() { 100 | let err = Error::from(format_err!("root cause")); 101 | assert_eq!("root cause", flatten_causes(&err)); 102 | } 103 | 104 | #[test] 105 | fn flatten_causes_multiple() { 106 | let err = Error::from(format_err!("root cause")); 107 | let err = Error::from(err.context("intermediate")); 108 | let err = Error::from(err.context("top")); 109 | assert_eq!("top: intermediate: root cause", flatten_causes(&err)); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | //! Command-line interface for the sandboxfs file system. 16 | 17 | // Keep these in sync with the list of checks in lib.rs. 18 | #![warn(bad_style, missing_docs)] 19 | #![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)] 20 | #![warn(unsafe_code)] 21 | 22 | extern crate env_logger; 23 | #[macro_use] extern crate failure; 24 | extern crate getopts; 25 | #[macro_use] extern crate log; 26 | extern crate sandboxfs; 27 | extern crate time; 28 | 29 | use failure::{Fallible, ResultExt}; 30 | use getopts::Options; 31 | use std::env; 32 | use std::path::{Path, PathBuf}; 33 | use std::process; 34 | use std::result::Result; 35 | use std::sync::Arc; 36 | use time::Timespec; 37 | 38 | /// Default value of the `--input` and `--output` flags. 39 | static DEFAULT_INOUT: &str = "-"; 40 | 41 | /// Default value of the `--ttl` flag. 42 | /// 43 | /// This is expressed as a string rather than a parsed value to ensure the default value can be 44 | /// parsed with the same semantics as user-provided values. 45 | static DEFAULT_TTL: &str = "60s"; 46 | 47 | /// Suffix for durations expressed in seconds. 48 | static SECONDS_SUFFIX: &str = "s"; 49 | 50 | /// Execution failure due to a user-triggered error. 51 | #[derive(Debug, Fail)] 52 | #[fail(display = "{}", message)] 53 | struct UsageError { 54 | message: String, 55 | } 56 | 57 | /// Parses the value of a flag that controls who has access to the mount point. 58 | /// 59 | /// Returns the collection of options, if any, to be passed to the FUSE mount operation in order to 60 | /// grant the requested permissions. 61 | fn parse_allow(s: &str) -> Fallible<&'static [&'static str]> { 62 | match s { 63 | "other" => Ok(&["-o", "allow_other"]), 64 | "root" => { 65 | if cfg!(target_os = "linux") { 66 | // "-o allow_root" is broken on Linux because this is not actually a 67 | // fusermount option: it is a libfuse option and the Go bindings don't 68 | // implement it as such. We could implement this on our own by handling 69 | // allow_root as if it were allow_other with an explicit user check... but 70 | // it's probably not worth doing. For now, just tell the user that we know 71 | // about the breakage. 72 | // 73 | // See https://github.com/bazil/fuse/issues/144 for context (which is about Go 74 | // but applies equally here). 75 | Err(format_err!("--allow=root is known to be broken on Linux")) 76 | } else { 77 | Ok(&["-o", "allow_root"]) 78 | } 79 | }, 80 | "self" => Ok(&[]), 81 | _ => { 82 | let message = format!("{} must be one of other, root, or self", s); 83 | Err(UsageError { message }.into()) 84 | }, 85 | } 86 | } 87 | 88 | /// Parses the value of a flag that takes a duration, which must specify its unit. 89 | fn parse_duration(s: &str) -> Result { 90 | let (value, unit) = match s.find(|c| !char::is_ascii_digit(&c) && c != '-') { 91 | Some(pos) => s.split_at(pos), 92 | None => { 93 | let message = format!("invalid time specification {}: missing unit", s); 94 | return Err(UsageError { message }); 95 | }, 96 | }; 97 | 98 | if unit != SECONDS_SUFFIX { 99 | let message = format!( 100 | "invalid time specification {}: unsupported unit '{}' (only '{}' is allowed)", 101 | s, unit, SECONDS_SUFFIX); 102 | return Err(UsageError { message }); 103 | } 104 | 105 | value.parse::() 106 | .map(|sec| Timespec { sec: i64::from(sec), nsec: 0 }) 107 | .map_err(|e| UsageError { message: format!("invalid time specification {}: {}", s, e) }) 108 | } 109 | 110 | /// Takes the list of strings that represent mappings (supplied via multiple instances of the 111 | /// `--mapping` flag) and returns a parsed representation of those flags. 112 | fn parse_mappings, U: IntoIterator>(args: U) 113 | -> Result, UsageError> { 114 | let mut mappings = Vec::new(); 115 | 116 | for arg in args { 117 | let arg = arg.as_ref(); 118 | 119 | let fields: Vec<&str> = arg.split(':').collect(); 120 | if fields.len() != 3 { 121 | let message = format!("bad mapping {}: expected three colon-separated fields", arg); 122 | return Err(UsageError { message }); 123 | } 124 | 125 | let writable = { 126 | if fields[0] == "ro" { 127 | false 128 | } else if fields[0] == "rw" { 129 | true 130 | } else { 131 | let message = format!("bad mapping {}: type was {} but should be ro or rw", 132 | arg, fields[0]); 133 | return Err(UsageError { message }); 134 | } 135 | }; 136 | 137 | let path = PathBuf::from(fields[1]); 138 | let underlying_path = PathBuf::from(fields[2]); 139 | 140 | match sandboxfs::Mapping::from_parts(path, underlying_path, writable) { 141 | Ok(mapping) => mappings.push(mapping), 142 | Err(e) => { 143 | // TODO(jmmv): Figure how to best leverage failure's cause propagation. May need 144 | // to define a custom ErrorKind to represent UsageError, instead of having a special 145 | // error type. 146 | let message = format!("bad mapping {}: {}", arg, e); 147 | return Err(UsageError { message }); 148 | } 149 | } 150 | } 151 | 152 | Ok(mappings) 153 | } 154 | 155 | /// Obtains the program name from the execution's first argument, or returns a default if the 156 | /// program name cannot be determined for whatever reason. 157 | fn program_name(args: &[String], default: &'static str) -> String { 158 | let default = String::from(default); 159 | match args.get(0) { 160 | Some(arg0) => match Path::new(arg0).file_name() { 161 | Some(basename) => match basename.to_str() { 162 | Some(basename) => String::from(basename), 163 | None => default, 164 | }, 165 | None => default, 166 | }, 167 | None => default, 168 | } 169 | } 170 | 171 | /// Parses the value of a flag specifying a file for I/O. 172 | /// 173 | /// `value` contains the textual value of the flag, which is returned as a path if present. 174 | /// Otherwise, if `value` is missing or if it matches `DEFAULT_INOUT`, then returns None. 175 | /// 176 | /// Note that, for simplicity, this will reopen any of the standard streams if provided as a path 177 | /// to `/dev/std*`. Even though that's discouraged (because reopening these devices under `sudo` is 178 | /// not possible), we cannot reliably prevent the user from supplying those paths. 179 | fn file_flag(value: &Option) -> Option { 180 | value.as_ref().and_then( 181 | |path| if path == DEFAULT_INOUT { None } else { Some(PathBuf::from(path) )}) 182 | } 183 | 184 | /// Prints program usage information to stdout. 185 | fn usage(program: &str, opts: &Options) { 186 | let brief = format!("Usage: {} [options] MOUNT_POINT", program); 187 | print!("{}", opts.usage(&brief)); 188 | } 189 | 190 | /// Prints version information to stdout. 191 | fn version() { 192 | println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); 193 | } 194 | 195 | /// Program's entry point. This is a "safe" version of `main` in the sense that this doesn't 196 | /// directly handle errors: all errors are returned to the caller for consistent reporter to the 197 | /// user depending on their type. 198 | fn safe_main(program: &str, args: &[String]) -> Fallible<()> { 199 | env_logger::init(); 200 | 201 | let cpus = num_cpus::get(); 202 | 203 | let mut opts = Options::new(); 204 | opts.optopt("", "allow", concat!("specifies who should have access to the file system", 205 | " (default: self)"), "other|root|self"); 206 | opts.optopt("", "cpu_profile", "enables CPU profiling and writes a profile to the given path", 207 | "PATH"); 208 | opts.optflag("", "help", "prints usage information and exits"); 209 | opts.optopt("", "input", 210 | &format!("where to read reconfiguration data from ({} for stdin)", DEFAULT_INOUT), 211 | "PATH"); 212 | opts.optmulti("", "mapping", "type and locations of a mapping", "TYPE:PATH:UNDERLYING_PATH"); 213 | opts.optflag("", "node_cache", "enables the path-based node cache (known broken)"); 214 | opts.optopt("", "output", 215 | &format!("where to write the reconfiguration status to ({} for stdout)", DEFAULT_INOUT), 216 | "PATH"); 217 | opts.optopt("", "reconfig_threads", 218 | &format!("number of reconfiguration threads (default: {})", cpus), "COUNT"); 219 | opts.optopt("", "ttl", 220 | &format!("how long the kernel is allowed to keep file metadata (default: {})", DEFAULT_TTL), 221 | &format!("TIME{}", SECONDS_SUFFIX)); 222 | opts.optflag("", "version", "prints version information and exits"); 223 | opts.optflag("", "xattrs", "enables support for extended attributes"); 224 | let matches = opts.parse(args)?; 225 | 226 | if matches.opt_present("help") { 227 | usage(&program, &opts); 228 | return Ok(()); 229 | } 230 | 231 | if matches.opt_present("version") { 232 | version(); 233 | return Ok(()); 234 | } 235 | 236 | let mut options = vec!("-o", "fsname=sandboxfs"); 237 | // TODO(jmmv): Support passing in arbitrary FUSE options from the command line, like "-o ro". 238 | 239 | if let Some(value) = matches.opt_str("allow") { 240 | for arg in parse_allow(&value)? { 241 | options.push(arg); 242 | } 243 | } 244 | 245 | let mappings = parse_mappings(matches.opt_strs("mapping"))?; 246 | 247 | let ttl = match matches.opt_str("ttl") { 248 | Some(value) => parse_duration(&value)?, 249 | None => parse_duration(DEFAULT_TTL).expect( 250 | "default value for flag is not accepted by the parser; this is a bug in the value"), 251 | }; 252 | 253 | let input = { 254 | let input_flag = matches.opt_str("input"); 255 | sandboxfs::open_input(file_flag(&input_flag)) 256 | .with_context(|_| format!("Failed to open reconfiguration input '{}'", 257 | input_flag.unwrap_or_else(|| DEFAULT_INOUT.to_owned())))? 258 | }; 259 | 260 | let output = { 261 | let output_flag = matches.opt_str("output"); 262 | sandboxfs::open_output(file_flag(&output_flag)) 263 | .with_context(|_| format!("Failed to open reconfiguration output '{}'", 264 | output_flag.unwrap_or_else(|| DEFAULT_INOUT.to_owned())))? 265 | }; 266 | 267 | let reconfig_threads = match matches.opt_str("reconfig_threads") { 268 | Some(value) => { 269 | match value.parse::() { 270 | Ok(n) => n, 271 | Err(e) => return Err(UsageError { 272 | message: format!("invalid thread count {}: {}", value, e) 273 | }.into()), 274 | } 275 | }, 276 | None => cpus, 277 | }; 278 | 279 | let mount_point = if matches.free.len() == 1 { 280 | Path::new(&matches.free[0]) 281 | } else { 282 | return Err(UsageError { message: "invalid number of arguments".to_string() }.into()); 283 | }; 284 | 285 | let node_cache: sandboxfs::ArcCache = if matches.opt_present("node_cache") { 286 | warn!("Using --node_cache is known to be broken under certain scenarios; see the manpage"); 287 | Arc::from(sandboxfs::PathCache::default()) 288 | } else { 289 | Arc::from(sandboxfs::NoCache::default()) 290 | }; 291 | 292 | let _profiler; 293 | if let Some(path) = matches.opt_str("cpu_profile") { 294 | _profiler = sandboxfs::ScopedProfiler::start(&path).context("Failed to start CPU profile")?; 295 | }; 296 | sandboxfs::mount( 297 | mount_point, &options, &mappings, ttl, node_cache, matches.opt_present("xattrs"), 298 | input, output, reconfig_threads) 299 | .with_context(|_| format!("Failed to mount {}", mount_point.display()))?; 300 | Ok(()) 301 | } 302 | 303 | /// Program's entry point. This delegates to `safe_main` for all program logic and is just in 304 | /// charge of consistently formatting and reporting all possible errors to the caller. 305 | fn main() { 306 | let args: Vec = env::args().collect(); 307 | let program = program_name(&args, "sandboxfs"); 308 | 309 | if let Err(err) = safe_main(&program, &args[1..]) { 310 | if let Some(err) = err.downcast_ref::() { 311 | eprintln!("Usage error: {}", err); 312 | eprintln!("Type {} --help for more information", program); 313 | process::exit(2); 314 | } else if let Some(err) = err.downcast_ref::() { 315 | eprintln!("Usage error: {}", err); 316 | eprintln!("Type {} --help for more information", program); 317 | process::exit(2); 318 | } else { 319 | eprintln!("{}: {}", program, sandboxfs::flatten_causes(&err)); 320 | process::exit(1); 321 | } 322 | } 323 | } 324 | 325 | #[cfg(test)] 326 | mod tests { 327 | use sandboxfs::Mapping; 328 | use super::*; 329 | 330 | /// Checks that an error, once formatted for printing, contains the given substring. 331 | fn err_contains(substr: &str, err: impl failure::Fail) { 332 | let formatted = format!("{}", err); 333 | assert!(formatted.contains(substr), 334 | "bad error message '{}'; does not contain '{}'", formatted, substr); 335 | } 336 | 337 | #[test] 338 | fn test_parse_duration_ok() { 339 | assert_eq!(Timespec { sec: 1234, nsec: 0 }, parse_duration("1234s").unwrap()); 340 | } 341 | 342 | #[test] 343 | fn test_parse_duration_bad_unit() { 344 | err_contains("missing unit", parse_duration("1234").unwrap_err()); 345 | err_contains("unsupported unit 'ms'", parse_duration("1234ms").unwrap_err()); 346 | err_contains("unsupported unit 'ss'", parse_duration("1234ss").unwrap_err()); 347 | } 348 | 349 | #[test] 350 | fn test_parse_duration_bad_value() { 351 | err_contains("invalid digit", parse_duration("-5s").unwrap_err()); 352 | err_contains("unsupported unit ' s'", parse_duration("5 s").unwrap_err()); 353 | err_contains("unsupported unit ' 5s'", parse_duration(" 5s").unwrap_err()); 354 | } 355 | 356 | #[test] 357 | fn test_parse_mappings_ok() { 358 | let args = ["ro:/:/fake/root", "rw:/foo:/bar"]; 359 | let exp_mappings = vec!( 360 | Mapping::from_parts(PathBuf::from("/"), PathBuf::from("/fake/root"), false).unwrap(), 361 | Mapping::from_parts(PathBuf::from("/foo"), PathBuf::from("/bar"), true).unwrap(), 362 | ); 363 | match parse_mappings(&args) { 364 | Ok(mappings) => assert_eq!(exp_mappings, mappings), 365 | Err(e) => panic!(e), 366 | } 367 | } 368 | 369 | #[test] 370 | fn test_parse_mappings_bad_format() { 371 | for arg in ["", "foo:bar", "foo:bar:baz:extra"].iter() { 372 | let err = parse_mappings(&[arg]).unwrap_err(); 373 | err_contains( 374 | &format!("bad mapping {}: expected three colon-separated fields", arg), err); 375 | } 376 | } 377 | 378 | #[test] 379 | fn test_parse_mappings_bad_type() { 380 | let args = ["rr:/foo:/bar"]; 381 | let err = parse_mappings(&args).unwrap_err(); 382 | err_contains("bad mapping rr:/foo:/bar: type was rr but should be ro or rw", err); 383 | } 384 | 385 | #[test] 386 | fn test_parse_mappings_bad_path() { 387 | let args = ["ro:foo:/bar"]; 388 | let err = parse_mappings(&args).unwrap_err(); 389 | err_contains("bad mapping ro:foo:/bar: path \"foo\" is not absolute", err); 390 | } 391 | 392 | #[test] 393 | fn test_parse_mappings_bad_underlying_path() { 394 | let args = ["ro:/foo:bar"]; 395 | let err = parse_mappings(&args).unwrap_err(); 396 | err_contains("bad mapping ro:/foo:bar: path \"bar\" is not absolute", err); 397 | } 398 | 399 | #[test] 400 | fn test_program_name_uses_default_on_errors() { 401 | assert_eq!("default", program_name(&[], "default")); 402 | } 403 | 404 | #[test] 405 | fn test_program_name_uses_file_name_only() { 406 | assert_eq!("b", program_name(&["a/b".to_string()], "unused")); 407 | assert_eq!("foo", program_name(&["./x/y/foo".to_string()], "unused")); 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /src/nodes/caches.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | use {fuse, IdGenerator}; 16 | use nodes::{ArcNode, Cache, Dir, File, Symlink}; 17 | use std::collections::HashMap; 18 | use std::fs; 19 | use std::path::{Path, PathBuf}; 20 | use std::sync::Mutex; 21 | 22 | /// Node factory without any caching. 23 | #[derive(Default)] 24 | pub struct NoCache { 25 | } 26 | 27 | impl Cache for NoCache { 28 | fn get_or_create(&self, ids: &IdGenerator, underlying_path: &Path, attr: &fs::Metadata, 29 | writable: bool) -> ArcNode { 30 | if attr.is_dir() { 31 | Dir::new_mapped(ids.next(), underlying_path, attr, writable) 32 | } else if attr.file_type().is_symlink() { 33 | Symlink::new_mapped(ids.next(), underlying_path, attr, writable) 34 | } else { 35 | File::new_mapped(ids.next(), underlying_path, attr, writable) 36 | } 37 | } 38 | 39 | fn delete(&self, _path: &Path, _file_type: fuse::FileType) { 40 | // Nothing to do. 41 | } 42 | 43 | fn rename(&self, _old_path: &Path, _new_path: PathBuf, _file_type: fuse::FileType) { 44 | // Nothing to do. 45 | } 46 | } 47 | 48 | /// Cache of sandboxfs nodes indexed by their underlying path. 49 | /// 50 | /// This cache is critical to offer good performance during reconfigurations: if the identity of an 51 | /// underlying file changes across reconfigurations, the kernel will think it's a different file 52 | /// (even if it may not be) and will therefore not be able to take advantage of any caches. You 53 | /// would think that avoiding kernel cache invalidations during the reconfiguration itself (e.g. if 54 | /// file `A` was mapped and is still mapped now, don't invalidate it) would be sufficient to avoid 55 | /// this problem, but it's not: `A` could be mapped, then unmapped, and then remapped again in three 56 | /// different reconfigurations, and we'd still not want to lose track of it. 57 | /// 58 | /// Nodes should be inserted in this cache at creation time and removed from it when explicitly 59 | /// deleted by the user (because there is a chance they'll be recreated, and at that point we truly 60 | /// want to reload the data from disk). 61 | /// 62 | /// TODO(jmmv): There currently is no cache expiration, which means that memory usage can grow 63 | /// unboundedly. A preliminary attempt at expiring cache entries on a node's forget handler sounded 64 | /// promising (because then cache expiration would be delegated to the kernel)... but, on Linux, the 65 | /// kernel seems to be calling this very eagerly, rendering our cache useless. I did not track down 66 | /// what exactly triggered the forget notifications though. 67 | /// 68 | /// TODO(jmmv): This cache has proven to be problematic in some cases and should probably be 69 | /// removed. See https://jmmv.dev/2020/01/osxfuse-hardlinks-dladdr.html for details. 70 | #[derive(Default)] 71 | pub struct PathCache { 72 | entries: Mutex>, 73 | } 74 | 75 | impl Cache for PathCache { 76 | fn get_or_create(&self, ids: &IdGenerator, underlying_path: &Path, attr: &fs::Metadata, 77 | writable: bool) -> ArcNode { 78 | if attr.is_dir() { 79 | // Directories cannot be cached because they contain entries that are created only 80 | // in memory based on the mappings configuration. 81 | // 82 | // TODO(jmmv): Actually, they *could* be cached, but it's hard. Investigate doing so 83 | // after quantifying how much it may benefit performance. 84 | return Dir::new_mapped(ids.next(), underlying_path, attr, writable); 85 | } 86 | 87 | let mut entries = self.entries.lock().unwrap(); 88 | 89 | if let Some(node) = entries.get(underlying_path) { 90 | if node.writable() == writable { 91 | // We have a match from the cache! Return it immediately. 92 | // 93 | // It is tempting to ensure that the type of the cached node matches the type we 94 | // want to return based on the metadata we have now in `attr`... but doing so does 95 | // not really prevent problems: the type of the underlying file can change at any 96 | // point in time. We could check this here and the type could change immediately 97 | // afterwards behind our backs, so don't bother. 98 | return node.clone(); 99 | } 100 | 101 | // We had a match... but node writability has changed; recreate the node. 102 | // 103 | // You may wonder why we care about this and not the file type as described above: the 104 | // reason is that the writability property is a setting of the mappings, not a property 105 | // of the underlying files, and thus it's a setting that we fully control and must keep 106 | // correct across reconfigurations or across different mappings of the same files. 107 | info!("Missed node caching opportunity because writability has changed for {:?}", 108 | underlying_path) 109 | } 110 | 111 | let node: ArcNode = if attr.is_dir() { 112 | panic!("Directory entries cannot be cached and are handled above"); 113 | } else if attr.file_type().is_symlink() { 114 | Symlink::new_mapped(ids.next(), underlying_path, attr, writable) 115 | } else { 116 | File::new_mapped(ids.next(), underlying_path, attr, writable) 117 | }; 118 | entries.insert(underlying_path.to_path_buf(), node.clone()); 119 | node 120 | } 121 | 122 | fn delete(&self, path: &Path, file_type: fuse::FileType) { 123 | let mut entries = self.entries.lock().unwrap(); 124 | if file_type == fuse::FileType::Directory { 125 | debug_assert!(!entries.contains_key(path), "Directories are not currently cached"); 126 | } else { 127 | entries.remove(path).expect("Tried to delete unknown path from the cache"); 128 | } 129 | } 130 | 131 | fn rename(&self, old_path: &Path, new_path: PathBuf, file_type: fuse::FileType) { 132 | let mut entries = self.entries.lock().unwrap(); 133 | if file_type == fuse::FileType::Directory { 134 | debug_assert!(!entries.contains_key(old_path), "Directories are not currently cached"); 135 | } else { 136 | let node = entries.remove(old_path).expect("Tried to rename unknown path in the cache"); 137 | entries.insert(new_path, node); 138 | } 139 | } 140 | } 141 | 142 | #[cfg(test)] 143 | mod tests { 144 | use super::*; 145 | use tempfile::tempdir; 146 | use testutils; 147 | 148 | #[test] 149 | fn path_cache_behavior() { 150 | let root = tempdir().unwrap(); 151 | 152 | let dir1 = root.path().join("dir1"); 153 | fs::create_dir(&dir1).unwrap(); 154 | let dir1attr = fs::symlink_metadata(&dir1).unwrap(); 155 | 156 | let file1 = root.path().join("file1"); 157 | drop(fs::File::create(&file1).unwrap()); 158 | let file1attr = fs::symlink_metadata(&file1).unwrap(); 159 | 160 | let file2 = root.path().join("file2"); 161 | drop(fs::File::create(&file2).unwrap()); 162 | let file2attr = fs::symlink_metadata(&file2).unwrap(); 163 | 164 | let ids = IdGenerator::new(1); 165 | let cache = PathCache::default(); 166 | 167 | // Directories are not cached no matter what. 168 | assert_eq!(1, cache.get_or_create(&ids, &dir1, &dir1attr, false).inode()); 169 | assert_eq!(2, cache.get_or_create(&ids, &dir1, &dir1attr, false).inode()); 170 | assert_eq!(3, cache.get_or_create(&ids, &dir1, &dir1attr, true).inode()); 171 | 172 | // Different files get different nodes. 173 | assert_eq!(4, cache.get_or_create(&ids, &file1, &file1attr, false).inode()); 174 | assert_eq!(5, cache.get_or_create(&ids, &file2, &file2attr, true).inode()); 175 | 176 | // Files we queried before but with different writability get different nodes. 177 | assert_eq!(6, cache.get_or_create(&ids, &file1, &file1attr, true).inode()); 178 | assert_eq!(7, cache.get_or_create(&ids, &file2, &file2attr, false).inode()); 179 | 180 | // We get cache hits when everything matches previous queries. 181 | assert_eq!(6, cache.get_or_create(&ids, &file1, &file1attr, true).inode()); 182 | assert_eq!(7, cache.get_or_create(&ids, &file2, &file2attr, false).inode()); 183 | 184 | // We don't get cache hits for nodes whose writability changed. 185 | assert_eq!(8, cache.get_or_create(&ids, &file1, &file1attr, false).inode()); 186 | assert_eq!(9, cache.get_or_create(&ids, &file2, &file2attr, true).inode()); 187 | } 188 | 189 | #[test] 190 | fn path_cache_nodes_support_all_file_types() { 191 | let ids = IdGenerator::new(1); 192 | let cache = PathCache::default(); 193 | 194 | for (_fuse_type, path) in testutils::AllFileTypes::new().entries { 195 | let fs_attr = fs::symlink_metadata(&path).unwrap(); 196 | // The following panics if it's impossible to represent the given file type, which is 197 | // what we are testing. 198 | cache.get_or_create(&ids, &path, &fs_attr, false); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/nodes/conv.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | use fuse; 16 | use nix::{errno, fcntl, sys}; 17 | use nix::sys::time::TimeValLike; 18 | use nodes::{KernelError, NodeResult}; 19 | use std::fs; 20 | use std::io; 21 | use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt, PermissionsExt}; 22 | use std::path::Path; 23 | use std::time::{SystemTime, UNIX_EPOCH}; 24 | use time::Timespec; 25 | 26 | /// Fixed point in time to use when we fail to interpret file system supplied timestamps. 27 | const BAD_TIME: Timespec = Timespec { sec: 0, nsec: 0 }; 28 | 29 | /// Converts a system time as represented in a fs::Metadata object to a Timespec. 30 | /// 31 | /// `path` is the file from which the timestamp was originally extracted and `name` represents the 32 | /// metadata field the timestamp corresponds to; both are used for debugging purposes only. 33 | /// 34 | /// If the given system time is missing, or if it is invalid, logs a warning and returns a fixed 35 | /// time. It is reasonable for these details to be missing because the backing file systems do 36 | /// not always implement all possible file timestamps. 37 | fn system_time_to_timespec(path: &Path, name: &str, time: &io::Result) -> Timespec { 38 | match time { 39 | Ok(time) => match time.duration_since(UNIX_EPOCH) { 40 | Ok(duration) => Timespec::new(duration.as_secs() as i64, 41 | duration.subsec_nanos() as i32), 42 | Err(e) => { 43 | warn!("File system returned {} {:?} for {:?} that's before the Unix epoch: {}", 44 | name, time, path, e); 45 | BAD_TIME 46 | } 47 | }, 48 | Err(e) => { 49 | debug!("File system did not return a {} timestamp for {:?}: {}", name, path, e); 50 | BAD_TIME 51 | }, 52 | } 53 | } 54 | 55 | /// Converts a `time::Timespec` object into a `sys::time::TimeVal`. 56 | // TODO(jmmv): Consider upstreaming this function or a constructor for TimeVal that takes the two 57 | // components separately. 58 | pub fn timespec_to_timeval(spec: Timespec) -> sys::time::TimeVal { 59 | use sys::time::TimeVal; 60 | TimeVal::seconds(spec.sec) + TimeVal::nanoseconds(spec.nsec.into()) 61 | } 62 | 63 | /// Converts a `sys::time::TimeVal` object into a `time::Timespec`. 64 | // TODO(jmmv): Consider upstreaming this function as a TimeVal method. 65 | pub fn timeval_to_timespec(val: sys::time::TimeVal) -> Timespec { 66 | let usec = if val.tv_usec() > sys::time::suseconds_t::from(std::i32::MAX) { 67 | warn!("Cannot represent too-long usec quantity {} in timespec; using 0", val.tv_usec()); 68 | 0 69 | } else { 70 | val.tv_usec() as i32 71 | }; 72 | Timespec::new(val.tv_sec() as sys::time::time_t, usec) 73 | } 74 | 75 | /// Converts a `sys::time::TimeVal` object into a `sys::time::TimeSpec`. 76 | pub fn timeval_to_nix_timespec(val: sys::time::TimeVal) -> sys::time::TimeSpec { 77 | let usec = if val.tv_usec() > sys::time::suseconds_t::from(std::i32::MAX) { 78 | warn!("Cannot represent too-long usec quantity {} in timespec; using 0", val.tv_usec()); 79 | 0 80 | } else { 81 | val.tv_usec() as i64 82 | }; 83 | sys::time::TimeSpec::nanoseconds((val.tv_sec() as i64) * 1_000_000_000 + usec) 84 | } 85 | 86 | /// Converts a file type as returned by the file system to a FUSE file type. 87 | /// 88 | /// `path` is the file from which the file type was originally extracted and is only for debugging 89 | /// purposes. 90 | /// 91 | /// If the given file type cannot be mapped to a FUSE file type (because we don't know about that 92 | /// type or, most likely, because the file type is bogus), logs a warning and returns a regular 93 | /// file type with the assumption that most operations should work on it. 94 | pub fn filetype_fs_to_fuse(path: &Path, fs_type: fs::FileType) -> fuse::FileType { 95 | if fs_type.is_block_device() { 96 | fuse::FileType::BlockDevice 97 | } else if fs_type.is_char_device() { 98 | fuse::FileType::CharDevice 99 | } else if fs_type.is_dir() { 100 | fuse::FileType::Directory 101 | } else if fs_type.is_fifo() { 102 | fuse::FileType::NamedPipe 103 | } else if fs_type.is_file() { 104 | fuse::FileType::RegularFile 105 | } else if fs_type.is_socket() { 106 | fuse::FileType::Socket 107 | } else if fs_type.is_symlink() { 108 | fuse::FileType::Symlink 109 | } else { 110 | warn!("File system returned invalid file type {:?} for {:?}", fs_type, path); 111 | fuse::FileType::RegularFile 112 | } 113 | } 114 | 115 | /// Converts metadata attributes supplied by the file system to a FUSE file attributes tuple. 116 | /// 117 | /// `inode` is the value of the FUSE inode (not the value of the inode supplied within `attr`) to 118 | /// fill into the returned file attributes. `path` is the file from which the attributes were 119 | /// originally extracted and is only for debugging purposes. `nlink` is the number of links to 120 | /// expose, which is a sandboxfs-internal property and does not match the on-disk value included 121 | /// in `attr`. 122 | /// 123 | /// Any errors encountered along the conversion process are logged and the corresponding field is 124 | /// replaced by a reasonable value that should work. In other words: all errors are swallowed. 125 | pub fn attr_fs_to_fuse(path: &Path, inode: u64, nlink: u32, attr: &fs::Metadata) -> fuse::FileAttr { 126 | let len = if attr.is_dir() { 127 | 2 // TODO(jmmv): Reevaluate what directory sizes should be. 128 | } else { 129 | attr.len() 130 | }; 131 | 132 | // TODO(https://github.com/bazelbuild/sandboxfs/issues/43): Using the underlying ctimes is 133 | // slightly wrong because the ctimes track changes to the inodes. In most cases, operations 134 | // that flow via sandboxfs will affect the underlying ctime and propagate through here, which is 135 | // fine, but other operations are purely in-memory. To properly handle those cases, we should 136 | // have our own ctime handling. 137 | let ctime = Timespec { sec: attr.ctime(), nsec: attr.ctime_nsec() as i32 }; 138 | 139 | let perm = match attr.permissions().mode() { 140 | // TODO(https://github.com/rust-lang/rust/issues/51577): Drop :: prefix. 141 | mode if mode > u32::from(::std::u16::MAX) => { 142 | warn!("File system returned mode {} for {:?}, which is too large; set to 0400", 143 | mode, path); 144 | 0o400 145 | }, 146 | mode => (mode as u16) & !(sys::stat::SFlag::S_IFMT.bits() as u16), 147 | }; 148 | 149 | let rdev = match attr.rdev() { 150 | // TODO(https://github.com/rust-lang/rust/issues/51577): Drop :: prefix. 151 | rdev if rdev > u64::from(::std::u32::MAX) => { 152 | warn!("File system returned rdev {} for {:?}, which is too large; set to 0", 153 | rdev, path); 154 | 0 155 | }, 156 | rdev => rdev as u32, 157 | }; 158 | 159 | fuse::FileAttr { 160 | ino: inode, 161 | kind: filetype_fs_to_fuse(path, attr.file_type()), 162 | nlink: nlink, 163 | size: len, 164 | blocks: 0, // TODO(jmmv): Reevaluate what blocks should be. 165 | atime: system_time_to_timespec(path, "atime", &attr.accessed()), 166 | mtime: system_time_to_timespec(path, "mtime", &attr.modified()), 167 | ctime: ctime, 168 | crtime: system_time_to_timespec(path, "crtime", &attr.created()), 169 | perm: perm, 170 | uid: attr.uid(), 171 | gid: attr.gid(), 172 | rdev: rdev, 173 | flags: 0, 174 | } 175 | } 176 | 177 | /// Converts a set of `flags` bitmask to an `fs::OpenOptions`. 178 | /// 179 | /// `allow_writes` indicates whether the file to be opened supports writes or not. If the flags 180 | /// don't match this condition, then this returns an error. 181 | pub fn flags_to_openoptions(flags: u32, allow_writes: bool) -> NodeResult { 182 | let flags = flags as i32; 183 | let oflag = fcntl::OFlag::from_bits_truncate(flags); 184 | 185 | let mut options = fs::OpenOptions::new(); 186 | options.read(true); 187 | if oflag.contains(fcntl::OFlag::O_WRONLY) | oflag.contains(fcntl::OFlag::O_RDWR) { 188 | if !allow_writes { 189 | return Err(KernelError::from_errno(errno::Errno::EPERM)); 190 | } 191 | if oflag.contains(fcntl::OFlag::O_WRONLY) { 192 | options.read(false); 193 | } 194 | options.write(true); 195 | } 196 | options.custom_flags(flags); 197 | Ok(options) 198 | } 199 | 200 | /// Asserts that two FUSE file attributes are equal. 201 | // 202 | // TODO(jmmv): Remove once rust-fuse 0.4 is released as it will derive Eq for FileAttr. 203 | pub fn fileattrs_eq(attr1: &fuse::FileAttr, attr2: &fuse::FileAttr) -> bool { 204 | attr1.ino == attr2.ino 205 | && attr1.kind == attr2.kind 206 | && attr1.nlink == attr2.nlink 207 | && attr1.size == attr2.size 208 | && attr1.blocks == attr2.blocks 209 | && attr1.atime == attr2.atime 210 | && attr1.mtime == attr2.mtime 211 | && attr1.ctime == attr2.ctime 212 | && attr1.crtime == attr2.crtime 213 | && attr1.perm == attr2.perm 214 | && attr1.uid == attr2.uid 215 | && attr1.gid == attr2.gid 216 | && attr1.rdev == attr2.rdev 217 | && attr1.flags == attr2.flags 218 | } 219 | 220 | #[cfg(test)] 221 | mod tests { 222 | use super::*; 223 | 224 | use nix::{errno, unistd}; 225 | use nix::sys::time::TimeValLike; 226 | use std::fs::File; 227 | use std::io::{Read, Write}; 228 | use std::os::unix; 229 | use std::time::Duration; 230 | use tempfile::tempdir; 231 | use testutils; 232 | 233 | /// Creates a file at `path` with the given `content` and closes it. 234 | fn create_file(path: &Path, content: &str) { 235 | let mut file = File::create(path).expect("Test file creation failed"); 236 | let written = file.write(content.as_bytes()).expect("Test file data write failed"); 237 | assert_eq!(content.len(), written, "Test file wasn't fully written"); 238 | } 239 | 240 | #[test] 241 | fn test_timespec_to_timeval() { 242 | let spec = Timespec { sec: 123, nsec: 45000 }; 243 | let val = timespec_to_timeval(spec); 244 | assert_eq!(123, val.tv_sec()); 245 | assert_eq!(45, val.tv_usec()); 246 | } 247 | 248 | #[test] 249 | fn test_timeval_to_timespec() { 250 | let val = sys::time::TimeVal::seconds(654) + sys::time::TimeVal::nanoseconds(123_456); 251 | let spec = timeval_to_timespec(val); 252 | assert_eq!(654, spec.sec); 253 | assert_eq!(123, spec.nsec); 254 | } 255 | 256 | #[test] 257 | fn test_timeval_to_nix_timespec() { 258 | let val = sys::time::TimeVal::seconds(654) + sys::time::TimeVal::nanoseconds(123_456); 259 | let spec = timeval_to_nix_timespec(val); 260 | assert_eq!(654, spec.tv_sec()); 261 | assert_eq!(123, spec.tv_nsec()); 262 | } 263 | 264 | #[test] 265 | fn test_system_time_to_timespec_ok() { 266 | let sys_time = SystemTime::UNIX_EPOCH + Duration::new(12345, 6789); 267 | let timespec = system_time_to_timespec( 268 | &Path::new("irrelevant"), "irrelevant", &Ok(sys_time)); 269 | assert_eq!(Timespec { sec: 12345, nsec: 6789 }, timespec); 270 | } 271 | 272 | #[test] 273 | fn test_system_time_to_timespec_bad() { 274 | let sys_time = SystemTime::UNIX_EPOCH - Duration::new(1, 0); 275 | let timespec = system_time_to_timespec( 276 | &Path::new("irrelevant"), "irrelevant", &Ok(sys_time)); 277 | assert_eq!(BAD_TIME, timespec); 278 | } 279 | 280 | #[test] 281 | fn test_system_time_to_timespec_missing() { 282 | let timespec = system_time_to_timespec( 283 | &Path::new("irrelevant"), "irrelevant", 284 | &Err(io::Error::from_raw_os_error(errno::Errno::ENOENT as i32))); 285 | assert_eq!(BAD_TIME, timespec); 286 | } 287 | 288 | #[test] 289 | fn test_filetype_fs_to_fuse() { 290 | let files = testutils::AllFileTypes::new(); 291 | for (exp_type, path) in files.entries { 292 | let fs_type = fs::symlink_metadata(&path).unwrap().file_type(); 293 | assert_eq!(exp_type, filetype_fs_to_fuse(&path, fs_type)); 294 | } 295 | } 296 | 297 | #[test] 298 | fn test_attr_fs_to_fuse_directory() { 299 | let dir = tempdir().unwrap(); 300 | let path = dir.path().join("root"); 301 | fs::create_dir(&path).unwrap(); 302 | fs::create_dir(path.join("subdir1")).unwrap(); 303 | fs::create_dir(path.join("subdir2")).unwrap(); 304 | 305 | fs::set_permissions(&path, fs::Permissions::from_mode(0o750)).unwrap(); 306 | sys::stat::utimes(&path, &sys::time::TimeVal::seconds(12345), 307 | &sys::time::TimeVal::seconds(678)).unwrap(); 308 | 309 | let exp_attr = fuse::FileAttr { 310 | ino: 1234, // Ensure underlying inode is not propagated. 311 | kind: fuse::FileType::Directory, 312 | nlink: 56, // TODO(jmmv): Should this account for subdirs? 313 | size: 2, 314 | blocks: 0, 315 | atime: Timespec { sec: 12345, nsec: 0 }, 316 | mtime: Timespec { sec: 678, nsec: 0 }, 317 | ctime: BAD_TIME, 318 | crtime: BAD_TIME, 319 | perm: 0o750, 320 | uid: unistd::getuid().as_raw(), 321 | gid: unistd::getgid().as_raw(), 322 | rdev: 0, 323 | flags: 0, 324 | }; 325 | 326 | let mut attr = attr_fs_to_fuse(&path, 1234, 56, &fs::symlink_metadata(&path).unwrap()); 327 | // We cannot really make any useful assertions on ctime and crtime as these cannot be 328 | // modified and may not be queryable, so stub them out. 329 | attr.ctime = BAD_TIME; 330 | attr.crtime = BAD_TIME; 331 | assert!(fileattrs_eq(&exp_attr, &attr)); 332 | } 333 | 334 | #[test] 335 | fn test_attr_fs_to_fuse_regular() { 336 | let dir = tempdir().unwrap(); 337 | let path = dir.path().join("file"); 338 | 339 | let content = "Some text\n"; 340 | create_file(&path, content); 341 | 342 | fs::set_permissions(&path, fs::Permissions::from_mode(0o640)).unwrap(); 343 | sys::stat::utimes(&path, &sys::time::TimeVal::seconds(54321), 344 | &sys::time::TimeVal::seconds(876)).unwrap(); 345 | 346 | let exp_attr = fuse::FileAttr { 347 | ino: 42, // Ensure underlying inode is not propagated. 348 | kind: fuse::FileType::RegularFile, 349 | nlink: 50, 350 | size: content.len() as u64, 351 | blocks: 0, 352 | atime: Timespec { sec: 54321, nsec: 0 }, 353 | mtime: Timespec { sec: 876, nsec: 0 }, 354 | ctime: BAD_TIME, 355 | crtime: BAD_TIME, 356 | perm: 0o640, 357 | uid: unistd::getuid().as_raw(), 358 | gid: unistd::getgid().as_raw(), 359 | rdev: 0, 360 | flags: 0, 361 | }; 362 | 363 | let mut attr = attr_fs_to_fuse(&path, 42, 50, &fs::symlink_metadata(&path).unwrap()); 364 | // We cannot really make any useful assertions on ctime and crtime as these cannot be 365 | // modified and may not be queryable, so stub them out. 366 | attr.ctime = BAD_TIME; 367 | attr.crtime = BAD_TIME; 368 | assert!(fileattrs_eq(&exp_attr, &attr)); 369 | } 370 | 371 | #[test] 372 | fn test_flags_to_openoptions_rdonly() { 373 | let dir = tempdir().unwrap(); 374 | let path = dir.path().join("file"); 375 | create_file(&path, "original content"); 376 | 377 | let flags = fcntl::OFlag::O_RDONLY.bits() as u32; 378 | let openoptions = flags_to_openoptions(flags, false).unwrap(); 379 | let mut file = openoptions.open(&path).unwrap(); 380 | 381 | write!(file, "foo").expect_err("Write to read-only file succeeded"); 382 | 383 | let mut buf = String::new(); 384 | file.read_to_string(&mut buf).expect("Read from read-only file failed"); 385 | assert_eq!("original content", buf); 386 | } 387 | 388 | #[test] 389 | fn test_flags_to_openoptions_wronly() { 390 | let dir = tempdir().unwrap(); 391 | let path = dir.path().join("file"); 392 | create_file(&path, ""); 393 | 394 | let flags = fcntl::OFlag::O_WRONLY.bits() as u32; 395 | flags_to_openoptions(flags, false).expect_err("Writability permission not respected"); 396 | let openoptions = flags_to_openoptions(flags, true).unwrap(); 397 | let mut file = openoptions.open(&path).unwrap(); 398 | 399 | let mut buf = String::new(); 400 | file.read_to_string(&mut buf).expect_err("Read from write-only file succeeded"); 401 | 402 | write!(file, "foo").expect("Write to write-only file failed"); 403 | } 404 | 405 | #[test] 406 | fn test_flags_to_openoptions_rdwr() { 407 | let dir = tempdir().unwrap(); 408 | let path = dir.path().join("file"); 409 | create_file(&path, "some content"); 410 | 411 | let flags = fcntl::OFlag::O_RDWR.bits() as u32; 412 | flags_to_openoptions(flags, false).expect_err("Writability permission not respected"); 413 | let openoptions = flags_to_openoptions(flags, true).unwrap(); 414 | let mut file = openoptions.open(&path).unwrap(); 415 | 416 | let mut buf = String::new(); 417 | file.read_to_string(&mut buf).expect("Read from read/write file failed"); 418 | 419 | write!(file, "foo").expect("Write to read/write file failed"); 420 | } 421 | 422 | #[test] 423 | fn test_flags_to_openoptions_custom() { 424 | let dir = tempdir().unwrap(); 425 | create_file(&dir.path().join("file"), ""); 426 | let path = dir.path().join("link"); 427 | unix::fs::symlink("file", &path).unwrap(); 428 | 429 | { 430 | let flags = fcntl::OFlag::O_RDONLY.bits() as u32; 431 | let openoptions = flags_to_openoptions(flags, true).unwrap(); 432 | openoptions.open(&path).expect("Failed to open symlink target; test setup bogus"); 433 | } 434 | 435 | let flags = (fcntl::OFlag::O_RDONLY | fcntl::OFlag::O_NOFOLLOW).bits() as u32; 436 | let openoptions = flags_to_openoptions(flags, true).unwrap(); 437 | openoptions.open(&path).expect_err("Open of symlink succeeded"); 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /src/nodes/file.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | extern crate fuse; 16 | 17 | use failure::Fallible; 18 | use nix::errno; 19 | use nodes::{ 20 | ArcHandle, ArcNode, AttrDelta, Cache, Handle, KernelError, Node, NodeResult, conv, setattr}; 21 | use std::ffi::OsStr; 22 | use std::fs; 23 | use std::os::unix::fs::FileExt; 24 | use std::path::{Path, PathBuf}; 25 | use std::sync::{Arc, Mutex}; 26 | 27 | /// Handle for an open file. 28 | struct OpenFile { 29 | /// Reference to the node's state for this file. Needed to update attributes on writes. 30 | state: Arc>, 31 | 32 | /// Handle for the open file descriptor. 33 | file: fs::File, 34 | } 35 | 36 | impl OpenFile { 37 | /// Creates a new handle that references the given node's `state` and the already-open `file`. 38 | fn from(state: Arc>, file: fs::File) -> OpenFile { 39 | Self { state, file } 40 | } 41 | } 42 | 43 | impl Handle for OpenFile { 44 | fn read(&self, offset: i64, size: u32) -> NodeResult> { 45 | let mut buffer = vec![0; size as usize]; 46 | let n = self.file.read_at(&mut buffer[..size as usize], offset as u64)?; 47 | buffer.truncate(n); 48 | Ok(buffer) 49 | } 50 | 51 | fn write(&self, offset: i64, mut data: &[u8]) -> NodeResult { 52 | const MAX_WRITE: usize = std::u32::MAX as usize; 53 | if data.len() > MAX_WRITE { 54 | // We only do this check because FUSE wants an u32 as the return value but data could 55 | // theoretically be bigger. 56 | // TODO(jmmv): Should fix the FUSE libraries to just expose Rust API-friendly quantities 57 | // (usize in this case) and handle the kernel/Rust boundary internally. 58 | warn!("Truncating too-long write to {} (asked for {} bytes)", MAX_WRITE, data.len()); 59 | data = &data[..MAX_WRITE]; 60 | } 61 | 62 | let mut state = self.state.lock().unwrap(); 63 | 64 | let n = self.file.write_at(data, offset as u64)?; 65 | debug_assert!(n <= MAX_WRITE, "Size bounds checked above"); 66 | 67 | let new_size = (offset as u64) + (n as u64); 68 | if state.attr.size < new_size { 69 | state.attr.size = new_size; 70 | } 71 | 72 | Ok(n as u32) 73 | } 74 | } 75 | 76 | /// Representation of a file node. 77 | /// 78 | /// File nodes represent all kinds of files (except for directories and symlinks), not just regular 79 | /// files, because the set of node operations required by them is the same. 80 | pub struct File { 81 | inode: u64, 82 | writable: bool, 83 | state: Arc>, 84 | } 85 | 86 | /// Holds the mutable data of a file node. 87 | struct MutableFile { 88 | underlying_path: Option, 89 | attr: fuse::FileAttr, 90 | } 91 | 92 | impl File { 93 | /// Returns true if this node can represent the given file type. 94 | fn supports_type(t: fs::FileType) -> bool { 95 | !t.is_dir() && !t.is_symlink() 96 | } 97 | 98 | /// Creates a new file backed by a file on an underlying file system. 99 | /// 100 | /// `inode` is the node number to assign to the created in-memory file and has no relation 101 | /// to the underlying file. `underlying_path` indicates the path to the file outside 102 | /// of the sandbox that backs this one. `fs_attr` contains the stat data for the given path. 103 | /// 104 | /// `fs_attr` is an input parameter because, by the time we decide to instantiate a file 105 | /// node (e.g. as we discover directory entries during readdir or lookup), we have already 106 | /// issued a stat on the underlying file system and we cannot re-do it for efficiency reasons. 107 | pub fn new_mapped(inode: u64, underlying_path: &Path, fs_attr: &fs::Metadata, writable: bool) 108 | -> ArcNode { 109 | if !File::supports_type(fs_attr.file_type()) { 110 | panic!("Can only construct based on non-directories / non-symlinks"); 111 | } 112 | let attr = conv::attr_fs_to_fuse(underlying_path, inode, 1, &fs_attr); 113 | 114 | let state = MutableFile { 115 | underlying_path: Some(PathBuf::from(underlying_path)), 116 | attr: attr, 117 | }; 118 | 119 | Arc::new(File { inode, writable, state: Arc::from(Mutex::from(state)) }) 120 | } 121 | 122 | /// Same as `getattr` but with the node already locked. 123 | fn getattr_locked(inode: u64, state: &mut MutableFile) -> NodeResult { 124 | if let Some(path) = &state.underlying_path { 125 | let fs_attr = fs::symlink_metadata(path)?; 126 | if !File::supports_type(fs_attr.file_type()) { 127 | warn!("Path {} backing a file node is no longer a file; got {:?}", 128 | path.display(), fs_attr.file_type()); 129 | return Err(KernelError::from_errno(errno::Errno::EIO)); 130 | } 131 | state.attr = conv::attr_fs_to_fuse(path, inode, state.attr.nlink, &fs_attr); 132 | } 133 | 134 | Ok(state.attr) 135 | } 136 | } 137 | 138 | impl Node for File { 139 | fn inode(&self) -> u64 { 140 | self.inode 141 | } 142 | 143 | fn writable(&self) -> bool { 144 | self.writable 145 | } 146 | 147 | fn file_type_cached(&self) -> fuse::FileType { 148 | let state = self.state.lock().unwrap(); 149 | state.attr.kind 150 | } 151 | 152 | fn delete(&self, cache: &dyn Cache) { 153 | let mut state = self.state.lock().unwrap(); 154 | assert!( 155 | state.underlying_path.is_some(), 156 | "Delete already called or trying to delete an explicit mapping"); 157 | cache.delete(state.underlying_path.as_ref().unwrap(), state.attr.kind); 158 | state.underlying_path = None; 159 | debug_assert!(state.attr.nlink >= 1); 160 | state.attr.nlink -= 1; 161 | } 162 | 163 | fn set_underlying_path(&self, path: &Path, cache: &dyn Cache) { 164 | let mut state = self.state.lock().unwrap(); 165 | debug_assert!(state.underlying_path.is_some(), 166 | "Renames should not have been allowed in scaffold or deleted nodes"); 167 | cache.rename( 168 | state.underlying_path.as_ref().unwrap(), path.to_owned(), state.attr.kind); 169 | state.underlying_path = Some(PathBuf::from(path)); 170 | } 171 | 172 | fn unmap(&self, inodes: &mut Vec) -> Fallible<()> { 173 | inodes.push(self.inode); 174 | Ok(()) 175 | } 176 | 177 | fn getattr(&self) -> NodeResult { 178 | let mut state = self.state.lock().unwrap(); 179 | File::getattr_locked(self.inode, &mut state) 180 | } 181 | 182 | fn getxattr(&self, name: &OsStr) -> NodeResult>> { 183 | let state = self.state.lock().unwrap(); 184 | match &state.underlying_path { 185 | Some(path) => Ok(xattr::get(path, name)?), 186 | None => Ok(None), 187 | } 188 | } 189 | 190 | fn handle_from(&self, file: fs::File) -> ArcHandle { 191 | Arc::from(OpenFile::from(self.state.clone(), file)) 192 | } 193 | 194 | fn listxattr(&self) -> NodeResult> { 195 | let state = self.state.lock().unwrap(); 196 | match &state.underlying_path { 197 | Some(path) => Ok(Some(xattr::list(path)?)), 198 | None => Ok(None), 199 | } 200 | } 201 | 202 | fn open(&self, flags: u32) -> NodeResult { 203 | let state = self.state.lock().unwrap(); 204 | 205 | let options = conv::flags_to_openoptions(flags, self.writable)?; 206 | let path = state.underlying_path.as_ref().expect( 207 | "Don't know how to handle a request to reopen a deleted file"); 208 | let file = options.open(&path)?; 209 | Ok(Arc::from(OpenFile::from(self.state.clone(), file))) 210 | } 211 | 212 | fn removexattr(&self, name: &OsStr) -> NodeResult<()> { 213 | let state = self.state.lock().unwrap(); 214 | match &state.underlying_path { 215 | Some(path) => Ok(xattr::remove(path, name)?), 216 | None => Err(KernelError::from_errno(errno::Errno::EACCES)), 217 | } 218 | } 219 | 220 | fn setattr(&self, delta: &AttrDelta) -> NodeResult { 221 | let mut state = self.state.lock().unwrap(); 222 | state.attr = setattr(state.underlying_path.as_ref(), &state.attr, delta)?; 223 | Ok(state.attr) 224 | } 225 | 226 | fn setxattr(&self, name: &OsStr, value: &[u8]) -> NodeResult<()> { 227 | let state = self.state.lock().unwrap(); 228 | match &state.underlying_path { 229 | Some(path) => Ok(xattr::set(path, name, value)?), 230 | None => Err(KernelError::from_errno(errno::Errno::EACCES)), 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/nodes/symlink.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | extern crate fuse; 16 | 17 | use failure::Fallible; 18 | use nix::errno; 19 | use nodes::{ArcNode, AttrDelta, Cache, KernelError, Node, NodeResult, conv, setattr}; 20 | use std::ffi::OsStr; 21 | use std::fs; 22 | use std::path::{Path, PathBuf}; 23 | use std::sync::{Arc, Mutex}; 24 | 25 | /// Representation of a symlink node. 26 | pub struct Symlink { 27 | inode: u64, 28 | writable: bool, 29 | state: Mutex, 30 | } 31 | 32 | /// Holds the mutable data of a symlink node. 33 | struct MutableSymlink { 34 | underlying_path: Option, 35 | attr: fuse::FileAttr, 36 | } 37 | 38 | impl Symlink { 39 | /// Creates a new symlink backed by a symlink on an underlying file system. 40 | /// 41 | /// `inode` is the node number to assign to the created in-memory symlink and has no relation 42 | /// to the underlying symlink. `underlying_path` indicates the path to the symlink outside 43 | /// of the sandbox that backs this one. `fs_attr` contains the stat data for the given path. 44 | /// 45 | /// `fs_attr` is an input parameter because, by the time we decide to instantiate a symlink 46 | /// node (e.g. as we discover directory entries during readdir or lookup), we have already 47 | /// issued a stat on the underlying file system and we cannot re-do it for efficiency reasons. 48 | pub fn new_mapped(inode: u64, underlying_path: &Path, fs_attr: &fs::Metadata, writable: bool) 49 | -> ArcNode { 50 | if !fs_attr.file_type().is_symlink() { 51 | panic!("Can only construct based on symlinks"); 52 | } 53 | let attr = conv::attr_fs_to_fuse(underlying_path, inode, 1, &fs_attr); 54 | 55 | let state = MutableSymlink { 56 | underlying_path: Some(PathBuf::from(underlying_path)), 57 | attr: attr, 58 | }; 59 | 60 | Arc::new(Symlink { inode, writable, state: Mutex::from(state) }) 61 | } 62 | 63 | /// Same as `getattr` but with the node already locked. 64 | fn getattr_locked(inode: u64, state: &mut MutableSymlink) -> NodeResult { 65 | if let Some(path) = &state.underlying_path { 66 | let fs_attr = fs::symlink_metadata(path)?; 67 | if !fs_attr.file_type().is_symlink() { 68 | warn!("Path {} backing a symlink node is no longer a symlink; got {:?}", 69 | path.display(), fs_attr.file_type()); 70 | return Err(KernelError::from_errno(errno::Errno::EIO)); 71 | } 72 | state.attr = conv::attr_fs_to_fuse(path, inode, state.attr.nlink, &fs_attr); 73 | } 74 | 75 | Ok(state.attr) 76 | } 77 | } 78 | 79 | impl Node for Symlink { 80 | fn inode(&self) -> u64 { 81 | self.inode 82 | } 83 | 84 | fn writable(&self) -> bool { 85 | self.writable 86 | } 87 | 88 | fn file_type_cached(&self) -> fuse::FileType { 89 | fuse::FileType::Symlink 90 | } 91 | 92 | fn delete(&self, cache: &dyn Cache) { 93 | let mut state = self.state.lock().unwrap(); 94 | assert!( 95 | state.underlying_path.is_some(), 96 | "Delete already called or trying to delete an explicit mapping"); 97 | cache.delete(state.underlying_path.as_ref().unwrap(), state.attr.kind); 98 | state.underlying_path = None; 99 | } 100 | 101 | fn set_underlying_path(&self, path: &Path, cache: &dyn Cache) { 102 | let mut state = self.state.lock().unwrap(); 103 | debug_assert!(state.underlying_path.is_some(), 104 | "Renames should not have been allowed in scaffold or deleted nodes"); 105 | cache.rename( 106 | state.underlying_path.as_ref().unwrap(), path.to_owned(), state.attr.kind); 107 | state.underlying_path = Some(PathBuf::from(path)); 108 | debug_assert!(state.attr.nlink >= 1); 109 | state.attr.nlink -= 1; 110 | } 111 | 112 | fn unmap(&self, inodes: &mut Vec) -> Fallible<()> { 113 | inodes.push(self.inode); 114 | Ok(()) 115 | } 116 | 117 | fn getattr(&self) -> NodeResult { 118 | let mut state = self.state.lock().unwrap(); 119 | Symlink::getattr_locked(self.inode, &mut state) 120 | } 121 | 122 | fn getxattr(&self, name: &OsStr) -> NodeResult>> { 123 | let state = self.state.lock().unwrap(); 124 | assert!( 125 | state.underlying_path.is_some(), 126 | "There is no known API to access the extended attributes of a symlink via an fd"); 127 | let value = xattr::get(state.underlying_path.as_ref().unwrap(), name)?; 128 | Ok(value) 129 | } 130 | 131 | fn listxattr(&self) -> NodeResult> { 132 | let state = self.state.lock().unwrap(); 133 | assert!( 134 | state.underlying_path.is_some(), 135 | "There is no known API to access the extended attributes of a symlink via an fd"); 136 | let xattrs = xattr::list(state.underlying_path.as_ref().unwrap())?; 137 | Ok(Some(xattrs)) 138 | } 139 | 140 | fn readlink(&self) -> NodeResult { 141 | let state = self.state.lock().unwrap(); 142 | 143 | let path = state.underlying_path.as_ref().expect( 144 | "There is no known API to get the target of a deleted symlink"); 145 | Ok(fs::read_link(path)?) 146 | } 147 | 148 | fn removexattr(&self, name: &OsStr) -> NodeResult<()> { 149 | let state = self.state.lock().unwrap(); 150 | assert!( 151 | state.underlying_path.is_some(), 152 | "There is no known API to access the extended attributes of a symlink via an fd"); 153 | xattr::remove(state.underlying_path.as_ref().unwrap(), name)?; 154 | Ok(()) 155 | } 156 | 157 | fn setattr(&self, delta: &AttrDelta) -> NodeResult { 158 | let mut state = self.state.lock().unwrap(); 159 | state.attr = setattr(state.underlying_path.as_ref(), &state.attr, delta)?; 160 | Ok(state.attr) 161 | } 162 | 163 | fn setxattr(&self, name: &OsStr, value: &[u8]) -> NodeResult<()> { 164 | let state = self.state.lock().unwrap(); 165 | assert!( 166 | state.underlying_path.is_some(), 167 | "There is no known API to access the extended attributes of a symlink via an fd"); 168 | xattr::set(state.underlying_path.as_ref().unwrap(), name, value)?; 169 | Ok(()) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/profiling.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | #[cfg(feature = "profiling")] use cpuprofiler::PROFILER; 16 | use failure::Fallible; 17 | use std::path::Path; 18 | 19 | /// Facade for `cpuprofiler::PROFILER` to cope with the optional gperftools dependency and to 20 | /// ensure profiling stops on `drop`. 21 | pub struct ScopedProfiler {} 22 | 23 | impl ScopedProfiler { 24 | #[cfg(not(feature = "profiling"))] 25 | fn real_start>(_path: P) -> Fallible { 26 | Err(format_err!("Compile-time \"profiling\" feature not enabled")) 27 | } 28 | 29 | #[cfg(feature = "profiling")] 30 | fn real_start>(path: P) -> Fallible { 31 | let path = path.as_ref(); 32 | let path_str = match path.to_str() { 33 | Some(path_str) => path_str, 34 | None => return Err(format_err!("Invalid path {}", path.display())), 35 | }; 36 | let mut profiler = PROFILER.lock().unwrap(); 37 | info!("Starting CPU profiler and writing results to {}", path_str); 38 | profiler.start(path_str.as_bytes()).unwrap(); 39 | Ok(ScopedProfiler {}) 40 | } 41 | 42 | /// Starts the CPU profiler and stores the profile in the given `path`. 43 | /// 44 | /// This will fail if sandboxfs was built without the "profiler" feature. This may fail if 45 | /// there are problems initializing the profiler. 46 | /// 47 | /// Note that, due to the nature of profiling, there can only be one `ScopedPointer` active at 48 | /// any given time. Trying to create two instances of this will cause this method to block 49 | /// until the other object is dropped. 50 | pub fn start>(path: P) -> Fallible { 51 | ScopedProfiler::real_start(path) 52 | } 53 | 54 | #[cfg(not(feature = "profiling"))] 55 | fn real_stop(&mut self) { 56 | } 57 | 58 | #[cfg(feature = "profiling")] 59 | fn real_stop(&mut self) { 60 | let mut profiler = PROFILER.lock().unwrap(); 61 | profiler.stop().expect("Profiler apparently not active, but it must have been"); 62 | info!("CPU profiler stopped"); 63 | } 64 | } 65 | 66 | impl Drop for ScopedProfiler { 67 | fn drop(&mut self) { 68 | self.real_stop() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/testutils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy 5 | // of the License at: 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | #![cfg(test)] 16 | 17 | use fuse; 18 | use nix::{sys, unistd}; 19 | use std::env; 20 | use std::fs; 21 | use std::os::unix; 22 | use std::path::PathBuf; 23 | use tempfile::{TempDir, tempdir}; 24 | use users; 25 | 26 | /// Holds a temporary directory and files of all possible kinds within it. 27 | /// 28 | /// The directory (including all of its contents) is removed when this object is dropped. 29 | pub struct AllFileTypes { 30 | #[allow(unused)] // Must retain to delay directory deletion. 31 | root: TempDir, 32 | 33 | /// Collection of test files. 34 | /// 35 | /// Tests should iterate over this vector and consume all entries to ensure all possible file 36 | /// types are verified everywhere. Prefer using `match` on the key to achieve this. 37 | // TODO(jmmv): This would be better as a HashMap of fuse::FileType to PathBuf, but we cannot do 38 | // so until FileTypes are comparable (which will happen with rust-fuse 0.4). 39 | pub entries: Vec<(fuse::FileType, PathBuf)>, 40 | } 41 | 42 | impl AllFileTypes { 43 | /// Creates a new temporary directory with files of all possible kinds within it. 44 | pub fn new() -> Self { 45 | let root = tempdir().unwrap(); 46 | 47 | let mut entries: Vec<(fuse::FileType, PathBuf)> = vec!(); 48 | 49 | if unistd::getuid().is_root() { 50 | let block_device = root.path().join("block_device"); 51 | sys::stat::mknod( 52 | &block_device, sys::stat::SFlag::S_IFBLK, sys::stat::Mode::S_IRUSR, 50).unwrap(); 53 | entries.push((fuse::FileType::BlockDevice, block_device)); 54 | 55 | let char_device = root.path().join("char_device"); 56 | sys::stat::mknod( 57 | &char_device, sys::stat::SFlag::S_IFCHR, sys::stat::Mode::S_IRUSR, 50).unwrap(); 58 | entries.push((fuse::FileType::CharDevice, char_device)); 59 | } else { 60 | warn!("Not running as root; cannot create block/char devices"); 61 | } 62 | 63 | let directory = root.path().join("dir"); 64 | fs::create_dir(&directory).unwrap(); 65 | entries.push((fuse::FileType::Directory, directory)); 66 | 67 | let named_pipe = root.path().join("named_pipe"); 68 | unistd::mkfifo(&named_pipe, sys::stat::Mode::S_IRUSR).unwrap(); 69 | entries.push((fuse::FileType::NamedPipe, named_pipe)); 70 | 71 | let regular = root.path().join("regular"); 72 | drop(fs::File::create(®ular).unwrap()); 73 | entries.push((fuse::FileType::RegularFile, regular)); 74 | 75 | let socket = root.path().join("socket"); 76 | drop(unix::net::UnixListener::bind(&socket).unwrap()); 77 | entries.push((fuse::FileType::Socket, socket)); 78 | 79 | let symlink = root.path().join("symlink"); 80 | unix::fs::symlink("irrelevant", &symlink).unwrap(); 81 | entries.push((fuse::FileType::Symlink, symlink)); 82 | 83 | AllFileTypes { root, entries } 84 | } 85 | } 86 | 87 | /// Holds user-provided configuration details for the tests. 88 | pub struct Config { 89 | /// The unprivileged user for tests that need to drop privileges. None if unset. 90 | pub unprivileged_user: Option, 91 | } 92 | 93 | impl Config { 94 | /// Queries the test configuration. 95 | pub fn get() -> Config { 96 | let unprivileged_user = env::var("UNPRIVILEGED_USER") 97 | .map(|name| users::get_user_by_name(&name)).unwrap_or(None); 98 | Config { unprivileged_user } 99 | } 100 | } 101 | --------------------------------------------------------------------------------