Skip to content

Commit fa3ee21

Browse files
authored
fix: stage Chromium runtime libraries so they survive chroot shadowing
1 parent b329fee commit fa3ee21

6 files changed

Lines changed: 102 additions & 9 deletions

File tree

containers/agent/entrypoint.sh

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1560,6 +1560,7 @@ run_chroot_command() {
15601560
mount_host_cgroupfs
15611561
check_chroot_prereqs
15621562
copy_preload_libs
1563+
copy_browser_libs
15631564
copy_agent_helper_scripts
15641565
copy_dind_runner_binary
15651566
ensure_usr_local_bin_shims
@@ -1625,8 +1626,8 @@ run_chroot_command() {
16251626
CLEANUP_CMD="${CLEANUP_CMD}; sed -i '/^[0-9.]\\+[[:space:]]\\+host\\.docker\\.internal\$/d' /etc/hosts 2>/dev/null || true"
16261627
echo "[entrypoint] host.docker.internal will be removed from /etc/hosts on exit"
16271628
fi
1628-
# Clean up /run/awf-lib if anything was copied (one-shot-token, CA cert, key helper)
1629-
if [ -n "${ONE_SHOT_TOKEN_LIB}" ] || [ -n "${AWF_CA_CHROOT}" ] || [ -n "${SYSTEM_CA_CHROOT}" ] || [ -n "${CHROOT_KEY_HELPER}" ] || [ -n "${STAGED_RUNNER_BINARY_CHROOT}" ]; then
1629+
# Clean up /run/awf-lib if anything was copied (one-shot-token, CA cert, key helper, browser libs)
1630+
if [ -n "${ONE_SHOT_TOKEN_LIB}" ] || [ -n "${AWF_CA_CHROOT}" ] || [ -n "${SYSTEM_CA_CHROOT}" ] || [ -n "${CHROOT_KEY_HELPER}" ] || [ -n "${STAGED_RUNNER_BINARY_CHROOT}" ] || [ -n "${BROWSER_LD_LIBRARY_PATH}" ]; then
16301631
CLEANUP_CMD="${CLEANUP_CMD}; rm -rf /run/awf-lib 2>/dev/null || true"
16311632
fi
16321633
# NOTE: the /usr/local/bin overlay is torn down by cleanup_usr_local_bin_overlay(),
@@ -1682,10 +1683,18 @@ run_chroot_command() {
16821683
LD_PRELOAD_CMD="export LD_PRELOAD=${ONE_SHOT_TOKEN_LIB};"
16831684
fi
16841685

1686+
# Build LD_LIBRARY_PATH command so Chromium/Playwright can resolve the
1687+
# staged browser runtime libraries (see copy_browser_libs above).
1688+
LD_LIBRARY_PATH_CMD=""
1689+
if [ -n "${BROWSER_LD_LIBRARY_PATH}" ]; then
1690+
LD_LIBRARY_PATH_CMD="export LD_LIBRARY_PATH=${BROWSER_LD_LIBRARY_PATH}\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH};"
1691+
fi
1692+
16851693
run_command_with_stdout run_agent_with_token_protection chroot /host /bin/bash -c "
16861694
cd '${CHROOT_WORKDIR}' 2>/dev/null || cd / 2>/dev/null || true
16871695
trap '${CLEANUP_CMD}' EXIT
16881696
${LD_PRELOAD_CMD}
1697+
${LD_LIBRARY_PATH_CMD}
16891698
exec capsh --drop=${CAPS_TO_DROP} ${CAPSH_IDENTITY_ARGS} -- -c 'exec ${SCRIPT_FILE}'
16901699
"
16911700
}

docs-site/src/content/docs/reference/agent-images.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ The default `agent` image (based on Ubuntu 22.04) includes the following pre-ins
7979
| Chromium runtime libraries || see below | Shared libraries required by Playwright-managed browsers |
8080

8181
:::note[Chromium/Playwright runtime libraries]
82-
Because the agent uses selective bind mounts instead of a full host filesystem mount, browsers downloaded by Playwright cannot rely on host libraries. The image therefore preinstalls `libasound2`, `libatk-bridge2.0-0`, `libatk1.0-0`, `libatspi2.0-0`, `libcairo2`, `libcups2`, `libdbus-1-3`, `libdrm2`, `libexpat1`, `libgbm1`, `libglib2.0-0`, `libnspr4`, `libnss3`, `libpango-1.0-0`, `libpangocairo-1.0-0`, `libx11-6`, `libxcb1`, `libxcomposite1`, `libxdamage1`, `libxext6`, `libxfixes3`, `libxkbcommon0`, `libxrandr2`, `libxrender1`, `libxshmfence1`, and `fonts-liberation` (plus their transitive dependencies). On Ubuntu 24.04 base images the `t64` variants of these packages are installed automatically.
82+
Because the agent uses selective bind mounts instead of a full host filesystem mount, browsers downloaded by Playwright cannot rely on host libraries. The image therefore preinstalls `libasound2`, `libatk-bridge2.0-0`, `libatk1.0-0`, `libatspi2.0-0`, `libcairo2`, `libcups2`, `libdbus-1-3`, `libdrm2`, `libexpat1`, `libgbm1`, `libglib2.0-0`, `libnspr4`, `libnss3`, `libpango-1.0-0`, `libpangocairo-1.0-0`, `libx11-6`, `libxcb1`, `libxcomposite1`, `libxdamage1`, `libxext6`, `libxfixes3`, `libxkbcommon0`, `libxrandr2`, `libxrender1`, `libxshmfence1`, and `fonts-liberation` (plus their transitive dependencies). On Ubuntu 24.04 base images the `t64` variants of these packages are installed automatically. In chroot mode (the default), these image-installed libraries would otherwise be shadowed by the host's own `/usr`, `/lib`, etc.; `entrypoint.sh` stages copies of the files under `/run/awf-lib/browser-libs` (on the container's own writable rootfs, unaffected by the chroot bind mounts) and sets `LD_LIBRARY_PATH` so Chromium resolves them there.
8383
:::
8484

8585
:::caution[Docker CLI Stub]

docs/agent-images.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ The `default` preset image (based on Ubuntu 22.04) includes the following pre-in
7373
| ca-certificates || `ca-certificates` | Trusted root certificates |
7474
| Chromium runtime libraries || see below | Shared libraries required by Playwright-managed browsers |
7575

76-
**Chromium/Playwright runtime libraries:** Because the agent uses selective bind mounts instead of a full host filesystem mount, browsers downloaded by Playwright cannot rely on host libraries. The image therefore preinstalls `libasound2`, `libatk-bridge2.0-0`, `libatk1.0-0`, `libatspi2.0-0`, `libcairo2`, `libcups2`, `libdbus-1-3`, `libdrm2`, `libexpat1`, `libgbm1`, `libglib2.0-0`, `libnspr4`, `libnss3`, `libpango-1.0-0`, `libpangocairo-1.0-0`, `libx11-6`, `libxcb1`, `libxcomposite1`, `libxdamage1`, `libxext6`, `libxfixes3`, `libxkbcommon0`, `libxrandr2`, `libxrender1`, `libxshmfence1`, and `fonts-liberation` (plus their transitive dependencies). On Ubuntu 24.04 base images the `t64` variants of these packages are installed automatically.
76+
**Chromium/Playwright runtime libraries:** Because the agent uses selective bind mounts instead of a full host filesystem mount, browsers downloaded by Playwright cannot rely on host libraries. The image therefore preinstalls `libasound2`, `libatk-bridge2.0-0`, `libatk1.0-0`, `libatspi2.0-0`, `libcairo2`, `libcups2`, `libdbus-1-3`, `libdrm2`, `libexpat1`, `libgbm1`, `libglib2.0-0`, `libnspr4`, `libnss3`, `libpango-1.0-0`, `libpangocairo-1.0-0`, `libx11-6`, `libxcb1`, `libxcomposite1`, `libxdamage1`, `libxext6`, `libxfixes3`, `libxkbcommon0`, `libxrandr2`, `libxrender1`, `libxshmfence1`, and `fonts-liberation` (plus their transitive dependencies). On Ubuntu 24.04 base images the `t64` variants of these packages are installed automatically. In chroot mode (the default), these image-installed libraries would otherwise be shadowed by the host's own `/usr`, `/lib`, etc.; `entrypoint.sh` stages copies of the files under `/run/awf-lib/browser-libs` (on the container's own writable rootfs, unaffected by the chroot bind mounts) and sets `LD_LIBRARY_PATH` so Chromium resolves them there.
7777

7878
**⚠️ Docker CLI Stub:** The `docker` command is present but is a stub—there is no Docker daemon running inside the container. Docker-in-Docker is not supported. Use `--mount` to access Docker sockets from the host if needed.
7979

docs/troubleshooting.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ Host system is missing dependencies to run browsers.
377377

378378
**Cause:** The agent container uses selective bind mounts rather than a full host filesystem mount, so browsers downloaded by Playwright cannot pick up native libraries installed on the host.
379379

380-
**Solution:** The agent image preinstalls Chromium's native runtime libraries (`libnspr4`, `libnss3`, `libatk1.0-0`, `libatk-bridge2.0-0`, `libatspi2.0-0`, `libcups2`, `libdrm2`, `libgbm1`, `libpango-1.0-0`, `libxkbcommon0`, `libxcomposite1`, `libxdamage1`, `libxrandr2`, `libasound2`, `fonts-liberation`, and related packages). Use a current agent image (`--image-tag latest`, or `--build-local` when building from source) and the browser will launch.
380+
**Solution:** The agent image preinstalls Chromium's native runtime libraries (`libnspr4`, `libnss3`, `libatk1.0-0`, `libatk-bridge2.0-0`, `libatspi2.0-0`, `libcups2`, `libdrm2`, `libgbm1`, `libpango-1.0-0`, `libxkbcommon0`, `libxcomposite1`, `libxdamage1`, `libxrandr2`, `libasound2`, `fonts-liberation`, and related packages). In chroot mode (the default), `entrypoint.sh` stages copies of these libraries under `/run/awf-lib/browser-libs` — on the container's own writable rootfs, so they are not shadowed by the host `/usr`/`/lib` bind mounts — and sets `LD_LIBRARY_PATH` so Chromium resolves them there. Use a current agent image (`--image-tag latest`, or `--build-local` when building from source) and the browser will launch.
381381

382382
Verify the libraries are present inside the sandbox:
383383

scripts/ci/agent-dockerfile-browser-deps.test.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ apt-cache() {
3737
done
3838
return 100
3939
}
40-
PKGS="iptables"
40+
BROWSER_ACTUAL_PKGS=""
4141
BROWSER_PKGS="${readBrowserPackages().join(' ')}"
4242
${readResolutionScript()}
43-
echo "$PKGS"
43+
echo "$BROWSER_ACTUAL_PKGS"
4444
`;
4545

4646
const result = spawnSync('bash', ['-c', script], { encoding: 'utf-8' });
@@ -85,7 +85,7 @@ describe('agent Dockerfile Chromium runtime dependencies', () => {
8585
it('keeps the 22.04 names when the base image has no t64 packages', () => {
8686
const resolved = runResolution([]);
8787

88-
expect(resolved.split(/\s+/)).toEqual(['iptables', ...readBrowserPackages()]);
88+
expect(resolved.split(/\s+/).filter(Boolean)).toEqual(readBrowserPackages());
8989
});
9090

9191
it('prefers t64 packages when the base image provides them', () => {
@@ -98,7 +98,14 @@ describe('agent Dockerfile Chromium runtime dependencies', () => {
9898
expect(resolved).toContain('libnss3');
9999
});
100100

101-
it('installs the resolved browser packages with the other agent packages', () => {
101+
it('installs the resolved browser packages separately so their transitive deps can be diffed', () => {
102102
expect(readDockerfile()).toMatch(/apt_install_retry \$PKGS/);
103+
expect(readDockerfile()).toMatch(/apt_install_retry \$BROWSER_ACTUAL_PKGS/);
104+
});
105+
106+
it('records the installed .so files into a manifest entrypoint.sh stages for the chroot', () => {
107+
const dockerfile = readDockerfile();
108+
expect(dockerfile).toMatch(/\/usr\/local\/share\/awf\/browser-libs\.manifest/);
109+
expect(dockerfile).toMatch(/dpkg -L \$\(cat \/tmp\/browser-pkgs-installed\.txt\)/);
103110
});
104111
});

tests/entrypoint-phase-functions.test.sh

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ required_functions=(
3030
mount_host_procfs
3131
mount_host_cgroupfs
3232
copy_preload_libs
33+
copy_browser_libs
3334
copy_agent_helper_scripts
3435
copy_dind_runner_binary
3536
resolve_chroot_binary_path
@@ -122,6 +123,7 @@ chroot_helpers=(
122123
'mount_host_cgroupfs'
123124
'check_chroot_prereqs'
124125
'copy_preload_libs'
126+
'copy_browser_libs'
125127
'copy_agent_helper_scripts'
126128
'copy_dind_runner_binary'
127129
'ensure_usr_local_bin_shims'
@@ -222,6 +224,81 @@ else
222224
fail "run_chroot_command() does not clean up copied system CA bundles"
223225
fi
224226

227+
# copy_browser_libs() must stage the Chromium/Playwright runtime libraries
228+
# recorded in the Dockerfile's manifest (containers/agent/Dockerfile:
229+
# /usr/local/share/awf/browser-libs.manifest) under /run/awf-lib/browser-libs
230+
# -- on the container's own writable rootfs, not the /host bind mount -- and
231+
# export their directories via BROWSER_LD_LIBRARY_PATH. This directly
232+
# exercises the staging mechanism that makes the packages installed by
233+
# BROWSER_PKGS visible inside the chroot, where buildSystemMounts() otherwise
234+
# shadows the image's /usr, /lib, etc. with the runner's own.
235+
run_copy_browser_libs_fixture() {
236+
local tmp_dir
237+
tmp_dir="$(mktemp -d)"
238+
local host_root="${tmp_dir}/host-root"
239+
local fixture_entrypoint="${tmp_dir}/entrypoint-fixture.sh"
240+
local fake_lib_dir="${tmp_dir}/image-rootfs/usr/lib/x86_64-linux-gnu"
241+
local fake_lib="${fake_lib_dir}/libnspr4.so"
242+
local manifest="${tmp_dir}/browser-libs.manifest"
243+
244+
mkdir -p "${host_root}" "${fake_lib_dir}"
245+
printf 'real-libnspr4-bytes' > "${fake_lib}"
246+
printf '%s\n' "${fake_lib}" > "${manifest}"
247+
248+
awk '$0 != "main \"$@\""' "${ENTRYPOINT}" > "${fixture_entrypoint}"
249+
sed -i "s#/host#\${AWF_TEST_HOST_ROOT}#g" "${fixture_entrypoint}"
250+
sed -i "s#/usr/local/share/awf/browser-libs.manifest#\${AWF_TEST_MANIFEST}#g" "${fixture_entrypoint}"
251+
252+
(
253+
set -e
254+
# shellcheck disable=SC1090
255+
. "${fixture_entrypoint}"
256+
257+
AWF_TEST_HOST_ROOT="${host_root}"
258+
AWF_TEST_MANIFEST="${manifest}"
259+
260+
unset BROWSER_LD_LIBRARY_PATH
261+
copy_browser_libs
262+
263+
staged_lib="${host_root}/run/awf-lib/browser-libs${fake_lib}"
264+
[ -f "${staged_lib}" ]
265+
[ "$(cat "${staged_lib}")" = "real-libnspr4-bytes" ]
266+
[ "${BROWSER_LD_LIBRARY_PATH}" = "/run/awf-lib/browser-libs${fake_lib_dir}" ]
267+
268+
# Simulate buildSystemMounts() shadowing the image's own /usr/lib with the
269+
# runner's: overwrite the original library path with different content
270+
# (as if the host's own libnspr4.so.0 had been bind-mounted over it).
271+
printf 'shadowed-by-host-bind-mount' > "${fake_lib}"
272+
273+
# The staged copy under /run/awf-lib must be unaffected by the shadowing,
274+
# proving Chromium resolves the image-provided library via
275+
# LD_LIBRARY_PATH rather than being silently broken by the chroot bind mount.
276+
[ "$(cat "${staged_lib}")" = "real-libnspr4-bytes" ]
277+
)
278+
local result=$?
279+
rm -rf "${tmp_dir}"
280+
return "${result}"
281+
}
282+
283+
if run_copy_browser_libs_fixture; then
284+
pass "copy_browser_libs() stages manifest libraries under /run/awf-lib and survives /usr shadowing"
285+
else
286+
fail "copy_browser_libs() does not stage Chromium/Playwright runtime libraries correctly"
287+
fi
288+
289+
if grep -Eq '\[ -n "\$\{BROWSER_LD_LIBRARY_PATH\}" \]' "${ENTRYPOINT}"; then
290+
pass "run_chroot_command() cleans up staged browser runtime libraries"
291+
else
292+
fail "run_chroot_command() does not clean up staged browser runtime libraries"
293+
fi
294+
295+
if grep -Fq 'LD_LIBRARY_PATH_CMD' "${ENTRYPOINT}" && \
296+
grep -Fq 'export LD_LIBRARY_PATH=${BROWSER_LD_LIBRARY_PATH}' "${ENTRYPOINT}"; then
297+
pass "run_chroot_command() exports LD_LIBRARY_PATH from BROWSER_LD_LIBRARY_PATH before exec"
298+
else
299+
fail "run_chroot_command() does not export LD_LIBRARY_PATH from BROWSER_LD_LIBRARY_PATH"
300+
fi
301+
225302
# configure_jvm_proxy must not abort the entrypoint (set -e) when $HOME is
226303
# read-only, including when .m2/.gradle already exist but cannot be written.
227304
run_configure_jvm_proxy_readonly_home_fixture() {

0 commit comments

Comments
 (0)