Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/scripts/ci-runner-routing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,6 @@ describe('serve-ab.yml runner routing', () => {
);
assert.ok(wipe, 'self-hosted reuse must not bleed one PR into the next');
assert.equal(wipe.if, "${{ runner.environment == 'self-hosted' }}");
assert.match(wipe.run, /find "\$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 1 -exec rm -rf/);
assert.match(wipe.run, /find "\$WS" -mindepth 1 -maxdepth 1 -exec rm -rf/);
});
});
63 changes: 62 additions & 1 deletion .github/workflows/qwen-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2574,10 +2574,50 @@ jobs:
WS="${GITHUB_WORKSPACE:?}"
# Refuse to run anywhere unexpected: a wipe pointed at the wrong
# path by a mangled env is far worse than a skipped wipe, and
# this job cannot proceed safely without it either way.
# this job cannot proceed safely without it either way. This is
# the guard from qwen-code-pr-review.yml's checkout heal (#9220),
# backported per #9265: measured on main, the bare denylist let
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
# non-canonical spellings of the guarded roots through (/home/,
# /home/., //usr, /root/, /var/ all reached the rm).
# Canonicalize before matching: the kernel resolves non-canonical
# spellings to the guarded roots (`/home/.` -> /home, `//usr` ->
# /usr), so a raw string match lets them slip past the case arms.
# `-m` is GNU-only — a BSD realpath exits 1 on it and the fallback
# silently keeps the raw spelling. Safe here (this pool is
# Linux-only), and off-GNU the strip loop, `..` refusal, and
# allowlist below are what still hold; the suite gates its GNU-only
# assertion on a host probe rather than assuming this line ran.
WS="$(realpath -m -- "$WS" 2>/dev/null || printf '%s' "$WS")"
# Trailing slashes slip past the exact-match case arms below
# (`/home/` would pass the guard and reach the rm); realpath strips
# them too, but this keeps the guard whole when realpath is absent.
while [ "${WS%/}" != "$WS" ]; do WS="${WS%/}"; done
case "$WS" in
..|../*|*/..|*/../*) echo "::error::refusing to wipe path containing '..': ${WS}"; exit 1 ;;
esac
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
case "$WS" in
/|/home|/root|/usr*|/etc*|/var|"") echo "::error::refusing to wipe suspicious workspace path: ${WS}"; exit 1 ;;
esac
# A denylist can only enumerate known roots — the allowlist closes
# every other one (/tmp, /opt, ...): only a directory inside the
# runner workspace may be wiped. RUNNER_WORKSPACE is set in every
# step env, and for container steps the runner translates it —
# together with GITHUB_WORKSPACE — to the container path, so the
# allowlist holds inside this job's container as well.
RWS="${RUNNER_WORKSPACE:?}"
RWS="$(realpath -m -- "$RWS" 2>/dev/null || printf '%s' "$RWS")"
# Mirror the WS strip: without realpath, a trailing slash would
# turn the allowlist pattern into "$RWS"//* and refuse the real
# workspace; "/" stripped empty would match every path instead.
while [ "${RWS%/}" != "$RWS" ]; do RWS="${RWS%/}"; done
if [ -z "$RWS" ]; then echo "::error::refusing to wipe: runner workspace resolved to /"; exit 1; fi
case "$RWS" in
..|../*|*/..|*/../*) echo "::error::refusing runner workspace path containing '..': ${RWS}"; exit 1 ;;
esac
case "$WS" in
"$RWS"/*) ;;
*) echo "::error::refusing to wipe workspace outside the runner workspace: ${WS} (runner workspace: ${RWS})"; exit 1 ;;
esac
# Contents, not the directory itself: the runner owns the mount
# point. Dotfiles included — a planted .npmrc or .git is exactly
# what this removes. `find -exec rm -rf` rather than a glob so the
Expand Down Expand Up @@ -3504,10 +3544,31 @@ jobs:
if: "always() && needs.authorize.outputs.verify_trust == 'external'"
run: |-
set -uo pipefail
# Same guard as the pre-run wipe above (canonicalize, strip
# trailing slashes, denylist, RUNNER_WORKSPACE allowlist) — this
# copy predates the checkout-heal hardening and never received it
# (#9265). See that step's comments for what each layer catches;
# the suite pins this copy's behavior on its own.
WS="${GITHUB_WORKSPACE:?}"
WS="$(realpath -m -- "$WS" 2>/dev/null || printf '%s' "$WS")"
while [ "${WS%/}" != "$WS" ]; do WS="${WS%/}"; done
case "$WS" in
..|../*|*/..|*/../*) echo "::error::refusing to wipe path containing '..': ${WS}"; exit 1 ;;
esac
case "$WS" in
/|/home|/root|/usr*|/etc*|/var|"") echo "::error::refusing to wipe suspicious workspace path: ${WS}"; exit 1 ;;
esac
RWS="${RUNNER_WORKSPACE:?}"
RWS="$(realpath -m -- "$RWS" 2>/dev/null || printf '%s' "$RWS")"
while [ "${RWS%/}" != "$RWS" ]; do RWS="${RWS%/}"; done
if [ -z "$RWS" ]; then echo "::error::refusing to wipe: runner workspace resolved to /"; exit 1; fi
case "$RWS" in
..|../*|*/..|*/../*) echo "::error::refusing runner workspace path containing '..': ${RWS}"; exit 1 ;;
esac
case "$WS" in
"$RWS"/*) ;;
*) echo "::error::refusing to wipe workspace outside the runner workspace: ${WS} (runner workspace: ${RWS})"; exit 1 ;;
esac
find "$WS" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
echo "Workspace wiped after external code (deny-by-default cleanup for the next pool job)." >> "$GITHUB_STEP_SUMMARY"

Expand Down
49 changes: 48 additions & 1 deletion .github/workflows/serve-ab.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,54 @@ jobs:
# could bleed into the builds and silently change the posted A/B
# diff. Hosted runners are ephemeral and never see this. After the
# ownership-restore step everything is user-owned, so no sudo.
find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
#
# Guard, ported from qwen-code-pr-review.yml's checkout heal
# (#9220, #9265): this step had none — under a mangled env even
# `/home` or an empty string reached the rm. A wipe pointed at the
# wrong path is far worse than a skipped wipe, so canonicalize,
# strip trailing slashes, denylist the known roots, and require
# the target to sit inside the runner workspace before any rm.
WS="${GITHUB_WORKSPACE:?}"
# Canonicalize before matching: the kernel resolves non-canonical
# spellings to the guarded roots (`/home/.` -> /home, `//usr` ->
# /usr), so a raw string match lets them slip past the case arms.
# `-m` is GNU-only — a BSD realpath exits 1 on it and the fallback
# silently keeps the raw spelling. Safe here (this pool is
# Linux-only), and off-GNU the strip loop, `..` refusal, and
# allowlist below are what still hold; the suite gates its GNU-only
# assertion on a host probe rather than assuming this line ran.
WS="$(realpath -m -- "$WS" 2>/dev/null || printf '%s' "$WS")"
# Trailing slashes slip past the exact-match case arms below
# (`/home/` would pass the guard and reach the rm); realpath strips
# them too, but this keeps the guard whole when realpath is absent.
while [ "${WS%/}" != "$WS" ]; do WS="${WS%/}"; done
case "$WS" in
..|../*|*/..|*/../*) echo "::error::refusing to wipe path containing '..': ${WS}"; exit 1 ;;
esac
case "$WS" in
/|/home|/root|/usr*|/etc*|/var|"") echo "::error::refusing to wipe suspicious workspace path: ${WS}"; exit 1 ;;
esac
# A denylist can only enumerate known roots — the allowlist closes
# every other one (/tmp, /opt, ...): only a directory inside the
# runner workspace may be wiped.
RWS="${RUNNER_WORKSPACE:?}"
RWS="$(realpath -m -- "$RWS" 2>/dev/null || printf '%s' "$RWS")"
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
# Mirror the WS strip: without realpath, a trailing slash would
# turn the allowlist pattern into "$RWS"//* and refuse the real
# workspace; "/" stripped empty would match every path instead.
while [ "${RWS%/}" != "$RWS" ]; do RWS="${RWS%/}"; done
if [ -z "$RWS" ]; then echo "::error::refusing to wipe: runner workspace resolved to /"; exit 1; fi
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
case "$RWS" in
..|../*|*/..|*/../*) echo "::error::refusing runner workspace path containing '..': ${RWS}"; exit 1 ;;
esac
case "$WS" in
"$RWS"/*) ;;
*) echo "::error::refusing to wipe workspace outside the runner workspace: ${WS} (runner workspace: ${RWS})"; exit 1 ;;
esac
# Bare on purpose: this job runs under `bash -eo pipefail`, so a
# wipe that cannot clear the workspace fails the job here instead
# of building both checkouts on top of the leftovers.
find "$WS" -mindepth 1 -maxdepth 1 -exec rm -rf {} +

- name: 'Checkout PR head'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
Expand Down
Loading
Loading