diff --git a/.bazelrc b/.bazelrc index c269e5b31..9c298bddd 100644 --- a/.bazelrc +++ b/.bazelrc @@ -35,3 +35,15 @@ common:ci --nobuild_runfile_links # Override the preset's `--lockfile_mode=error` on CI since we don't # yet commit MODULE.bazel.lock — see .gitignore. common:ci --lockfile_mode=off + +# Run everything under Bazel's hermetic Linux sandbox, which mounts only the declared +# inputs of an action. Each mount below is necessary, primarily for things like shebang +# lines in shell scripts and libraries for dynamically linked node binaries. +build:hermetic-sandbox --strategy=linux-sandbox +build:hermetic-sandbox --experimental_use_hermetic_linux_sandbox +build:hermetic-sandbox --sandbox_add_mount_pair=/bin +build:hermetic-sandbox --sandbox_add_mount_pair=/usr +build:hermetic-sandbox --sandbox_add_mount_pair=/lib +build:hermetic-sandbox --sandbox_add_mount_pair=/lib64 +build:hermetic-sandbox --sandbox_add_mount_pair=/etc +build:hermetic-sandbox --sandbox_add_mount_pair=/proc diff --git a/.github/workflows/ci-workflows.yaml b/.github/workflows/ci-workflows.yaml index 17ba61720..ff1348a10 100644 --- a/.github/workflows/ci-workflows.yaml +++ b/.github/workflows/ci-workflows.yaml @@ -266,6 +266,28 @@ jobs: echo "No test.sh in ${PWD}; skipping" fi + # Bazel's hermetic Linux sandbox mounts only an action's declared inputs, so it catches + # host leakage that the ordinary sandbox hides. This run needs its own job so that it does + # not share an action cache with other test runs. Otherwise we could end up reusing cached + # results from there instead of exercising the hermetic sandbox. We also set the necessary + # flags to ensure we do not use the local disk cache or the remote cache. + hermetic-sandbox: + # More recent versions of Ubuntu set kernel.apparmor_restrict_unprivileged_userns=1, + # which prevents linux-sandbox from working. + runs-on: ubuntu-22.04 + permissions: + contents: read + id-token: write + env: + USE_BAZEL_VERSION: 8.x + steps: + - uses: actions/checkout@v6 + - uses: aspect-build/setup-aspect@c22a8f64fb38f82f59ce809cd7eb9f8ae096da44 # v2026.23.2 + with: + aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }} + - name: Test + run: aspect test --task-key=hermetic-sandbox --bazel-flag=--test_tag_filters=-skip-on-bazel8 --bazel-flag=--config=hermetic-sandbox --bazel-flag=--noremote_accept_cached --bazel-flag=--noremote_upload_local_results --bazel-flag=--disk_cache= --bazel-flag=--nocache_test_results -- //... + # Mac/Windows smoke tests for the root workspace and e2e/bzlmod. # Only run on main branch (not PRs) to minimize minutes (billed at 10X and 2X respectively) # unless the branch name contains 'macos' or 'windows'. @@ -309,7 +331,7 @@ jobs: # For branch protection settings, this job provides a "stable" name that can be used to gate PR merges # on "all matrix jobs were successful". conclusion: - needs: [format, buildifier, test, smoke] + needs: [format, buildifier, test, hermetic-sandbox, smoke] runs-on: ubuntu-latest if: always() steps: @@ -317,6 +339,7 @@ jobs: if [[ "${{ needs.format.result }}" == "success" \ && "${{ needs.buildifier.result }}" == "success" \ && "${{ needs.test.result }}" == "success" \ + && "${{ needs.hermetic-sandbox.result }}" == "success" \ && ("${{ needs.smoke.result }}" == "success" || "${{ needs.smoke.result }}" == "skipped") ]]; then exit 0 else diff --git a/js/private/js_image_layer.mjs b/js/private/js_image_layer.mjs index 4998223bf..566081e1a 100644 --- a/js/private/js_image_layer.mjs +++ b/js/private/js_image_layer.mjs @@ -143,7 +143,14 @@ function _mtree_file_line(key, content) { const dest = vis(key) // Due to filesystems setting different bits depending on the os we have to opt-in // to use a stable mode for files. - return `${dest} uid={{UID}} gid={{GID}} time=0 mode={{FILE_MODE}} type=file content=${vis( + // + // nlink=1 keeps the archive independent of how the sandbox materializes our inputs. + // Two entries may share a `content` file (the js_binary launcher appears both as the + // image entrypoint and inside the runfiles tree); bsdtar collapses such a pair into a + // hard link, but only when the source file's st_nlink > 1. The standard linux sandbox + // symlinks inputs (nlink 1) while the hermetic one hard links them (nlink 2), so + // without this the tar bytes would differ by spawn strategy. + return `${dest} uid={{UID}} gid={{GID}} time=0 mode={{FILE_MODE}} nlink=1 type=file content=${vis( content )}` }