diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b9130e..53337b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: strategy: fail-fast: ${{ github.event_name == 'merge_group' }} matrix: - environment: [ubuntu-latest] + environment: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.environment }} @@ -71,11 +71,14 @@ jobs: - name: Install tools run: | - sudo apt install bubblewrap rustup install nightly rustup +nightly component add clippy cargo install group-runner || true + - name: Install Bubblewrap + if: ${{ matrix.environment == 'ubuntu-latest' }} + run: sudo apt install bubblewrap + - name: Build run: cargo test --no-run diff --git a/README.md b/README.md index c90a286..1b16f1d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A linker replacement to help protect against malicious build scripts -`build-wrap` "re-links" a build script so that it is executed under another command. By default the command is [Bubblewrap], though this is configurable. See [Environment variables] and [How it works] for more information. +`build-wrap` "re-links" a build script so that it is executed under another command. By default, the command is [Bubblewrap] (Linux) or [`sandbox-exec`] (macOS), though this is configurable. See [Environment variables] and [How it works] for more information. ## Installation @@ -20,7 +20,7 @@ Installing `build-wrap` requires two steps: ## Environment variables -- `BUILD_WRAP_CMD`: Command used to execute a build script. Default: +- `BUILD_WRAP_CMD`: Command used to execute a build script. Linux default: ```sh bwrap @@ -34,6 +34,25 @@ Installing `build-wrap` requires two steps: Note that `bwrap` is [Bubblewrap]. + macOS default: + + ```sh + sandbox-exec -p + (version\ 1)\ + (deny\ default)\ + (allow\ file-read*)\ # Allow read-only access everywhere + (allow\ file-write*\ (subpath\ "/dev"))\ # Allow write access to /dev + (allow\ file-write*\ (subpath\ "{OUT_DIR}"))\ # Allow write access to `OUT_DIR` + (allow\ file-write*\ (subpath\ "{TMPDIR}"))\ # Allow write access to `TMPDIR` + (allow\ process-exec)\ # Allow `exec` + (allow\ process-fork)\ # Allow `fork` + (allow\ sysctl-read)\ # Allow reading kernel state + (deny\ network*) # Deny network access + {} # Build script path + ``` + + Note that `(version\ 1)\ ... (deny\ network*)` expands to a single string (see [How `BUILD_WRAP_CMD` is expanded] below). + - `BUILD_WRAP_LD`: Linker to use. Default: `cc` Note that the above environment variables are read **when the build script is linked**. So, for example, changing `BUILD_WRAP_CMD` will not change the command used to execute already linked build scripts. @@ -65,4 +84,6 @@ Given a build script `B`, its "wrapped" version `B'` contains a copy of `B` and [Bubblewrap]: https://github.com/containers/bubblewrap [Environment variables]: #environment-variables [How it works]: #how-it-works +[How `BUILD_WRAP_CMD` is expanded]: #how-build_wrap_cmd-is-expanded +[`sandbox-exec`]: https://keith.github.io/xcode-man-pages/sandbox-exec.1.html [manner described above]: #how-build_wrap_cmd-is-expanded diff --git a/src/main.rs b/src/main.rs index a324035..d0fc194 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,13 +10,38 @@ use std::{ mod util; mod wrapper; -const DEFAULT_CMD: &str = "bwrap +const DEFAULT_CMD: &str = if cfg!(target_os = "linux") { + "bwrap --ro-bind / / --dev-bind /dev /dev --bind {OUT_DIR} {OUT_DIR} --bind /tmp /tmp --unshare-net - {}"; + {}" +} else { + // smoelius: The following blog post is a useful `sandbox-exec` reference: + // https://7402.org/blog/2020/macos-sandboxing-of-folder.html + // smoelius: The following package does not build with the current `sandbox-exec` command: + // https://crates.io/crates/psm + // Adding the following line fixes the problem: + // ``` + // (allow\ file-write*\ (subpath\ "/private{TMPDIR}"))\ + // ``` + // However, I don't want to pollute the command for this one package. This issue requires + // further investigation. + r#"sandbox-exec -p +(version\ 1)\ +(deny\ default)\ +(allow\ file-read*)\ +(allow\ file-write*\ (subpath\ "/dev"))\ +(allow\ file-write*\ (subpath\ "{OUT_DIR}"))\ +(allow\ file-write*\ (subpath\ "{TMPDIR}"))\ +(allow\ process-exec)\ +(allow\ process-fork)\ +(allow\ sysctl-read)\ +(deny\ network*) +{}"# +}; fn main() -> Result<()> { let args: Vec = args().collect(); diff --git a/tests/cases/outside_out_dir.stderr b/tests/cases/outside_out_dir.linux.stderr similarity index 100% rename from tests/cases/outside_out_dir.stderr rename to tests/cases/outside_out_dir.linux.stderr diff --git a/tests/cases/outside_out_dir.macos.stderr b/tests/cases/outside_out_dir.macos.stderr new file mode 100644 index 0000000..a2a9fa5 --- /dev/null +++ b/tests/cases/outside_out_dir.macos.stderr @@ -0,0 +1 @@ +message: "Operation not permitted" diff --git a/tests/cases/ping.stderr b/tests/cases/ping.linux.stderr similarity index 100% rename from tests/cases/ping.stderr rename to tests/cases/ping.linux.stderr diff --git a/tests/cases/ping.macos.stderr b/tests/cases/ping.macos.stderr new file mode 100644 index 0000000..b06ee12 --- /dev/null +++ b/tests/cases/ping.macos.stderr @@ -0,0 +1 @@ +ping: sendto: Operation not permitted diff --git a/tests/cases/tiocsti.stderr b/tests/cases/tiocsti.linux.stderr similarity index 100% rename from tests/cases/tiocsti.stderr rename to tests/cases/tiocsti.linux.stderr diff --git a/tests/cases/tiocsti.macos.stderr b/tests/cases/tiocsti.macos.stderr new file mode 100644 index 0000000..e5f6235 --- /dev/null +++ b/tests/cases/tiocsti.macos.stderr @@ -0,0 +1 @@ +libc::ioctl: Operation not permitted diff --git a/tests/custom_build_name.rs b/tests/custom_build_name.rs index f1dd449..f2d7c5f 100644 --- a/tests/custom_build_name.rs +++ b/tests/custom_build_name.rs @@ -20,8 +20,13 @@ fn custom_build_name() { assert!(!output.status.success()); let stderr = std::str::from_utf8(&output.stderr).unwrap(); + let syscall = if cfg!(target_os = "linux") { + "socket" + } else { + "sendto" + }; assert!( - stderr.contains("ping: socket: Operation not permitted"), + stderr.contains(&format!("ping: {syscall}: Operation not permitted")), "stderr does not contain expected string:\n```\n{stderr}\n```", ); } diff --git a/tests/integration.rs b/tests/integration.rs index 3362396..01d0e71 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -36,7 +36,14 @@ fn integration() { } fn test_case(path: &Path) { - let stderr_path = path.with_extension("stderr"); + let mut stderr_path = path.with_extension("stderr"); + if !stderr_path.exists() { + stderr_path = if cfg!(target_os = "linux") { + path.with_extension("linux.stderr") + } else { + path.with_extension("macos.stderr") + } + } let expected_stderr_substring = read_to_string(stderr_path).unwrap(); let temp_package = util::temp_package(path).unwrap();