Skip to content

Commit c0460fe

Browse files
committed
Relocate reference binary out of workspace for enhanced isolation and symlink protection
- Move reference binary to a root-only directory (`/opt/programbench/reference`) and replace it with a symlink to prevent unauthorized modifications. - Harden `test.sh` by dropping symlinks that escape or dangle outside the workspace. - Ensure verifier image wipes relocated reference binaries for cleanroom guarantees. - Expand testing to cover relocation, verifier cleanup, and symlink drop logic.
1 parent c052393 commit c0460fe

4 files changed

Lines changed: 84 additions & 2 deletions

File tree

‎src/kotlinai/data/harbor/test.sh‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,25 @@ mkdir -p /logs/verifier
3333
cd "$WORKSPACE" || exit 1
3434

3535
# --- anti-cheat: drop any prebuilt binary and byte-identical gold copies ---
36+
# rm -f also removes a leftover ./executable symlink (the agent image exposes the
37+
# reference only through a symlink to a root-only path; the real bytes never sit
38+
# under /workspace, so nothing recoverable is dropped here).
3639
rm -f ./executable
40+
# Defense-in-depth: the artifact could still ship a crafted symlink (named
41+
# ./executable or anything else) that points outside /workspace or dangles. Since
42+
# docker cp copies links verbatim, resolving one during the rebuild could surface
43+
# out-of-workspace bytes. Drop every workspace symlink whose realpath escapes
44+
# /workspace (dangling links resolve to nothing and are dropped too). Compare
45+
# against the resolved realpath of /workspace so a symlinked mount point (e.g.
46+
# /var -> /private/var) doesn't misclassify a link that stays inside.
47+
ws_real="$(readlink -f "$WORKSPACE" 2>/dev/null || echo "$WORKSPACE")"
48+
while IFS= read -r -d '' link; do
49+
target="$(readlink -f "$link" 2>/dev/null || true)"
50+
case "$target" in
51+
"$ws_real"/*) : ;;
52+
*) rm -fv "$link" ;;
53+
esac
54+
done < <(find "$WORKSPACE" -type l -print0 2>/dev/null)
3755
if [ -s "$TESTS_DIR/clean_hashes.txt" ]; then
3856
pat="$(tr '\n' '|' < "$TESTS_DIR/clean_hashes.txt" | sed 's/|$//')"
3957
find "$WORKSPACE" -type f -exec sha256sum {} + 2>/dev/null \

‎src/kotlinai/data/templates/harbor_environment.Dockerfile.j2‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,24 @@ RUN sed -i 's#https://snapshot.ubuntu.com/ubuntu/[0-9TZ]*/#http://archive.ubuntu
2323
&& apt-get install -y --no-install-recommends file ripgrep xxd \
2424
&& rm -rf /var/lib/apt/lists/*
2525
{% endif %}
26+
# Reference isolation: the cleanroom bakes the execute-only reference binary at
27+
# /workspace/executable, but /workspace itself is agent-owned, so the agent can
28+
# `mv` the file (Unix rename permission is on the containing directory, not the
29+
# file) and Harbor would copy the renamed bytes out as the artifact. Relocate
30+
# the real bytes to a root-only directory outside /workspace and leave only a
31+
# symlink behind: the agent can rename or delete the *link*, never the bytes,
32+
# and `docker cp` (task.toml keeps artifacts=["/workspace"] with no exclusions)
33+
# copies the link, not its target. Runs as root before sudo is dropped and
34+
# before the agent takes ownership of /workspace. Conditional so exports without
35+
# a bundled reference remain a no-op.
36+
RUN if [ -f /workspace/executable ] && [ ! -L /workspace/executable ]; then \
37+
mkdir -p /opt/programbench/reference \
38+
&& cp -a /workspace/executable /opt/programbench/reference/executable \
39+
&& rm -f /workspace/executable \
40+
&& ln -s /opt/programbench/reference/executable /workspace/executable \
41+
&& chown -R root:root /opt/programbench/reference \
42+
&& chmod -R go-w /opt/programbench/reference ; \
43+
fi
2644
# The cleanroom image grants the non-root agent NOPASSWD sudo for apt-get, dpkg,
2745
# pip, npm, cargo and go — each an arbitrary-code-as-root path (e.g. an apt
2846
# Pre-Invoke hook), which would hand back the ability to read the execute-only

‎src/kotlinai/data/templates/harbor_verifier.Dockerfile.j2‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ ENV PATH="${KOTLIN_HOME}/bin:${PATH}"
2727
{% endif %}
2828
# The separate verifier must not inherit anything the agent left behind: wipe
2929
# /workspace at build time. Harbor then restores only the handed-off artifacts
30-
# (the agent's /workspace minus ./executable) on top before test.sh runs.
31-
RUN rm -rf /workspace && mkdir -p /workspace
30+
# (the agent's /workspace minus ./executable) on top before test.sh runs. Also
31+
# wipe /opt/programbench/reference (where the environment image relocated the
32+
# real reference binary) so the verifier image holds no recoverable copy either.
33+
RUN rm -rf /workspace /opt/programbench/reference && mkdir -p /workspace
3234
WORKDIR /workspace
3335
# In separate-verifier mode Harbor does not upload the tests dir (the verifier
3436
# runs with skip_tests_upload=True), so the image must own /tests: bake in

‎tests/test_harbor.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,50 @@ def test_agent_image_ships_inspection_tools_and_drops_sudo(tmp_path):
167167
assert "rm -f /etc/sudoers.d/agent-packages" in dockerfile
168168

169169

170+
def test_environment_image_relocates_reference_out_of_workspace(tmp_path):
171+
out = convert_instance(_calc_instance(), tmp_path, target_language="Kotlin")
172+
dockerfile = (out / "environment" / "Dockerfile").read_text()
173+
174+
# The real reference bytes are copied to a root-only dir outside /workspace,
175+
# the original is dropped, and only a symlink is left in its place.
176+
assert "cp -a /workspace/executable /opt/programbench/reference/executable" in dockerfile
177+
assert "rm -f /workspace/executable" in dockerfile
178+
assert "ln -s /opt/programbench/reference/executable /workspace/executable" in dockerfile
179+
# The relocated dir/file is root-owned and not group/other writable, so the
180+
# agent (who owns /workspace) can only rename/delete the symlink, not the bytes.
181+
assert "chown -R root:root /opt/programbench/reference" in dockerfile
182+
assert "chmod -R go-w /opt/programbench/reference" in dockerfile
183+
# Conditional so exports without a bundled reference stay a no-op.
184+
assert "if [ -f /workspace/executable ] && [ ! -L /workspace/executable ]; then" in dockerfile
185+
186+
# The relocation must run as root before sudo is dropped (otherwise the
187+
# chown/relocate could no longer be guaranteed to run privileged).
188+
assert dockerfile.index("cp -a /workspace/executable") < dockerfile.index(
189+
"rm -f /etc/sudoers.d/agent-packages"
190+
)
191+
192+
193+
def test_verifier_image_wipes_relocated_reference(tmp_path):
194+
out = convert_instance(_calc_instance(), tmp_path, target_language="Kotlin")
195+
verifier_dockerfile = (out / "tests" / "Dockerfile").read_text()
196+
197+
# The verifier wipes both /workspace and the relocated reference dir so no
198+
# recoverable copy of the reference survives on the verification side either.
199+
assert "rm -rf /workspace /opt/programbench/reference" in verifier_dockerfile
200+
201+
202+
def test_test_sh_drops_escaping_and_dangling_symlinks(tmp_path):
203+
test_sh = (harbor.HARBOR_DATA / "test.sh").read_text()
204+
205+
# Still drops a leftover ./executable (now possibly a symlink).
206+
assert "rm -f ./executable" in test_sh
207+
# Hardened cleanup: resolve every workspace symlink and drop any whose
208+
# realpath escapes /workspace (dangling links resolve to nothing => dropped).
209+
assert 'find "$WORKSPACE" -type l -print0' in test_sh
210+
assert 'target="$(readlink -f "$link" 2>/dev/null || true)"' in test_sh
211+
assert '"$ws_real"/*) : ;;' in test_sh
212+
213+
170214
def test_convert_instance_exports_root_agent_for_oracle_verification(tmp_path):
171215
out = convert_instance(_calc_instance(), tmp_path, agent_user="root")
172216

0 commit comments

Comments
 (0)