Skip to content

Commit 4597245

Browse files
authored
fix(crosscheck): support credentialless private PR evidence (#326)
* fix(crosscheck): support private repository evidence * no-mistakes(review): Verify private bundle ancestors without network access * no-mistakes(document): Document private Azure Crosscheck snapshot behavior * no-mistakes(review): Remove brittle assertion and repair ancestor fixture * no-mistakes(document): Document private Crosscheck bundle transport
1 parent c70044e commit 4597245

8 files changed

Lines changed: 454 additions & 104 deletions

bin/fm-azure-runner-exec.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121

2222
RESULT_SCHEMA = "fm.azure-command-result/v1"
23+
PRIVATE_SOURCE_MODES = ("private-parent-bundle", "private-exact-bundle")
2324

2425

2526
def fail(message):
@@ -73,6 +74,35 @@ def verify_request(request):
7374
return argv, limits
7475

7576

77+
def verify_private_source_ancestors(request_path, repo):
78+
try:
79+
request = json.loads(request_path.read_text(encoding="utf-8"))
80+
repository = request["repository"]
81+
if repository.get("source_mode") not in PRIVATE_SOURCE_MODES:
82+
raise ValueError("private source ancestor verification requires a private bundle")
83+
commit = repository["commit"]
84+
ancestors = repository.get("source_ancestors", [])
85+
for ancestor in ancestors:
86+
object_type = subprocess.run(
87+
["git", "-C", str(repo), "cat-file", "-t", ancestor],
88+
check=True,
89+
text=True,
90+
stdout=subprocess.PIPE,
91+
stderr=subprocess.PIPE,
92+
).stdout.strip()
93+
if object_type != "commit":
94+
raise ValueError("source ancestor is not a commit")
95+
subprocess.run(
96+
["git", "-C", str(repo), "merge-base", "--is-ancestor", ancestor, commit],
97+
check=True,
98+
stdout=subprocess.DEVNULL,
99+
stderr=subprocess.PIPE,
100+
)
101+
except (KeyError, OSError, ValueError, json.JSONDecodeError, subprocess.CalledProcessError) as exc:
102+
return fail("private source ancestor verification failed: {}".format(exc))
103+
return 0
104+
105+
76106
def drop_privileges(uid, gid, pid_max, disk_bytes):
77107
if os.environ.get("FM_AZURE_RUNNER_TEST_NO_DROP") == "1":
78108
if uid != os.getuid() or gid != os.getgid():
@@ -159,6 +189,8 @@ def __exit__(self, exc_type, exc, traceback):
159189

160190

161191
def main():
192+
if len(sys.argv) == 4 and sys.argv[1] == "--verify-private-source-ancestors":
193+
return verify_private_source_ancestors(Path(sys.argv[2]), Path(sys.argv[3]))
162194
if len(sys.argv) != 8:
163195
return fail("expected request, repo, output, uid, gid, VM id, and boot id")
164196
request_path = Path(sys.argv[1])

bin/fm-azure-runner-guest.sh

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ if supplied != "sha256:" + hashlib.sha256(canonical).hexdigest(): raise SystemEx
9494
if "sha256:" + hashlib.sha256(executor_path.read_bytes()).hexdigest() != request["protocol"]["executor_digest"]: raise SystemExit("guest bootstrap: executor digest mismatch")
9595
if request["protocol"]["guest_digest"] != sys.argv[3]: raise SystemExit("guest bootstrap: guest digest mismatch")
9696
repo = request["repository"]
97-
if repo.get("source_mode") not in ("public-github-https", "private-parent-bundle") or not repo.get("remote", "").startswith("https://github.com/"): raise SystemExit("guest bootstrap: source mode mismatch")
98-
if repo.get("source_mode") == "private-parent-bundle":
97+
if repo.get("source_mode") not in ("public-github-https", "private-parent-bundle", "private-exact-bundle") or not repo.get("remote", "").startswith("https://github.com/"): raise SystemExit("guest bootstrap: source mode mismatch")
98+
if repo.get("source_mode") in ("private-parent-bundle", "private-exact-bundle"):
9999
if not repo.get("input_blob") or not repo.get("snapshot_digest") or not repo.get("snapshot_bytes"): raise SystemExit("guest bootstrap: private snapshot binding is incomplete")
100100
else:
101101
if repo.get("input_blob") is not None or repo.get("snapshot_bytes") != 0: raise SystemExit("guest bootstrap: public source carries private staging")
@@ -145,7 +145,7 @@ runuser -u fmrunner -- git -C /work/repo remote add origin "$REMOTE"
145145
# repository as dubious (CVE-2022-24765); scope the exception through the
146146
# environment exactly as the validation cell guest does.
147147
export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0=/work/repo
148-
if [ "$SOURCE_MODE" = private-parent-bundle ]; then
148+
if [ "$SOURCE_MODE" = private-parent-bundle ] || [ "$SOURCE_MODE" = private-exact-bundle ]; then
149149
[ "$INPUT_BLOB" = "$(read_request repository.input_blob)" ] || { echo "guest bootstrap: private snapshot blob mismatch" >&2; exit 125; }
150150
SNAPSHOT=$BASE/snapshot.bundle
151151
TOKEN_FILE=$BASE/input-token
@@ -184,10 +184,15 @@ for value in json.load(open(sys.argv[1],encoding="utf-8"))["repository"].get("so
184184
PY
185185
while IFS= read -r ancestor; do
186186
[ -n "$ancestor" ] || continue
187-
run_bootstrap_network runuser -u fmrunner -- git -C /work/repo fetch --depth=1 origin "$ancestor"
188-
[ "$(git -C /work/repo rev-parse FETCH_HEAD)" = "$ancestor" ] || { echo "guest bootstrap: source ancestor identity mismatch" >&2; exit 125; }
189-
git -C /work/repo cat-file -e "$ancestor^{commit}" || { echo "guest bootstrap: source ancestor is absent" >&2; exit 125; }
187+
if [ "$SOURCE_MODE" = public-github-https ]; then
188+
run_bootstrap_network runuser -u fmrunner -- git -C /work/repo fetch --depth=1 origin "$ancestor"
189+
[ "$(git -C /work/repo rev-parse FETCH_HEAD)" = "$ancestor" ] || { echo "guest bootstrap: source ancestor identity mismatch" >&2; exit 125; }
190+
git -C /work/repo cat-file -e "$ancestor^{commit}" || { echo "guest bootstrap: source ancestor is absent" >&2; exit 125; }
191+
fi
190192
done <"$BASE/source-ancestors"
193+
if [ "$SOURCE_MODE" = private-parent-bundle ] || [ "$SOURCE_MODE" = private-exact-bundle ]; then
194+
/usr/bin/python3 "$EXECUTOR" --verify-private-source-ancestors "$REQUEST" /work/repo
195+
fi
191196
[ "$(git -C /work/repo rev-parse HEAD)" = "$COMMIT" ] && [ "$(git -C /work/repo rev-parse 'HEAD^{tree}')" = "$TREE" ] || { echo "guest bootstrap: source identity mismatch" >&2; exit 125; }
192197
# Repository tests compare the snapshot against the default branch through
193198
# the refs/remotes/origin view (generation 051 ground truth: a behavior
@@ -238,13 +243,19 @@ while IFS=$'\t' read -r url file bytes digest; do
238243
fetch_exact "$url" "/work/home/.fm-runner-tools/wheelhouse/$file" "$bytes" "$digest"
239244
done <"$BASE/wheels.tsv"
240245
chown -R fmrunner:fmrunner /work/home/.fm-runner-tools
241-
[ "sha256:$(sha256sum /work/repo/tools/agent-fleet/uv.lock | awk '{print $1}')" = "$(read_request protocol.agent_fleet_python.lock_digest)" ] || { echo "guest bootstrap: lock mismatch" >&2; exit 125; }
242-
# The run-command handler's download directory is root-only, so the
243-
# unprivileged uv invocations must not inherit it as their working
244-
# directory (uv's config discovery reads ./uv.toml and refuses on EACCES).
245-
cd /work/repo
246-
runuser -u fmrunner -- /work/home/.fm-runner-tools/uv/uv venv --python /usr/bin/python3 /work/repo/tools/agent-fleet/.venv >/dev/null
247-
runuser -u fmrunner -- env UV_OFFLINE=1 UV_NO_INDEX=1 /work/home/.fm-runner-tools/uv/uv pip install --python /work/repo/tools/agent-fleet/.venv/bin/python --offline --no-index --find-links /work/home/.fm-runner-tools/wheelhouse pytest ruff >/dev/null
246+
LOCK_DIGEST=$(read_request protocol.agent_fleet_python.lock_digest)
247+
if [ "$LOCK_DIGEST" != None ]; then
248+
[ "sha256:$(sha256sum /work/repo/tools/agent-fleet/uv.lock | awk '{print $1}')" = "$LOCK_DIGEST" ] || { echo "guest bootstrap: lock mismatch" >&2; exit 125; }
249+
# The run-command handler's download directory is root-only, so the
250+
# unprivileged uv invocations must not inherit it as their working
251+
# directory (uv's config discovery reads ./uv.toml and refuses on EACCES).
252+
cd /work/repo
253+
runuser -u fmrunner -- /work/home/.fm-runner-tools/uv/uv venv --python /usr/bin/python3 /work/repo/tools/agent-fleet/.venv >/dev/null
254+
runuser -u fmrunner -- env UV_OFFLINE=1 UV_NO_INDEX=1 /work/home/.fm-runner-tools/uv/uv pip install --python /work/repo/tools/agent-fleet/.venv/bin/python --offline --no-index --find-links /work/home/.fm-runner-tools/wheelhouse pytest ruff >/dev/null
255+
elif [ -s "$BASE/wheels.tsv" ]; then
256+
echo "guest bootstrap: unbound Python wheels" >&2
257+
exit 125
258+
fi
248259

249260
python3 - "$REQUEST" /work/repo <<'PY'
250261
import hashlib,json,pathlib,sys

0 commit comments

Comments
 (0)