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
22 changes: 18 additions & 4 deletions scripts/download-prebuilt-ghosttykit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ CHECKSUMS_FILE="${GHOSTTYKIT_CHECKSUMS_FILE:-$SCRIPT_DIR/ghosttykit-checksums.tx
DOWNLOAD_URL="${GHOSTTYKIT_URL:-https://github.com/manaflow-ai/ghostty/releases/download/$TAG/$ARCHIVE_NAME}"
DOWNLOAD_RETRIES="${GHOSTTYKIT_DOWNLOAD_RETRIES:-30}"
DOWNLOAD_RETRY_DELAY="${GHOSTTYKIT_DOWNLOAD_RETRY_DELAY:-20}"
DOWNLOAD_CONNECT_TIMEOUT="${GHOSTTYKIT_DOWNLOAD_CONNECT_TIMEOUT:-10}"
DOWNLOAD_MAX_TIME="${GHOSTTYKIT_DOWNLOAD_MAX_TIME:-300}"
ARCHIVE_VALIDATOR="${GHOSTTYKIT_ARCHIVE_VALIDATOR:-$SCRIPT_DIR/validate-xcframework-archive.py}"

if [ ! -f "$CHECKSUMS_FILE" ]; then
echo "Missing checksum file: $CHECKSUMS_FILE" >&2
Expand Down Expand Up @@ -48,24 +51,35 @@ if [ -z "$EXPECTED_SHA256" ]; then
fi

echo "Downloading $ARCHIVE_NAME for ghostty $GHOSTTY_SHA"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ghosttykit-download.XXXXXX")"
trap 'rm -rf "$TMP_DIR"' EXIT
ARCHIVE_BASENAME="$(basename "$ARCHIVE_NAME")"
ARCHIVE_PATH="$TMP_DIR/$ARCHIVE_BASENAME"
EXTRACT_DIR="$TMP_DIR/extract"
mkdir -p "$EXTRACT_DIR"

curl --fail --show-error --location \
--connect-timeout "$DOWNLOAD_CONNECT_TIMEOUT" \
--max-time "$DOWNLOAD_MAX_TIME" \
--retry "$DOWNLOAD_RETRIES" \
--retry-delay "$DOWNLOAD_RETRY_DELAY" \
--retry-all-errors \
-o "$ARCHIVE_NAME" \
-o "$ARCHIVE_PATH" \
"$DOWNLOAD_URL"

ACTUAL_SHA256="$(shasum -a 256 "$ARCHIVE_NAME" | awk '{print $1}')"
ACTUAL_SHA256="$(shasum -a 256 "$ARCHIVE_PATH" | awk '{print $1}')"
if [ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]; then
echo "$ARCHIVE_NAME checksum mismatch" >&2
echo "Expected: $EXPECTED_SHA256" >&2
echo "Actual: $ACTUAL_SHA256" >&2
exit 1
fi

python3 "$ARCHIVE_VALIDATOR" "$ARCHIVE_PATH"
mkdir -p "$(dirname "$OUTPUT_DIR")"
tar --no-same-owner -xzf "$ARCHIVE_PATH" -C "$EXTRACT_DIR"
rm -rf "$OUTPUT_DIR"
tar xzf "$ARCHIVE_NAME"
rm "$ARCHIVE_NAME"
mv "$EXTRACT_DIR/GhosttyKit.xcframework" "$OUTPUT_DIR"
test -d "$OUTPUT_DIR"

echo "Verified and extracted $OUTPUT_DIR"
99 changes: 99 additions & 0 deletions scripts/ensure-ghosttykit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ hash_file() {
fi
}

lookup_pinned_ghosttykit_sha256() {
local ghostty_sha="$1"
local checksums_file="$2"
awk -v sha="$ghostty_sha" '
$1 == sha {
print $2
found = 1
exit
}
END {
if (!found) {
exit 1
}
}
' "$checksums_file"
}

validate_bridge_header() {
local path="$1"
python3 - "$path" <<'PY'
Expand Down Expand Up @@ -86,6 +103,8 @@ LOCAL_XCFRAMEWORK="$PROJECT_DIR/ghostty/macos/GhosttyKit.xcframework"
LOCAL_KEY_STAMP="$LOCAL_XCFRAMEWORK/.ghostty_state_key"
LEGACY_LOCAL_SHA_STAMP="$LOCAL_XCFRAMEWORK/.ghostty_sha"
LOCK_DIR="$CACHE_ROOT/$GHOSTTY_KEY.lock"
GHOSTTYKIT_CHECKSUMS_FILE="${CMUX_GHOSTTYKIT_CHECKSUMS_FILE:-$SCRIPT_DIR/ghosttykit-checksums.txt}"
GHOSTTYKIT_ARCHIVE_VALIDATOR="${CMUX_GHOSTTYKIT_ARCHIVE_VALIDATOR:-$SCRIPT_DIR/validate-xcframework-archive.py}"

mkdir -p "$CACHE_ROOT"

Expand All @@ -104,6 +123,84 @@ while ! mkdir "$LOCK_DIR" 2>/dev/null; do
done
trap 'rmdir "$LOCK_DIR" >/dev/null 2>&1 || true' EXIT

try_fetch_prebuilt_xcframework() {
# Only attempt when Ghostty submodule is clean — dirty trees won't match any
# published release. Opt-out via CMUX_GHOSTTYKIT_NO_PREBUILT=1.
#
# Trust model: only install prebuilt artifacts whose SHA256 is pinned in the
# reviewed checksum manifest for the current ghostty submodule commit.
# Unpinned or mismatched artifacts fall back to a local ReleaseFast build.
if [[ "$GHOSTTY_KEY" != "$GHOSTTY_SHA" ]]; then
return 1
fi
if [[ "${CMUX_GHOSTTYKIT_NO_PREBUILT:-0}" == "1" ]]; then
return 1
fi
if ! command -v curl >/dev/null 2>&1; then
return 1
fi

local url="https://github.com/manaflow-ai/ghostty/releases/download/xcframework-${GHOSTTY_SHA}/GhosttyKit.xcframework.tar.gz"
if [[ ! -f "$GHOSTTYKIT_CHECKSUMS_FILE" ]]; then
echo "==> Missing GhosttyKit checksum manifest; falling back to local build." >&2
return 1
fi

local expected_sha
if ! expected_sha="$(lookup_pinned_ghosttykit_sha256 "$GHOSTTY_SHA" "$GHOSTTYKIT_CHECKSUMS_FILE" 2>/dev/null)"; then
echo "==> No pinned GhosttyKit checksum for ${GHOSTTY_SHA:0:12}; falling back to local build." >&2
return 1
fi

local tmp_dir tmp_tar tmp_extract actual_sha
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.

echo "==> Fetching prebuilt GhosttyKit.xcframework for ${GHOSTTY_SHA:0:12}..."
if ! curl -fSL --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 2 --retry-all-errors -o "$tmp_tar" "$url"; then
rm -rf "$tmp_dir"
echo "==> Prebuilt xcframework not available; falling back to local build."
return 1
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.

actual_sha="$(hash_file "$tmp_tar")"
if [[ "$actual_sha" != "$expected_sha" ]]; then
rm -rf "$tmp_dir"
echo "==> Prebuilt xcframework checksum mismatch; falling back to local build." >&2
echo " expected: $expected_sha" >&2
echo " actual: $actual_sha" >&2
return 1
fi

if ! python3 "$GHOSTTYKIT_ARCHIVE_VALIDATOR" "$tmp_tar"; then
rm -rf "$tmp_dir"
echo "==> Prebuilt xcframework archive failed validation; falling back to local build." >&2
return 1
fi

if ! tar --no-same-owner -xzf "$tmp_tar" -C "$tmp_extract"; then
rm -rf "$tmp_dir"
echo "==> Failed to extract verified prebuilt xcframework; falling back to local build." >&2
return 1
fi

local extracted="$tmp_extract/GhosttyKit.xcframework"
if [[ ! -d "$extracted" ]]; then
rm -rf "$tmp_dir"
echo "==> Prebuilt archive did not contain GhosttyKit.xcframework; falling back." >&2
return 1
fi

mkdir -p "$(dirname "$LOCAL_XCFRAMEWORK")"
rm -rf "$LOCAL_XCFRAMEWORK"
mv "$extracted" "$LOCAL_XCFRAMEWORK"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
rm -rf "$tmp_dir"
echo "$GHOSTTY_KEY" > "$LOCAL_KEY_STAMP"
echo "$GHOSTTY_SHA" > "$LEGACY_LOCAL_SHA_STAMP"
return 0
}

if [[ -d "$CACHE_XCFRAMEWORK" ]]; then
echo "==> Reusing cached GhosttyKit.xcframework"
else
Expand All @@ -116,6 +213,8 @@ else

if [[ -d "$LOCAL_XCFRAMEWORK" && "$LOCAL_KEY" == "$GHOSTTY_KEY" ]]; then
echo "==> Seeding cache from existing local GhosttyKit.xcframework (build key matches)"
elif try_fetch_prebuilt_xcframework; then
echo "==> Seeding cache from prebuilt GhosttyKit.xcframework"
else
echo "==> Building GhosttyKit.xcframework (this may take a few minutes)..."
(
Expand Down
41 changes: 40 additions & 1 deletion scripts/reload.sh
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,47 @@ fi
XCODEBUILD_ARGS+=(build)

XCODE_LOG="/tmp/cmux-xcodebuild-${TAG_SLUG}.log"
XCODEBUILD_LOCK="${TMPDIR:-/tmp}/cmux-xcodebuild-$(id -u).lock"
# Xcode 26's SWBBuildService is a per-user singleton. Concurrent xcodebuild
# invocations (even with separate -derivedDataPath) share that daemon and can
# crash it, SIGTERMing in-flight builds. Serialize via a per-user lock so
# parallel reload.sh runs queue instead of trampling each other.
set +e
xcodebuild "${XCODEBUILD_ARGS[@]}" 2>&1 | tee "$XCODE_LOG" | grep -E '(warning:|error:|fatal:|BUILD FAILED|BUILD SUCCEEDED|\*\* BUILD)'
python3 -c '
import fcntl
import os
import sys

lock_path = sys.argv[1]
command = sys.argv[2:]

try:
fd = os.open(lock_path, os.O_CREAT | os.O_RDWR, 0o600)
except OSError as exc:
raise SystemExit(f"error: open lock: {exc}")

try:
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
fcntl.fcntl(fd, fcntl.F_SETFD, flags & ~fcntl.FD_CLOEXEC)
except OSError as exc:
raise SystemExit(f"error: fcntl lock fd: {exc}")

try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
print(f"==> Another xcodebuild is running; waiting for {lock_path}...", file=sys.stderr, flush=True)
Comment thread
lawrencecchen marked this conversation as resolved.
try:
fcntl.flock(fd, fcntl.LOCK_EX)
except OSError as exc:
raise SystemExit(f"error: flock: {exc}")
except OSError as exc:
raise SystemExit(f"error: flock: {exc}")

try:
os.execvp(command[0], command)
except OSError as exc:
raise SystemExit(f"error: exec: {exc}")
' "$XCODEBUILD_LOCK" xcodebuild "${XCODEBUILD_ARGS[@]}" 2>&1 | tee "$XCODE_LOG" | grep -E '(warning:|error:|fatal:|BUILD FAILED|BUILD SUCCEEDED|\*\* BUILD|^==> )'
XCODE_PIPESTATUS=("${PIPESTATUS[@]}")
set -e
XCODE_EXIT="${XCODE_PIPESTATUS[0]}"
Expand Down
49 changes: 49 additions & 0 deletions scripts/validate-xcframework-archive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python3

from pathlib import PurePosixPath
import sys
import tarfile


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


def main() -> None:
if len(sys.argv) != 2:
raise SystemExit("usage: validate-xcframework-archive.py <archive>")

archive = sys.argv[1]
with tarfile.open(archive, "r:gz") as tar:
saw_root = False
for member in tar.getmembers():
name = normalize(member.name)
if not is_safe_member(name):
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

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}")
Comment on lines +35 to +40
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.

elif not (member.isfile() or member.isdir()):
raise SystemExit(f"unsupported archive member: {member.name}")

if not saw_root:
raise SystemExit(f"archive missing {ROOT}")


if __name__ == "__main__":
main()
Loading