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
32 changes: 32 additions & 0 deletions bin/fm-azure-runner-exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@


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


def fail(message):
Expand Down Expand Up @@ -73,6 +74,35 @@ def verify_request(request):
return argv, limits


def verify_private_source_ancestors(request_path, repo):
try:
request = json.loads(request_path.read_text(encoding="utf-8"))
repository = request["repository"]
if repository.get("source_mode") not in PRIVATE_SOURCE_MODES:
raise ValueError("private source ancestor verification requires a private bundle")
commit = repository["commit"]
ancestors = repository.get("source_ancestors", [])
for ancestor in ancestors:
object_type = subprocess.run(
["git", "-C", str(repo), "cat-file", "-t", ancestor],
check=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
).stdout.strip()
if object_type != "commit":
raise ValueError("source ancestor is not a commit")
subprocess.run(
["git", "-C", str(repo), "merge-base", "--is-ancestor", ancestor, commit],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
except (KeyError, OSError, ValueError, json.JSONDecodeError, subprocess.CalledProcessError) as exc:
return fail("private source ancestor verification failed: {}".format(exc))
return 0


def drop_privileges(uid, gid, pid_max, disk_bytes):
if os.environ.get("FM_AZURE_RUNNER_TEST_NO_DROP") == "1":
if uid != os.getuid() or gid != os.getgid():
Expand Down Expand Up @@ -159,6 +189,8 @@ def __exit__(self, exc_type, exc, traceback):


def main():
if len(sys.argv) == 4 and sys.argv[1] == "--verify-private-source-ancestors":
return verify_private_source_ancestors(Path(sys.argv[2]), Path(sys.argv[3]))
if len(sys.argv) != 8:
return fail("expected request, repo, output, uid, gid, VM id, and boot id")
request_path = Path(sys.argv[1])
Expand Down
37 changes: 24 additions & 13 deletions bin/fm-azure-runner-guest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ if supplied != "sha256:" + hashlib.sha256(canonical).hexdigest(): raise SystemEx
if "sha256:" + hashlib.sha256(executor_path.read_bytes()).hexdigest() != request["protocol"]["executor_digest"]: raise SystemExit("guest bootstrap: executor digest mismatch")
if request["protocol"]["guest_digest"] != sys.argv[3]: raise SystemExit("guest bootstrap: guest digest mismatch")
repo = request["repository"]
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")
if repo.get("source_mode") == "private-parent-bundle":
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")
if repo.get("source_mode") in ("private-parent-bundle", "private-exact-bundle"):
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")
else:
if repo.get("input_blob") is not None or repo.get("snapshot_bytes") != 0: raise SystemExit("guest bootstrap: public source carries private staging")
Expand Down Expand Up @@ -145,7 +145,7 @@ runuser -u fmrunner -- git -C /work/repo remote add origin "$REMOTE"
# repository as dubious (CVE-2022-24765); scope the exception through the
# environment exactly as the validation cell guest does.
export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0=/work/repo
if [ "$SOURCE_MODE" = private-parent-bundle ]; then
if [ "$SOURCE_MODE" = private-parent-bundle ] || [ "$SOURCE_MODE" = private-exact-bundle ]; then
[ "$INPUT_BLOB" = "$(read_request repository.input_blob)" ] || { echo "guest bootstrap: private snapshot blob mismatch" >&2; exit 125; }
SNAPSHOT=$BASE/snapshot.bundle
TOKEN_FILE=$BASE/input-token
Expand Down Expand Up @@ -184,10 +184,15 @@ for value in json.load(open(sys.argv[1],encoding="utf-8"))["repository"].get("so
PY
while IFS= read -r ancestor; do
[ -n "$ancestor" ] || continue
run_bootstrap_network runuser -u fmrunner -- git -C /work/repo fetch --depth=1 origin "$ancestor"
[ "$(git -C /work/repo rev-parse FETCH_HEAD)" = "$ancestor" ] || { echo "guest bootstrap: source ancestor identity mismatch" >&2; exit 125; }
git -C /work/repo cat-file -e "$ancestor^{commit}" || { echo "guest bootstrap: source ancestor is absent" >&2; exit 125; }
if [ "$SOURCE_MODE" = public-github-https ]; then
run_bootstrap_network runuser -u fmrunner -- git -C /work/repo fetch --depth=1 origin "$ancestor"
[ "$(git -C /work/repo rev-parse FETCH_HEAD)" = "$ancestor" ] || { echo "guest bootstrap: source ancestor identity mismatch" >&2; exit 125; }
git -C /work/repo cat-file -e "$ancestor^{commit}" || { echo "guest bootstrap: source ancestor is absent" >&2; exit 125; }
fi
done <"$BASE/source-ancestors"
if [ "$SOURCE_MODE" = private-parent-bundle ] || [ "$SOURCE_MODE" = private-exact-bundle ]; then
/usr/bin/python3 "$EXECUTOR" --verify-private-source-ancestors "$REQUEST" /work/repo
fi
[ "$(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; }
# Repository tests compare the snapshot against the default branch through
# the refs/remotes/origin view (generation 051 ground truth: a behavior
Expand Down Expand Up @@ -238,13 +243,19 @@ while IFS=$'\t' read -r url file bytes digest; do
fetch_exact "$url" "/work/home/.fm-runner-tools/wheelhouse/$file" "$bytes" "$digest"
done <"$BASE/wheels.tsv"
chown -R fmrunner:fmrunner /work/home/.fm-runner-tools
[ "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; }
# The run-command handler's download directory is root-only, so the
# unprivileged uv invocations must not inherit it as their working
# directory (uv's config discovery reads ./uv.toml and refuses on EACCES).
cd /work/repo
runuser -u fmrunner -- /work/home/.fm-runner-tools/uv/uv venv --python /usr/bin/python3 /work/repo/tools/agent-fleet/.venv >/dev/null
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
LOCK_DIGEST=$(read_request protocol.agent_fleet_python.lock_digest)
if [ "$LOCK_DIGEST" != None ]; then
[ "sha256:$(sha256sum /work/repo/tools/agent-fleet/uv.lock | awk '{print $1}')" = "$LOCK_DIGEST" ] || { echo "guest bootstrap: lock mismatch" >&2; exit 125; }
# The run-command handler's download directory is root-only, so the
# unprivileged uv invocations must not inherit it as their working
# directory (uv's config discovery reads ./uv.toml and refuses on EACCES).
cd /work/repo
runuser -u fmrunner -- /work/home/.fm-runner-tools/uv/uv venv --python /usr/bin/python3 /work/repo/tools/agent-fleet/.venv >/dev/null
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
elif [ -s "$BASE/wheels.tsv" ]; then
echo "guest bootstrap: unbound Python wheels" >&2
exit 125
fi

python3 - "$REQUEST" /work/repo <<'PY'
import hashlib,json,pathlib,sys
Expand Down
Loading
Loading