Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
167 changes: 131 additions & 36 deletions .github/workflows/qwen-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2599,21 +2599,68 @@ jobs:
run: |-
set -uo pipefail
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 is
# the guard from qwen-code-pr-review.yml's checkout heal (#9220),
# backported per #9265: measured on main, the bare denylist let
# non-canonical spellings of the guarded roots through (/home/,
# /home/., //usr, /root/, /var/ all reached the rm).
# Strip trailing slashes on the RAW path, before anything reads it:
# `[ -L "$WS/" ]` and `[ ! -d "$WS/" ]` both resolve THROUGH a link
# and report its target, so one trailing slash hides the corruption
# the heal below exists to clear.
while [ "${WS%/}" != "$WS" ]; do WS="${WS%/}"; done
# The allowlist root is prepared BEFORE the heal, because it bounds
# what the heal may touch: canonical, slash-free and non-degenerate.
# An empty $RUNNER_WORKSPACE would turn every containment pattern
# below into the match-all "/*".
RWS="${RUNNER_WORKSPACE:?}"
RWS="$(realpath -m -- "$RWS" 2>/dev/null)" || { echo "::error::refusing to wipe: realpath unavailable, cannot canonicalize ${RUNNER_WORKSPACE}"; exit 1; }
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
# Heal a workspace a previous job replaced with a symlink (or any
# non-directory) BEFORE canonicalizing it. Afterwards the path
# resolves to the link's target, the allowlist refuses that, and the
# refusal removes nothing — so every later job on this runner dies
# here, permanently, on corruption that is itself inside the runner
# workspace and safe to unlink.
if [ -L "$WS" ] || [ ! -d "$WS" ]; then
# Judge the PARENT, canonicalized. The heal necessarily acts on a
# raw path, and a raw containment match is not enough: the kernel
# resolves intermediate components too, so `$RWS/link/sub` matches
# "$RWS"/* as a string while naming a file outside it. Resolving
# the parent — never $WS itself, which would resolve through the
# very link being removed — is what makes the unlink containable.
HEAL_PARENT="$(realpath -m -- "$(dirname -- "$WS")" 2>/dev/null)" || { echo "::error::refusing to heal: realpath unavailable, cannot canonicalize the parent of ${WS}"; exit 1; }
case "$HEAL_PARENT" in
"$RWS"|"$RWS"/*) ;;
*) echo "::error::refusing to heal workspace outside the runner workspace: ${WS} (parent: ${HEAL_PARENT}, runner workspace: ${RWS})"; exit 1 ;;
esac
# The incident this heal exists for leaves no other trace: say what
# was found, and where it pointed, before it is gone.
if [ -L "$WS" ]; then
# The target is bytes a PREVIOUS job chose — on this pool that
# job may have run a contributor's code — and the runner parses
# `::` at the start of any stdout line as a workflow command. A
# target of $'x\n::error::forged' would therefore forge an
# annotation. Keep untrusted bytes off the command line itself,
# strip the line breaks that could start a new one, and cap the
# length.
heal_target="$(readlink -- "$WS" 2>/dev/null || printf '%s' '<unreadable>')"
heal_target="$(printf '%s' "$heal_target" | tr -d '\r\n' | cut -c1-200)"
echo "::warning::healing workspace ${WS}: it was a symlink"
printf 'heal: %s pointed at %s\n' "$WS" "$heal_target"
else
echo "::warning::healing workspace ${WS}: it was not a directory"
fi
# `rm -f` on the RAW path removes the link itself and never
# follows it. Both legs fail closed: under `-e` a failure that is
# not the last command of an && list is swallowed, and a swallowed
# one here would leave the wipe running against a corrupt path.
rm -f -- "$WS" || { echo "::error::refusing to continue: could not remove ${WS}"; exit 1; }
mkdir -- "$WS" || { echo "::error::refusing to continue: could not recreate ${WS}"; exit 1; }
fi
# 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.
WS="$(realpath -m -- "$WS" 2>/dev/null)" || { echo "::error::refusing to wipe: realpath unavailable, cannot canonicalize ${GITHUB_WORKSPACE}"; exit 1; }
# Trailing slashes slip past the exact-match case arms below
# (`/home/` would pass the guard and reach the rm); realpath strips
# them too; keep the guard whole if the path reaches this point with
# trailing slashes.
while [ "${WS%/}" != "$WS" ]; do WS="${WS%/}"; done
case "$WS" in
..|../*|*/..|*/../*) echo "::error::refusing to wipe path containing '..': ${WS}"; exit 1 ;;
Expand All @@ -2623,19 +2670,7 @@ jobs:
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)" || { echo "::error::refusing to wipe: realpath unavailable, cannot canonicalize ${RUNNER_WORKSPACE}"; exit 1; }
# Mirror the WS strip before building the allowlist pattern; "/"
# 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
# runner workspace may be wiped.
case "$WS" in
"$RWS"/*) ;;
*) echo "::error::refusing to wipe workspace outside the runner workspace: ${WS} (runner workspace: ${RWS})"; exit 1 ;;
Expand Down Expand Up @@ -4811,27 +4846,87 @@ 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.
# Same guard as the pre-run wipe above, layer for layer: raw
# trailing-slash strip, RUNNER_WORKSPACE allowlist root, symlink
# heal, canonicalize, strip, denylist, allowlist. Both copies now
# carry the checkout-heal hardening (#9277) and its heal (#9480);
# this header is the in-code inventory a future convergence of the
# wipe copies will read, so it must not understate what is here.
# 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)" || { echo "::error::refusing to wipe: realpath unavailable, cannot canonicalize ${GITHUB_WORKSPACE}"; exit 1; }
# Strip trailing slashes on the RAW path, before anything reads it:
# `[ -L "$WS/" ]` and `[ ! -d "$WS/" ]` both resolve THROUGH a link
# and report its target, so one trailing slash hides the corruption
# the heal below exists to clear.
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
# The allowlist root is prepared BEFORE the heal, because it bounds
# what the heal may touch: canonical, slash-free and non-degenerate.
# An empty $RUNNER_WORKSPACE would turn every containment pattern
# below into the match-all "/*".
RWS="${RUNNER_WORKSPACE:?}"
RWS="$(realpath -m -- "$RWS" 2>/dev/null)" || { echo "::error::refusing to wipe: realpath unavailable, cannot canonicalize ${RUNNER_WORKSPACE}"; exit 1; }
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
# Heal a workspace a previous job replaced with a symlink (or any
# non-directory) BEFORE canonicalizing it. Afterwards the path
# resolves to the link's target, the allowlist refuses that, and the
# refusal removes nothing — so every later job on this runner dies
# here, permanently, on corruption that is itself inside the runner
# workspace and safe to unlink.
if [ -L "$WS" ] || [ ! -d "$WS" ]; then
# Judge the PARENT, canonicalized. The heal necessarily acts on a
# raw path, and a raw containment match is not enough: the kernel
# resolves intermediate components too, so `$RWS/link/sub` matches
# "$RWS"/* as a string while naming a file outside it. Resolving
# the parent — never $WS itself, which would resolve through the
# very link being removed — is what makes the unlink containable.
HEAL_PARENT="$(realpath -m -- "$(dirname -- "$WS")" 2>/dev/null)" || { echo "::error::refusing to heal: realpath unavailable, cannot canonicalize the parent of ${WS}"; exit 1; }
case "$HEAL_PARENT" in
"$RWS"|"$RWS"/*) ;;
*) echo "::error::refusing to heal workspace outside the runner workspace: ${WS} (parent: ${HEAL_PARENT}, runner workspace: ${RWS})"; exit 1 ;;
esac
# The incident this heal exists for leaves no other trace: say what
# was found, and where it pointed, before it is gone.
if [ -L "$WS" ]; then
# The target is bytes a PREVIOUS job chose — on this pool that
# job may have run a contributor's code — and the runner parses
# `::` at the start of any stdout line as a workflow command. A
# target of $'x\n::error::forged' would therefore forge an
# annotation. Keep untrusted bytes off the command line itself,
# strip the line breaks that could start a new one, and cap the
# length.
heal_target="$(readlink -- "$WS" 2>/dev/null || printf '%s' '<unreadable>')"
heal_target="$(printf '%s' "$heal_target" | tr -d '\r\n' | cut -c1-200)"
echo "::warning::healing workspace ${WS}: it was a symlink"
printf 'heal: %s pointed at %s\n' "$WS" "$heal_target"
else
echo "::warning::healing workspace ${WS}: it was not a directory"
fi
# `rm -f` on the RAW path removes the link itself and never
# follows it. Both legs fail closed: under `-e` a failure that is
# not the last command of an && list is swallowed, and a swallowed
# one here would leave the wipe running against a corrupt path.
rm -f -- "$WS" || { echo "::error::refusing to continue: could not remove ${WS}"; exit 1; }
mkdir -- "$WS" || { echo "::error::refusing to continue: could not recreate ${WS}"; exit 1; }
fi
# 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.
WS="$(realpath -m -- "$WS" 2>/dev/null)" || { echo "::error::refusing to wipe: realpath unavailable, cannot canonicalize ${GITHUB_WORKSPACE}"; exit 1; }
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.
case "$WS" in
"$RWS"/*) ;;
*) echo "::error::refusing to wipe workspace outside the runner workspace: ${WS} (runner workspace: ${RWS})"; exit 1 ;;
Expand Down
Loading
Loading