Skip to content

Serialize xcodebuild and fetch prebuilt GhosttyKit to unblock Xcode 26 builds#2981

Merged
austinywang merged 7 commits intomainfrom
issue-2980-xcodebuild-concurrent-deadlock
Apr 19, 2026
Merged

Serialize xcodebuild and fetch prebuilt GhosttyKit to unblock Xcode 26 builds#2981
austinywang merged 7 commits intomainfrom
issue-2980-xcodebuild-concurrent-deadlock

Conversation

@austinywang
Copy link
Copy Markdown
Contributor

@austinywang austinywang commented Apr 18, 2026

Summary

Fixes #2980 — concurrent reload.sh invocations deadlock on Xcode 26's SWBBuildService.

Changes in this PR:

  • scripts/reload.sh — serialize the top-level xcodebuild behind a per-user lock so parallel reload.sh runs queue instead of racing the shared Xcode 26 build service. The lock wait line and wrapper errors remain visible in the filtered build output.
  • scripts/ensure-ghosttykit.sh — before falling back to zig build (which internally spawns another xcodebuild), try to fetch a prebuilt GhosttyKit.xcframework for a clean ghostty submodule checkout. Any fetch, checksum, validation, or extraction failure cleanly falls back to the existing local ReleaseFast build path.
  • scripts/download-prebuilt-ghosttykit.sh and scripts/validate-xcframework-archive.py — share the checksum verification, archive validation, and safe extraction logic used by the prebuilt path.

No workflow .yml files are changed in this PR.

Test plan

  • ./scripts/reload.sh --tag a & and ./scripts/reload.sh --tag b & simultaneously on Xcode 26 — second run prints the waiting message, first run completes, second run then proceeds.
  • Clean ~/.cache/cmux/ghosttykit and ghostty/macos/GhosttyKit.xcframework, then run ./scripts/reload.sh --tag test — verify ==> Fetching prebuilt GhosttyKit.xcframework path runs and succeeds.
  • Set CMUX_GHOSTTYKIT_NO_PREBUILT=1, clean caches, run again — verify falls back to zig build.
  • Make a dirty edit in ghostty/, clean caches, run — verify skips fetch (dirty key) and falls back to zig build.
  • Normal warm-cache ./scripts/reload.sh --tag warm still reuses $CACHE_XCFRAMEWORK with no fetch and no build.

🤖 Generated with Claude Code


Note

Medium Risk
Changes the build/reload pipeline and introduces remote artifact download/extraction; while guarded by pinned SHA256 and archive validation, failures could still affect developer build reliability on macOS/Xcode 26.

Overview
Unblocks Xcode 26 dev builds by serializing xcodebuild invocations in scripts/reload.sh using a per-user lock, so parallel reload.sh runs queue instead of crashing the shared SWBBuildService.

Speeds and stabilizes GhosttyKit setup by adding an optional prebuilt GhosttyKit.xcframework fetch path in scripts/ensure-ghosttykit.sh (clean submodule only, opt-out via CMUX_GHOSTTYKIT_NO_PREBUILT=1), and hardens scripts/download-prebuilt-ghosttykit.sh with temp-dir extraction, curl timeouts, --no-same-owner, and a new scripts/validate-xcframework-archive.py to prevent unsafe/unexpected tar contents before installing.

Reviewed by Cursor Bugbot for commit 6398d65. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Apr 18, 2026 7:37pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 18, 2026

📝 Walkthrough

Walkthrough

Adds conditional prebuilt GhosttyKit xcframework retrieval with checksum lookup and archive validation, moves CI download logic into a dedicated script, and serializes xcodebuild invocations via a per-user flocked launcher to avoid Xcode 26 SWBBuildService deadlocks.

Changes

Cohort / File(s) Summary
xcodebuild serialization
scripts/reload.sh
Wraps xcodebuild invocation with a Python launcher that acquires a per-user exclusive flock (blocks with a wait message if held) and then execs the original xcodebuild to preserve tee/PIPESTATUS behavior.
Prebuilt xcframework control & validation
scripts/ensure-ghosttykit.sh
Adds lookup_pinned_ghosttykit_sha256() and try_fetch_prebuilt_xcframework() plus configurable checksum/validator overrides; conditionally downloads/verifies/validates/extracts GhosttyKit.xcframework from releases when cache is cold, falling back to local build on any failure.
Prebuilt download & extraction
scripts/download-prebuilt-ghosttykit.sh
New script: configurable timeouts, downloads into a temp dir, computes/verifies SHA-256, runs Python archive validator, extracts into a temp directory with --no-same-owner, then atomically moves GhosttyKit.xcframework to the target; cleans temps on exit.
Archive validator
scripts/validate-xcframework-archive.py
New Python validator that enforces archive path safety, restricts top-level entries to GhosttyKit.xcframework and its contents, validates link targets, and rejects unsafe member types; fails with explicit errors on violations.
CI workflow callers
.github/workflows/test-depot.yml, .github/workflows/test-e2e.yml
Replaces inline download/extract steps with calls to ./scripts/download-prebuilt-ghosttykit.sh and removes step-level GH_TOKEN; delegates artifact acquisition to repository scripts.

Sequence Diagram(s)

sequenceDiagram
    actor Dev
    participant Reload as reload.sh
    participant Lock as /tmp/cmux-xcodebuild.lock
    participant Ensure as ensure-ghosttykit.sh
    participant Remote as GitHub Releases
    participant Validator as validate-xcframework-archive.py
    participant Xcode as xcodebuild

    Dev->>Reload: run reload.sh
    Reload->>Lock: acquire exclusive flock (block if held)
    Reload->>Ensure: ensure GhosttyKit cache
    alt cache missing
        Ensure->>Remote: download GhosttyKit.xcframework.tar.gz
        Remote-->>Ensure: tar.gz
        Ensure->>Validator: validate archive
        Validator-->>Ensure: valid / fail
        alt valid
            Ensure->>Ensure: extract & move GhosttyKit.xcframework, update stamps
        else invalid/fail
            Ensure->>Ensure: fallback to local zig build (may invoke xcodebuild)
        end
    end
    Reload->>Xcode: exec xcodebuild (under lock)
    Xcode-->>Reload: exit status (preserved via PIPESTATUS)
    Reload->>Lock: release flock
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

aardvark, codex

Poem

🐇 I hopped through scripts with careful paws,
Fetched a prebuilt prize and checked its laws,
I gated xcodebuild with a cozy lock,
Validated tarballs — no sneaky shock,
Now builds hum softly while I munch on logs.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the two main changes: serializing xcodebuild and fetching prebuilt GhosttyKit to address Xcode 26 build issues.
Linked Issues check ✅ Passed All coding objectives from issue #2980 are met: per-user flock serializes xcodebuild [#2980], prebuilt xcframework download [#2980] with checksum pinning and archive validation, opt-out support, dirty submodule handling, and fallback to zig build [#2980].
Out of Scope Changes check ✅ Passed All changes are directly scoped to addressing #2980: reload.sh locking, ensure-ghosttykit.sh prebuilt fetch, CI workflow updates, and the new validation and download helper scripts. No unrelated changes detected.
Description check ✅ Passed PR description includes Summary, detailed testing plan with multiple scenarios, and explicit reference to linked issue #2980, but lacks Demo Video section and incomplete Testing verification boxes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-2980-xcodebuild-concurrent-deadlock

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
scripts/reload.sh (1)

331-345: Consider a per-user lock path for robustness on shared systems.

The global /tmp/cmux-xcodebuild.lock works correctly on single-user dev machines, but has two edge cases worth addressing:

  1. Cross-user EACCES on shared /tmp: If another user creates the lock first, the file is typically world-readable (0644) and the current user's open(..., ">>", ...) fails with Permission denied. The pre-check (line 336) silently swallows this via or exit 0, skipping the "waiting" message, then the blocking perl (line 340) dies with open lock: Permission denied. Since SWBBuildService is itself per-user, there is no reason for users to share the lock.
  2. TOCTOU on the wait banner (lines 336–338): The non-blocking probe releases the lock before the blocking call, so the "waiting" message may print with no actual wait — cosmetic only.

A per-user lock path fixes (1) and aligns with codebase patterns (e.g., build_remote_daemon_release_assets.sh uses ${TMPDIR:-/tmp}):

🔧 Suggested refinement
-XCODEBUILD_LOCK="/tmp/cmux-xcodebuild.lock"
+XCODEBUILD_LOCK="${TMPDIR:-/tmp}/cmux-xcodebuild-$(id -u).lock"

Functionality is otherwise correct — exec { $ARGV[0] } @ARGV`` after shift replaces perl with `xcodebuild`, the flock(2) lock survives the `exec` via the inherited file descriptor, and is auto-released when `xcodebuild` exits. `PIPESTATUS[0]` correctly reflects the `xcodebuild` exit code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/reload.sh` around lines 331 - 345, Set XCODEBUILD_LOCK to a per-user
tmp path (e.g. XCODEBUILD_LOCK="${TMPDIR:-/tmp}/cmux-xcodebuild-$UID.lock") and
update both perl invocations to use that variable; remove the non-blocking probe
perl block (the initial open/fcntl check that prints "waiting") to avoid the
TOCTOU banner and rely on the blocking perl flock block which will now open the
per-user lock without cross-user EACCES issues. Ensure the remaining perl flock
block still uses "$XCODEBUILD_LOCK" and preserves the exec { $ARGV[0] } `@ARGV`
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/ensure-ghosttykit.sh`:
- Around line 120-148: The script downloads and blindly extracts the GhosttyKit
tarball (tmp_tar -> tmp_extract -> LOCAL_XCFRAMEWORK) without integrity or
path-safety checks; modify ensure-ghosttykit.sh to first fetch the corresponding
checksum/signature (e.g., from "$url.sha256" or a cosign/minisign artifact) and
verify tmp_tar with shasum -a 256 -c (or verify signature) before any
extraction, and if verification fails exit with a clear error and fallback; only
after successful verification perform a safe extraction using tar
--no-same-owner and pre-validate the archive contents (reject absolute paths or
.. components by listing/tarring first) and only then move the validated
"$tmp_extract/GhosttyKit.xcframework" into LOCAL_XCFRAMEWORK; also add a brief
comment documenting the trust model if no checksum/signature is available.
- Around line 120-128: The mktemp template used for tmp_tar is invalid on macOS
(suffix after Xs) causing mktemp to fail and curl to write to empty path; update
try_fetch_prebuilt_xcframework to create a temp directory with mktemp -d using a
template that ends with Xs (e.g., ".ghosttykit-prebuilt.XXXXXX"), then set
tmp_tar to a file path inside that directory (e.g.,
"$tmp_dl_dir/GhosttyKit.xcframework.tar.gz"); ensure cleanup removes the temp
directory variable (tmp_dl_dir) instead of tmp_tar on failure; also harden curl
by adding --connect-timeout and --max-time flags to avoid hangs when downloading
from $url.

---

Nitpick comments:
In `@scripts/reload.sh`:
- Around line 331-345: Set XCODEBUILD_LOCK to a per-user tmp path (e.g.
XCODEBUILD_LOCK="${TMPDIR:-/tmp}/cmux-xcodebuild-$UID.lock") and update both
perl invocations to use that variable; remove the non-blocking probe perl block
(the initial open/fcntl check that prints "waiting") to avoid the TOCTOU banner
and rely on the blocking perl flock block which will now open the per-user lock
without cross-user EACCES issues. Ensure the remaining perl flock block still
uses "$XCODEBUILD_LOCK" and preserves the exec { $ARGV[0] } `@ARGV` behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 229e6b48-1b1c-4cf3-a9e1-c8166e94d95e

📥 Commits

Reviewing files that changed from the base of the PR and between 060eafd and b66e081.

📒 Files selected for processing (2)
  • scripts/ensure-ghosttykit.sh
  • scripts/reload.sh

Comment thread scripts/ensure-ghosttykit.sh
Comment thread scripts/ensure-ghosttykit.sh
Comment thread scripts/reload.sh Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/reload.sh">

<violation number="1" location="scripts/reload.sh:344">
P1: The lock is dropped before `xcodebuild` starts because the Perl wrapper uses `exec`, so concurrent builds can still run in parallel.</violation>
</file>

<file name="scripts/ensure-ghosttykit.sh">

<violation number="1" location="scripts/ensure-ghosttykit.sh:122">
P2: Use a BSD-compatible `mktemp` template with trailing `X`s. This pattern leaves `XXXXXX` literal on macOS, making the temp tarball path predictable and causing fetch fallback when that stale file already exists.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread scripts/reload.sh Outdated
Comment thread scripts/ensure-ghosttykit.sh Outdated
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 18, 2026

Greptile Summary

This PR fixes concurrent reload.sh deadlocks on Xcode 26 by serializing xcodebuild behind a per-user flock (via an inline Python script that correctly clears FD_CLOEXEC so the lock survives execvp), and adds an optional prebuilt GhosttyKit.xcframework fetch path backed by pinned SHA256 checksums, archive content validation, and temp-dir extraction before any destructive operations.

Confidence Score: 5/5

Safe to merge — all remaining findings are P2 hardening suggestions that don't affect correctness on the happy path.

The locking mechanism is technically sound (FD_CLOEXEC correctly cleared, execvp transfers the lock fd to xcodebuild). The prebuilt fetch path is well-guarded with pinned SHA256, archive validation, and temp-dir extraction. The two P2 findings (hard link target root-containment check in the validator; missing EXIT trap for tmp_dir in ensure-ghosttykit.sh) are hardening improvements and do not affect normal operation.

scripts/validate-xcframework-archive.py (hard link target check) and scripts/ensure-ghosttykit.sh (tmp_dir trap)

Important Files Changed

Filename Overview
scripts/reload.sh Adds a Python-based flock wrapper around xcodebuild to serialize concurrent invocations; correctly clears FD_CLOEXEC so the lock fd survives execvp and is held for xcodebuild's full lifetime. grep pattern updated to pass ==> progress lines through.
scripts/ensure-ghosttykit.sh Adds try_fetch_prebuilt_xcframework with SHA256 pinning, archive validation, and safe temp-dir extraction before falling back to zig build. Minor: tmp_dir lacks a signal-safe EXIT trap (leaks on SIGTERM).
scripts/download-prebuilt-ghosttykit.sh Hardened with temp-dir extraction, --connect-timeout/--max-time, --no-same-owner, and validate-xcframework-archive.py validation before rm -rf OUTPUT_DIR; clean EXIT trap for tmp cleanup.
scripts/validate-xcframework-archive.py New validator: blocks absolute paths, .. traversal, out-of-root entries, and unsafe symlinks before extraction. Hard link targets are not checked against the ROOT prefix — they pass is_safe_member but would cause confusing tar failures at extraction time.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[reload.sh] --> B[acquire per-user flock\nTMPDIR/cmux-xcodebuild-UID.lock]
    B -->|lock held| C[execvp xcodebuild]
    B -->|blocked| D[print waiting msg\nthen block on flock]
    D --> C
    C --> E[xcodebuild runs\nfd kept open - lock held]
    E --> F[xcodebuild exits\nfd closed - lock released]

    G[ensure-ghosttykit.sh] --> H{Cache hit?}
    H -->|yes| I[Reuse cached xcframework]
    H -->|no| J{Local xcframework\nkey matches?}
    J -->|yes| K[Seed cache from local]
    J -->|no| L[try_fetch_prebuilt_xcframework]
    L --> M{ghostty clean and\nchecksum pinned?}
    M -->|no| N[zig build ReleaseFast]
    M -->|yes| O[curl download\n--connect-timeout 10\n--max-time 300]
    O -->|fail| N
    O -->|ok| P[SHA256 verify]
    P -->|mismatch| N
    P -->|ok| Q[validate-xcframework-archive.py\ncheck paths/symlinks/hardlinks]
    Q -->|fail| N
    Q -->|ok| R[tar --no-same-owner\nto temp dir]
    R --> S[mv to LOCAL_XCFRAMEWORK\nwrite stamp files]
    S --> K
    K --> T[cp -R to CACHE_XCFRAMEWORK]
    N --> T
Loading

Reviews (2): Last reviewed commit: "Revert workflow changes from GhosttyKit ..." | Re-trigger Greptile

Comment thread scripts/ensure-ghosttykit.sh Outdated
Comment thread scripts/ensure-ghosttykit.sh Outdated
Comment thread scripts/reload.sh Outdated
Copy link
Copy Markdown
Contributor

Addressed the review findings in ec5f2380.

Changes:

  • scripts/ensure-ghosttykit.sh: fixed the macOS mktemp usage by switching to a temp directory layout, added connect/max timeouts, pinned SHA256 verification against scripts/ghosttykit-checksums.txt, documented the trust model, validated archive contents before extraction, and extracted with tar --no-same-owner.
  • scripts/reload.sh: replaced the Perl wrapper with a per-user lock file and a python3 exec wrapper that explicitly clears FD_CLOEXEC, so the lock is preserved across exec into xcodebuild.
  • scripts/download-prebuilt-ghosttykit.sh, test-e2e.yml, and test-depot.yml: moved CI onto the same verified/safe GhosttyKit download path so the fix is centralized.

Validation:

  • bash -n scripts/ensure-ghosttykit.sh scripts/reload.sh scripts/download-prebuilt-ghosttykit.sh
  • real prebuilt GhosttyKit smoke test against the current release asset: download + checksum verify + archive validation + extract succeeded
  • lock smoke test with two concurrent runs serialized correctly (elapsed=4.27 for two sleep 2 commands)

I also ran ./scripts/reload.sh --tag fix-pr2981-review; the script exercised the updated path, but xcodebuild itself stalled inside Xcode after CreateBuildDescription, so I terminated only that tagged build and relied on the targeted smoke checks above for verification.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
scripts/download-prebuilt-ghosttykit.sh (1)

27-64: Consider extracting validate_xcframework_archive into a shared helper.

The body of validate_xcframework_archive() here is a verbatim duplicate of the same function in scripts/ensure-ghosttykit.sh (see lines 43-81 of that file). Any future tightening of the archive-safety policy (new member types, additional path checks, etc.) will need to be applied in both places, which is easy to miss. Consider sourcing a common shell fragment (e.g. scripts/lib/ghosttykit-validate.sh) from both scripts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/download-prebuilt-ghosttykit.sh` around lines 27 - 64, The
validate_xcframework_archive function is duplicated across scripts (e.g., this
file and scripts/ensure-ghosttykit.sh); extract its Python-validation body into
a shared helper (e.g., create scripts/lib/ghosttykit-validate.sh that defines
validate_xcframework_archive) and update both scripts to source that helper and
call the shared validate_xcframework_archive, keeping the function name and
behavior identical so future policy changes apply in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/download-prebuilt-ghosttykit.sh`:
- Around line 117-120: The script currently removes "$OUTPUT_DIR" before
extracting the archive, which can irreversibly delete the existing
GhosttyKit.xcframework if extraction fails; change the order so you extract to
"$EXTRACT_DIR" first (tar --no-same-owner -xzf "$ARCHIVE_PATH" -C
"$EXTRACT_DIR"), then remove "$OUTPUT_DIR" immediately before replacing it, and
finally mv "$EXTRACT_DIR/GhosttyKit.xcframework" "$OUTPUT_DIR"; reference the
existing variables/commands OUTPUT_DIR, EXTRACT_DIR, ARCHIVE_PATH, rm -rf, tar
and mv to locate and reorder these steps.

---

Nitpick comments:
In `@scripts/download-prebuilt-ghosttykit.sh`:
- Around line 27-64: The validate_xcframework_archive function is duplicated
across scripts (e.g., this file and scripts/ensure-ghosttykit.sh); extract its
Python-validation body into a shared helper (e.g., create
scripts/lib/ghosttykit-validate.sh that defines validate_xcframework_archive)
and update both scripts to source that helper and call the shared
validate_xcframework_archive, keeping the function name and behavior identical
so future policy changes apply in one place.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b22906b6-0636-49af-87f3-52c6d277f1c2

📥 Commits

Reviewing files that changed from the base of the PR and between b66e081 and ec5f238.

📒 Files selected for processing (5)
  • .github/workflows/test-depot.yml
  • .github/workflows/test-e2e.yml
  • scripts/download-prebuilt-ghosttykit.sh
  • scripts/ensure-ghosttykit.sh
  • scripts/reload.sh
🚧 Files skipped from review as they are similar to previous changes (2)
  • scripts/reload.sh
  • scripts/ensure-ghosttykit.sh

Comment thread scripts/download-prebuilt-ghosttykit.sh Outdated
Comment thread scripts/reload.sh
Comment thread scripts/ensure-ghosttykit.sh Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
scripts/validate-xcframework-archive.py (1)

28-44: Minor: saw_root is set for any accepted member, not specifically the root.

Because the prior check at line 33 already rejects any entry that isn't GhosttyKit.xcframework or under GhosttyKit.xcframework/, setting saw_root = True for every member is effectively equivalent to "archive was non-empty." The variable name suggests stricter semantics (that the GhosttyKit.xcframework directory entry itself was seen). Functionally correct today, but consider either renaming (e.g., saw_any_entry) or tightening the check to name == ROOT or name == ROOT + "/" so the guard matches its name.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/validate-xcframework-archive.py` around lines 28 - 44, The flag
saw_root currently flips for any accepted archive member, which only verifies
the archive is non-empty; change its semantics to truly detect the presence of
the ROOT directory entry by only setting saw_root when name == ROOT or name ==
ROOT + "/", i.e. inside the tar.getmembers() loop after the existing validations
check for the ROOT entry explicitly (use the existing normalize(name),
is_safe_member(), and ROOT symbols) so the final missing-ROOT guard accurately
reflects whether the GhosttyKit.xcframework root directory was present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@scripts/validate-xcframework-archive.py`:
- Around line 28-44: The flag saw_root currently flips for any accepted archive
member, which only verifies the archive is non-empty; change its semantics to
truly detect the presence of the ROOT directory entry by only setting saw_root
when name == ROOT or name == ROOT + "/", i.e. inside the tar.getmembers() loop
after the existing validations check for the ROOT entry explicitly (use the
existing normalize(name), is_safe_member(), and ROOT symbols) so the final
missing-ROOT guard accurately reflects whether the GhosttyKit.xcframework root
directory was present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a26d18b8-a4d3-4083-aee1-c7204efde77c

📥 Commits

Reviewing files that changed from the base of the PR and between ec5f238 and 590f1b5.

📒 Files selected for processing (4)
  • scripts/download-prebuilt-ghosttykit.sh
  • scripts/ensure-ghosttykit.sh
  • scripts/reload.sh
  • scripts/validate-xcframework-archive.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • scripts/reload.sh
  • scripts/ensure-ghosttykit.sh
  • scripts/download-prebuilt-ghosttykit.sh

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/validate-xcframework-archive.py`:
- Around line 35-40: The link-validation currently calls
normalize(member.linkname) and is_safe_member(...) but does not ensure links
point inside the xcframework or that the ROOT entry itself isn't a link; update
the validation in the branch where member.islnk() or member.issym() is true to
(1) reject if the current member name equals ROOT or ROOT + "/" (i.e., the ROOT
entry is a link), (2) compute the resolved target path relative to the archive
entry (using normalize(member.linkname) and joining it with the member's
dirname) and then verify the resolved target is under the ROOT subtree (e.g.,
resolved_path == ROOT or startswith(ROOT + "/")), and (3) raise SystemExit with
a clear message referencing member.linkname if the resolved target is outside
ROOT; keep using is_safe_member() first to reject absolute/.. paths but add this
extra ROOT containment check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a35dd511-d136-48ac-b6f4-b5de8d79ca43

📥 Commits

Reviewing files that changed from the base of the PR and between 590f1b5 and 5a92a7d.

📒 Files selected for processing (1)
  • scripts/validate-xcframework-archive.py

Comment on lines +35 to +40
if name == ROOT or name == ROOT + "/":
saw_root = True
if member.islnk() or member.issym():
target = normalize(member.linkname)
if not target or not is_safe_member(target):
raise SystemExit(f"unsafe archive link target: {member.linkname}")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat scripts/validate-xcframework-archive.py

Repository: manaflow-ai/cmux

Length of output: 1572


🏁 Script executed:

#!/bin/bash
# Check if there are any test files that show expected behavior
find . -type f -name "*test*validate*" -o -name "*tar*test*" 2>/dev/null | head -20

# Also check git history for context on this file
git log --oneline scripts/validate-xcframework-archive.py 2>/dev/null | head -5

# Search for how this archive is created
git grep -l "xcframework.tar.gz\|validate-xcframework" 2>/dev/null

Repository: manaflow-ai/cmux

Length of output: 341


🏁 Script executed:

# Check the download and ensure scripts to understand archive usage
cat scripts/download-prebuilt-ghosttykit.sh

echo "---"

cat scripts/ensure-ghosttykit.sh

Repository: manaflow-ai/cmux

Length of output: 11270


🏁 Script executed:

#!/bin/bash
# Verify the key security claims about the link validation logic

# Create a test script to check path safety logic
python3 << 'VERIFY'
from pathlib import PurePosixPath

# Current validation function
def is_safe_member(name: str) -> bool:
    path = PurePosixPath(name)
    return not path.is_absolute() and ".." not in path.parts

# Test cases for the review's concerns

print("=== Testing current is_safe_member() validation ===\n")

# Test 1: Hardlink target outside ROOT with no ".." or absolute path
print("Test 1: Hardlink to 'some-other-path'")
target = "some-other-path"
safe = is_safe_member(target)
print(f"  is_safe_member('{target}'): {safe}")
print(f"  Issue: This path doesn't start with ROOT but is_safe_member returns {safe}\n")

# Test 2: Relative path with ".."
print("Test 2: Path with '..'")
target = "../escape"
safe = is_safe_member(target)
print(f"  is_safe_member('{target}'): {safe}\n")

# Test 3: Absolute path
print("Test 3: Absolute path")
target = "/etc/passwd"
safe = is_safe_member(target)
print(f"  is_safe_member('{target}'): {safe}\n")

# Test 4: Relative path that looks safe but could escape via symlink resolution
print("Test 4: Relative symlink in subdirectory")
symlink_name = "GhosttyKit.xcframework/subdir/link"
symlink_target = "foo"
safe = is_safe_member(symlink_target)
print(f"  Member path: {symlink_name}")
print(f"  Link target: {symlink_target}")
print(f"  is_safe_member('{symlink_target}'): {safe}")
symlink_parent = PurePosixPath(symlink_name).parent
resolved = symlink_parent / symlink_target
print(f"  Resolved target when extracted: {resolved}")
print(f"  Stays under ROOT? {str(resolved).startswith('GhosttyKit.xcframework')}\n")

# Test 5: ROOT as a symlink
print("Test 5: ROOT entry as symlink")
root_name = "GhosttyKit.xcframework"
root_target = "../real"
safe = is_safe_member(root_target)
print(f"  Member path: {root_name}")
print(f"  Link target: {root_target}")
print(f"  Current code rejects with '..'? {'..' in PurePosixPath(root_target).parts}")
print(f"  But if target was 'real'? {is_safe_member('real')}")
print(f"  Issue: ROOT as symlink could redirect framework access\n")

print("=== Summary of issues ===")
print("1. Hardlinks can target paths outside ROOT (e.g., 'some-other-path')")
print("2. ROOT itself could be a symlink to a relative target without '..'")
print("3. Symlinks in subdirs could resolve outside ROOT if we only validate target safety, not resolution")

VERIFY

Repository: manaflow-ai/cmux

Length of output: 1109


🏁 Script executed:

#!/bin/bash
# Verify the proposed fix logic

python3 << 'VERIFY'
from pathlib import PurePosixPath

ROOT = "GhosttyKit.xcframework"

def normalize(name: str) -> str:
    while name.startswith("./"):
        name = name[2:]
    return name

def is_safe_member(name: str) -> bool:
    path = PurePosixPath(name)
    return not path.is_absolute() and ".." not in path.parts

# Proposed fix helper
def is_under_root(path: PurePosixPath) -> bool:
    value = str(path)
    return value == ROOT or value.startswith(ROOT + "/")

print("=== Testing proposed fix logic ===\n")

# Test case 1: ROOT as hardlink to relative target
print("Test 1: ROOT as hardlink")
name = "GhosttyKit.xcframework"
member_linkname = "something"
target = normalize(member_linkname)
if name.rstrip("/") == ROOT:
    print(f"  Member: {name}")
    print(f"  Target: {target}")
    print(f"  Proposed fix REJECTS: 'archive root must be a directory'\n")

# Test case 2: Symlink in subdirectory with absolute resolution
print("Test 2: Symlink in subdir - absolute resolution")
name = "GhosttyKit.xcframework/lib"
member_linkname = "external"
target = normalize(member_linkname)
target_path = PurePosixPath(target)
name_path = PurePosixPath(name)
resolved_target = name_path.parent / target_path
print(f"  Member: {name}")
print(f"  Target: {target}")
print(f"  Name parent: {name_path.parent}")
print(f"  Resolved: {resolved_target}")
print(f"  is_under_root({resolved_target}): {is_under_root(resolved_target)}")
if not is_under_root(resolved_target):
    print(f"  Proposed fix REJECTS: 'escapes ROOT'\n")

# Test case 3: Symlink with relative path back out
print("Test 3: Symlink pointing outside")
name = "GhosttyKit.xcframework/subdir/lib"
member_linkname = "../../external"
target = normalize(member_linkname)
if ".." in PurePosixPath(target).parts:
    print(f"  Member: {name}")
    print(f"  Target: {target}")
    print(f"  Contains '..': Yes")
    print(f"  Already caught by is_safe_member check\n")

# Test case 4: Hardlink to outside member
print("Test 4: Hardlink validation")
name = "GhosttyKit.xcframework/libghost.a"
member_linkname = "other-lib"
target = normalize(member_linkname)
target_path = PurePosixPath(target)
resolved_target = target_path  # For hardlinks, no relative resolution
print(f"  Member: {name}")
print(f"  Hardlink target: {target}")
print(f"  Resolved (hardlink, no parent): {resolved_target}")
print(f"  is_under_root({resolved_target}): {is_under_root(resolved_target)}")
if not is_under_root(resolved_target):
    print(f"  Proposed fix REJECTS: 'escapes ROOT'\n")

# Test case 5: Valid symlink within framework
print("Test 5: Valid symlink within framework")
name = "GhosttyKit.xcframework/lib/link"
member_linkname = "libghostty.a"
target = normalize(member_linkname)
target_path = PurePosixPath(target)
name_path = PurePosixPath(name)
resolved_target = name_path.parent / target_path
print(f"  Member: {name}")
print(f"  Target: {target}")
print(f"  Resolved: {resolved_target}")
print(f"  is_under_root({resolved_target}): {is_under_root(resolved_target)}")
print(f"  Result: ALLOWED\n")

print("=== Fix correctness assessment ===")
print("✓ Prevents ROOT from being a symlink/hardlink")
print("✓ Ensures symlink targets resolve within ROOT (relative resolution)")
print("✓ Ensures hardlink targets are within ROOT")

VERIFY

Repository: manaflow-ai/cmux

Length of output: 1288


🏁 Script executed:

#!/bin/bash
# Verify edge cases and implementation correctness of the proposed fix

python3 << 'VERIFY'
from pathlib import PurePosixPath

ROOT = "GhosttyKit.xcframework"

def is_under_root(path: PurePosixPath) -> bool:
    value = str(path)
    return value == ROOT or value.startswith(ROOT + "/")

print("=== Testing edge cases in proposed fix ===\n")

# Edge case 1: name.rstrip("/") logic
print("Edge case 1: ROOT with trailing slash")
names = ["GhosttyKit.xcframework", "GhosttyKit.xcframework/"]
for name in names:
    result = name.rstrip("/") == ROOT
    print(f"  name.rstrip('/') == ROOT for '{name}': {result}")
print()

# Edge case 2: Path with PurePosixPath normalization
print("Edge case 2: PurePosixPath path normalization")
test_paths = [
    ("GhosttyKit.xcframework", True),
    ("GhosttyKit.xcframework/", True),
    ("GhosttyKit.xcframework/subdir", True),
    ("GhosttyKit.xcframework/subdir/file", True),
    ("other-dir", False),
    ("other-dir/GhosttyKit.xcframework", False),
]
for path_str, expected in test_paths:
    result = is_under_root(PurePosixPath(path_str))
    status = "✓" if result == expected else "✗"
    print(f"  {status} is_under_root('{path_str}'): {result} (expected {expected})")
print()

# Edge case 3: Symlink resolution when name ends with /
print("Edge case 3: Symlink resolution with trailing slash in name")
name = "GhosttyKit.xcframework/lib/"
target = "external"
target_path = PurePosixPath(target)
name_path = PurePosixPath(name)
resolved = name_path.parent / target_path
print(f"  name: '{name}'")
print(f"  name_path.parent: {name_path.parent}")
print(f"  resolved: {resolved}")
print(f"  Correct? {str(resolved) == 'GhosttyKit.xcframework/external'}\n")

# Edge case 4: Hardlink with relative target
print("Edge case 4: Hardlink to relative path")
target = "lib/libghostty.a"
target_path = PurePosixPath(target)
under = is_under_root(target_path)
print(f"  Hardlink target: '{target}'")
print(f"  is_under_root: {under}")
print(f"  Correctly REJECTED: {not under}\n")

# Edge case 5: Check if all member names are already validated to start with ROOT
print("Edge case 5: Pre-validation of member names")
print("  From code: every member name is checked with:")
print("    if name != ROOT and not name.startswith(ROOT + '/'):")
print("        raise SystemExit(...)")
print("  So at link validation, name ALWAYS satisfies is_under_root(name)")
print("  Therefore, for symlinks, resolved_target will too (if it stays local)")
print()

print("=== Implementation completeness ===")
print("✓ is_under_root() correctly identifies ROOT and descendants")
print("✓ name.rstrip('/') handles trailing slashes properly")
print("✓ Symlink resolution uses parent directory (correct for relative symlinks)")
print("✓ Hardlink resolution uses target directly (correct for hardlinks)")

VERIFY

Repository: manaflow-ai/cmux

Length of output: 1671


Ensure symlink and hardlink targets remain within the xcframework tree.

Line 37 validates link targets with is_safe_member(), which rejects absolute paths and .. components, but does not verify that targets stay under GhosttyKit.xcframework. This allows:

  1. Hardlinks to point to paths outside the framework (e.g., some-other-path passes is_safe_member() but is not under ROOT)
  2. The ROOT entry itself to be a symlink or hardlink, potentially redirecting access

Add a check to reject ROOT as a link and verify that resolved link targets remain under GhosttyKit.xcframework before extraction.

🛡️ Proposed hardening
 def is_safe_member(name: str) -> bool:
     path = PurePosixPath(name)
     return not path.is_absolute() and ".." not in path.parts
 
 
+def is_under_root(path: PurePosixPath) -> bool:
+    value = str(path)
+    return value == ROOT or value.startswith(ROOT + "/")
+
+
 def main() -> None:
@@
             if name == ROOT or name == ROOT + "/":
                 saw_root = True
             if member.islnk() or member.issym():
                 target = normalize(member.linkname)
                 if not target or not is_safe_member(target):
                     raise SystemExit(f"unsafe archive link target: {member.linkname}")
+                if name.rstrip("/") == ROOT:
+                    raise SystemExit(f"archive root must be a directory: {member.name}")
+
+                target_path = PurePosixPath(target)
+                if member.issym():
+                    resolved_target = PurePosixPath(name).parent / target_path
+                else:
+                    resolved_target = target_path
+
+                if not is_under_root(resolved_target):
+                    raise SystemExit(f"archive link target escapes {ROOT}: {member.linkname}")
             elif not (member.isfile() or member.isdir()):
                 raise SystemExit(f"unsupported archive member: {member.name}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/validate-xcframework-archive.py` around lines 35 - 40, The
link-validation currently calls normalize(member.linkname) and
is_safe_member(...) but does not ensure links point inside the xcframework or
that the ROOT entry itself isn't a link; update the validation in the branch
where member.islnk() or member.issym() is true to (1) reject if the current
member name equals ROOT or ROOT + "/" (i.e., the ROOT entry is a link), (2)
compute the resolved target path relative to the archive entry (using
normalize(member.linkname) and joining it with the member's dirname) and then
verify the resolved target is under the ROOT subtree (e.g., resolved_path ==
ROOT or startswith(ROOT + "/")), and (3) raise SystemExit with a clear message
referencing member.linkname if the resolved target is outside ROOT; keep using
is_safe_member() first to reject absolute/.. paths but add this extra ROOT
containment check.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".github/workflows/test-e2e.yml">

<violation number="1">
P1: The inlined GhosttyKit download/extract path skips checksum and archive validation, so CI can extract an unverified release artifact.</violation>
</file>

<file name=".github/workflows/test-depot.yml">

<violation number="1">
P1: The new inlined GhosttyKit download/extract bypasses pinned checksum and archive validation, weakening CI supply-chain protections.</violation>
</file>

<file name="scripts/validate-xcframework-archive.py">

<violation number="1" location="scripts/validate-xcframework-archive.py:35">
P2: The validator incorrectly requires an explicit root directory entry, so valid archives with only `GhosttyKit.xcframework/...` members are rejected.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

raise SystemExit(f"unsafe archive entry: {member.name}")
if name != ROOT and not name.startswith(ROOT + "/"):
raise SystemExit(f"unexpected archive entry: {member.name}")
if name == ROOT or name == ROOT + "/":
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The validator incorrectly requires an explicit root directory entry, so valid archives with only GhosttyKit.xcframework/... members are rejected.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/validate-xcframework-archive.py, line 35:

<comment>The validator incorrectly requires an explicit root directory entry, so valid archives with only `GhosttyKit.xcframework/...` members are rejected.</comment>

<file context>
@@ -0,0 +1,49 @@
+                raise SystemExit(f"unsafe archive entry: {member.name}")
+            if name != ROOT and not name.startswith(ROOT + "/"):
+                raise SystemExit(f"unexpected archive entry: {member.name}")
+            if name == ROOT or name == ROOT + "/":
+                saw_root = True
+            if member.islnk() or member.issym():
</file context>
Fix with Cubic

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6398d65. Configure here.

tmp_dir="$(mktemp -d "$CACHE_ROOT/.ghosttykit-prebuilt.XXXXXX")"
tmp_tar="$tmp_dir/GhosttyKit.xcframework.tar.gz"
tmp_extract="$tmp_dir/extract"
mkdir -p "$tmp_extract"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set -e disabled in conditional context risks bad paths

Medium Severity

Because try_fetch_prebuilt_xcframework is invoked inside an elif condition (line 216), bash disables set -e for the entire function body. If mktemp -d fails at line 156, tmp_dir silently becomes an empty string, causing tmp_tar to resolve to /GhosttyKit.xcframework.tar.gz and tmp_extract to /extract. Subsequent operations like mkdir -p and curl -o then target the root filesystem. The mktemp failure and cascading errors are all silently swallowed. An explicit guard after mktemp (e.g. checking tmp_dir is non-empty and a directory) is needed.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6398d65. Configure here.

@austinywang austinywang reopened this Apr 19, 2026
@austinywang austinywang merged commit e976893 into main Apr 19, 2026
34 of 35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Concurrent reload.sh invocations deadlock on Xcode 26 SWBBuildService

2 participants