Skip to content

Commit df7060e

Browse files
committed
git-launcher: scrub checkout before launch
1 parent cab7a2f commit df7060e

4 files changed

Lines changed: 50 additions & 10 deletions

File tree

git-launcher/README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ no shell expansion in the parse step.
131131
| --- | --- |
132132
| `REPO_URL` | Git URL of the upstream workload repo (`https://…` or `git@…`). |
133133
| `COMMIT_SHA` | **Full** 40-hex SHA-1 or 64-hex SHA-256. Branches, tags, and short SHAs are rejected. |
134-
| `WORK_DIR` | Local directory used as the checkout. Created if missing. Reused on subsequent runs as long as the existing clone's `origin` URL matches `REPO_URL`. |
134+
| `WORK_DIR` | Local directory used as the checkout. Created if missing. Reused on subsequent runs as long as the existing clone's `origin` URL matches `REPO_URL`. Scrubbed before each launch; keep mutable app state outside this directory. |
135135

136136
### Optional
137137

@@ -178,8 +178,9 @@ audited alongside `COMMIT_SHA`.
178178

179179
* Will: clone fresh if `WORK_DIR` is empty; reuse the existing clone otherwise
180180
(after asserting that its `origin` URL matches `REPO_URL`).
181-
* Will: `git fetch --tags --prune origin`, then `git checkout --detach $SHA`,
182-
then `git rev-parse HEAD` and assert it equals `COMMIT_SHA`.
181+
* Will: `git fetch --tags --prune origin`, `git checkout --detach $SHA`,
182+
`git reset --hard $SHA`, `git clean -ffdx`, then `git rev-parse HEAD` and
183+
assert it equals `COMMIT_SHA`.
183184
* Will not: fall back to a branch, tag, or `HEAD` if the commit is missing.
184185
A missing commit is a hard failure.
185186
* Will not: accept short SHAs. A truncated SHA could resolve ambiguously if
@@ -198,10 +199,13 @@ directory is reused only if it is already a git checkout whose `origin` exactly
198199
matches `REPO_URL`.
199200

200201
Every boot still runs the same verification path: fetch from `origin`, detach
201-
checkout to `COMMIT_SHA`, then assert `git rev-parse HEAD == COMMIT_SHA`.
202-
Persistent state is therefore only a cache. It does not decide what runs. If
203-
the volume is empty, the launcher reclones. If the volume is non-empty but not a
204-
git checkout, or if it points at a different origin, startup fails.
202+
checkout to `COMMIT_SHA`, reset tracked files, remove untracked files with
203+
`git clean -ffdx`, then assert `git rev-parse HEAD == COMMIT_SHA`. The checkout
204+
volume is therefore a source cache only. It must not hold application state,
205+
SQLite databases, uploaded files, or build artefacts that the workload expects
206+
to survive; put those in a separate workload-owned volume. If the volume is
207+
empty, the launcher reclones. If the volume is non-empty but not a git checkout,
208+
or if it points at a different origin, startup fails.
205209

206210
## Example
207211

git-launcher/VERIFY.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ flowchart LR
4242
the workload code *and* its entry point `entrypoint.sh`; no separate
4343
install/run command audit is needed.
4444
4. **Spot-check runtime logs.** `phala logs --cvm-id <id>` should show
45-
`HEAD verified: <COMMIT_SHA>` and `exec in <dir>: bash entrypoint.sh`.
46-
Logs are corroborating only; the trust root is steps 1–3.
45+
`scrubbing checkout`, `HEAD verified: <COMMIT_SHA>`, and
46+
`exec in <dir>: bash entrypoint.sh`. Logs are corroborating only; the
47+
trust root is steps 1–3.
4748

4849
If all four line up, the bytes executing in the TEE are exactly the
4950
upstream commit you audited, produced by an audited launcher.
@@ -263,12 +264,13 @@ phala logs --cvm-id <id> -n 200
263264
```
264265

265266
Default-mode output should include these lines (the launcher logs `mode`
266-
during config summary, then the checkout/verify lines, then the `exec`
267+
during config summary, then the checkout/scrub/verify lines, then the `exec`
267268
line, so they appear in this order):
268269

269270
```
270271
[git-launcher] mode: default (workload repo entrypoint.sh)
271272
[git-launcher] checking out <COMMIT_SHA>
273+
[git-launcher] scrubbing checkout
272274
[git-launcher] HEAD verified: <COMMIT_SHA>
273275
[git-launcher] exec in <WORK_DIR>[/<REPO_SUBDIR>]: bash entrypoint.sh
274276
```

git-launcher/bin/git-launcher

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Required keys:
4141
Branches, tags, and short SHAs are rejected.
4242
WORK_DIR Local directory used as the checkout. Created if missing.
4343
Reused across runs if it already contains a clone of REPO_URL.
44+
Scrubbed before each launch; keep mutable app state elsewhere.
4445
4546
Optional keys:
4647
REPO_SUBDIR Path inside the repo to cd into before running the entry
@@ -206,6 +207,10 @@ ensure_repo_at_commit() {
206207
die "git checkout failed for commit $sha (commit not present in $url? short SHA disallowed)"
207208
fi
208209

210+
log "scrubbing checkout"
211+
git reset --hard "$sha" >/dev/null
212+
git clean -ffdx >/dev/null
213+
209214
local head
210215
head=$(git rev-parse --verify HEAD)
211216
local sha_lc=${sha,,}

git-launcher/tests/run-tests.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,34 @@ EOF
173173
return 0
174174
}
175175

176+
test_checkout_scrubs_dirty_source() {
177+
local work=$TMPROOT/work-scrub
178+
local marker=$TMPROOT/marker-scrub.txt
179+
local conf=$TMPROOT/conf-scrub.env
180+
181+
git clone -q "$FIXTURE" "$work"
182+
git -C "$work" checkout -q --detach "$PIN_SHA"
183+
echo "tampered" > "$work/greeting.txt"
184+
echo "stale" > "$work/stale.txt"
185+
mkdir -p "$work/target"
186+
echo "compiled object from a previous boot" > "$work/target/old.o"
187+
188+
cat > "$conf" <<EOF
189+
REPO_URL=$FIXTURE
190+
COMMIT_SHA=$PIN_SHA
191+
WORK_DIR=$work
192+
REPO_SUBDIR=sub
193+
INSTALL_CMD=
194+
RUN_CMD="MARKER_FILE='$marker' ./run.sh"
195+
EOF
196+
"$LAUNCHER" "$conf" || return 1
197+
grep -q "greeting=hello" "$marker" || { echo "dirty tracked file was not reset" >&2; cat "$marker" >&2; return 1; }
198+
[[ ! -e $work/stale.txt ]] || { echo "untracked stale file survived checkout scrub" >&2; return 1; }
199+
[[ ! -e $work/target/old.o ]] || { echo "untracked build artifact survived checkout scrub" >&2; return 1; }
200+
[[ -z $(git -C "$work" status --short --untracked-files=all) ]] || { echo "worktree is not clean after launcher scrub" >&2; git -C "$work" status --short --untracked-files=all >&2; return 1; }
201+
return 0
202+
}
203+
176204
test_bogus_sha_fails() {
177205
local work=$TMPROOT/work-bogus
178206
local conf=$TMPROOT/conf-bogus.env
@@ -518,6 +546,7 @@ echo
518546

519547
run_case "happy_pinning_runs_pinned_commit" test_happy_pinning
520548
run_case "rerun_advances_pin" test_rerun_advance_pin
549+
run_case "checkout_scrubs_dirty_source" test_checkout_scrubs_dirty_source
521550
run_case "bogus_sha_fails" test_bogus_sha_fails
522551
run_case "branch_name_rejected" test_branch_name_rejected
523552
run_case "tag_name_rejected" test_tag_name_rejected

0 commit comments

Comments
 (0)