Skip to content

Strip any embedded framework without a valid dynamic-library executable - #8245

Merged
azooz2003-bit merged 1 commit into
mainfrom
feat-internal-beta-unblock-v2
Jul 16, 2026
Merged

azooz2003-bit merged 1 commit into
mainfrom
feat-internal-beta-unblock-v2

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 16, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #8235. Build 20260716043221 (run https://github.com/manaflow-ai/cmux/actions/runs/29471458156, which contained the ar-archive-only strip) was still rejected with ITMS-90208: Xcode's export-time processing can remove a static SPM binaryTarget's executable and leave an invalid framework shell (Info.plist, no binary) in Frameworks/, which the [[ -f ]] guard skipped.

The keep policy is now a whitelist: an embedded framework stays only if its executable exists and is a dynamically linked Mach-O. Static archives, stripped shells, and anything else are removed (gated on the app executable not referencing the framework in its load commands), the pre-strip Frameworks/ state is logged for ground truth, and the final-IPA verifier enforces the same whitelist for the automatic-signing path. Verified locally against the real iroh-ffi 1.0.2-cmux.2 static artifact, a binary-less shell, and a real dylib.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Prevent App Store Connect ITMS-90208 by only keeping embedded frameworks that have a valid, dynamically linked executable; everything else is stripped with clear logging. The same whitelist is enforced during IPA verification to catch issues in both manual and automatic signing paths.

  • Bug Fixes
    • Keep an embedded framework only if its CFBundleExecutable exists and is a Mach-O dynamic library; strip static archives, binary-less shells, and other non-dylib blobs.
    • Before stripping, log Frameworks/ contents and each framework’s file type; remove an empty Frameworks/ directory afterward.
    • Check the app binary with otool -L and fail if it links an invalid framework we would otherwise strip.
    • Apply the same whitelist in the final IPA verifier for automatic-signing uploads.

Written for commit 834874d. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved validation of embedded iOS frameworks during TestFlight uploads.
    • Uploads now fail clearly when framework executables are missing or invalid.
    • Manual re-signing removes unsupported frameworks more reliably while preventing removal of frameworks required by the app.
    • Added clearer validation logging and cleanup of empty framework directories.

Build 20260716043221 was still rejected with ITMS-90208 after the
ar-archive-only strip: Xcode's export-time distribution processing can
remove a static SPM binaryTarget's executable and leave an invalid
framework shell (Info.plist, no binary) in Frameworks/, which the
previous check's [[ -f ]] guard skipped.

The keep policy is now a whitelist: an embedded framework stays only if
its executable (per its own CFBundleExecutable) exists and is a
dynamically linked Mach-O; static archives, stripped shells, and any
other blob are removed, gated on the app executable not referencing the
framework in its dynamic load commands. The pre-strip Frameworks/ state
and each framework's file(1) kind are logged so any future ASC
rejection comes with ground truth. verify_ipa_framework_minimum_os_versions
enforces the same whitelist on the final IPA for the automatic-signing
path. Verified locally against all three states (static archive from the
real iroh-ffi 1.0.2-cmux.2 artifact, shell without binary, real dylib).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 16, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b196ae4c-f6a2-4db6-88dc-ed6bb18a3fe5

📥 Commits

Reviewing files that changed from the base of the PR and between b129a86 and 834874d.

📒 Files selected for processing (1)
  • ios/scripts/upload-testflight.sh

📝 Walkthrough

Walkthrough

The upload script now resolves embedded framework executables from CFBundleExecutable, requires dynamically linked shared libraries, and applies a whitelist-based stripping policy during manual re-signing with dependency checks and empty-directory cleanup.

Changes

iOS framework handling

Layer / File(s) Summary
Framework executable validation
ios/scripts/upload-testflight.sh
Framework executable paths use CFBundleExecutable with a basename fallback, missing executables fail validation, and non-dynamic binaries are rejected.
Manual re-sign framework stripping
ios/scripts/upload-testflight.sh
Manual re-signing retains only dynamically linked framework binaries, logs classifications, rejects stripped frameworks still linked by the app executable, and removes an empty Frameworks/ directory.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#8147: Updates the same upload script with post-export validation for missing framework MinimumOSVersion.
  • manaflow-ai/cmux#8235: Modifies framework validation and manual re-signing behavior based on dynamic-library classification.

Suggested reviewers: lawrencecchen

✨ 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 feat-internal-beta-unblock-v2

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.

@azooz2003-bit
azooz2003-bit merged commit 3bffdde into main Jul 16, 2026
5 of 6 checks passed
@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR closes a gap in the static-framework strip guard for the manual-resign TestFlight path: Xcode's export-time distribution processing can fully remove a static SPM binaryTarget's executable and leave an invalid framework shell (Info.plist with no binary), which the previous [[ -f ]] && grep 'ar archive' guard skipped over. The fix replaces the blacklist with a whitelist — an embedded framework is kept only when its executable exists and file(1) identifies it as a dynamically linked Mach-O — and back-fills the same whitelist into the IPA post-verifier used by the automatic-signing path.

  • Strip loop (manual-resign path): Reads CFBundleExecutable from each framework's Info.plist to locate the binary, captures the file -b output once into embedded_fw_kind, keeps dylibs, and strips everything else after confirming via otool -L that the app executable doesn't reference the framework dynamically. An empty Frameworks/ directory is removed after stripping.
  • Verifier (verify_ipa_framework_minimum_os_versions): Now enforces the same dylib whitelist for the automatic-signing path, failing hard on missing-binary shells as well as non-dylib executables.
  • Logging: Adds a find-based snapshot of Frameworks/ before stripping to provide ground truth for future ASC rejection analysis.

Confidence Score: 4/5

Safe to merge; the whitelist-based strip and verifier correctly handle all three invalid-framework cases (static ar, missing binary, non-dylib Mach-O) and the otool -L safety gate prevents stripping a framework the app executable genuinely links against.

The logic of the fix is sound: replacing the blacklist with a whitelist closes the shell-framework gap that slipped past the previous guard. The strip loop and the verifier now use the same string ("dynamically linked shared library") and the same PlistBuddy-driven binary-path resolution. The only notable roughness is the double file -b invocation in the verifier error path — a style inconsistency with the strip loop, which already captures the output in a variable, but not a correctness issue.

ios/scripts/upload-testflight.sh — specifically the verifier function around lines 168–172 where file -b is called twice.

Important Files Changed

Filename Overview
ios/scripts/upload-testflight.sh Replaces the blacklist (ar-archive-only) framework-strip guard with a whitelist (dylib-only) in both the manual-resign strip loop and the IPA verifier, adds pre-strip Frameworks/ logging, and cleans up an empty Frameworks/ dir after stripping; one minor double-invocation of file -b in the verifier error path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Embedded .framework found] --> B[Read CFBundleExecutable from Info.plist]
    B --> C{Binary file exists?}
    C -- No --> D[embedded_fw_kind = &lt;executable missing&gt;]
    C -- Yes --> E[file -b binary → embedded_fw_kind]
    D --> F{Is dynamically linked shared library?}
    E --> F
    F -- Yes --> G[vtool -show-build → log build info]
    G --> H[Keep framework, continue]
    F -- No --> I{otool -L app_exe references framework?}
    I -- Yes --> J[ERROR: refusing to strip — exit 1]
    I -- No --> K[Strip: rm -rf embedded_fw]
    K --> L[After loop: rmdir Frameworks/ if empty]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Embedded .framework found] --> B[Read CFBundleExecutable from Info.plist]
    B --> C{Binary file exists?}
    C -- No --> D[embedded_fw_kind = &lt;executable missing&gt;]
    C -- Yes --> E[file -b binary → embedded_fw_kind]
    D --> F{Is dynamically linked shared library?}
    E --> F
    F -- Yes --> G[vtool -show-build → log build info]
    G --> H[Keep framework, continue]
    F -- No --> I{otool -L app_exe references framework?}
    I -- Yes --> J[ERROR: refusing to strip — exit 1]
    I -- No --> K[Strip: rm -rf embedded_fw]
    K --> L[After loop: rmdir Frameworks/ if empty]
Loading

Reviews (1): Last reviewed commit: "Strip any embedded framework without a v..." | Re-trigger Greptile

Comment on lines +168 to 172
if ! file -b "$framework_binary" | grep -q 'dynamically linked shared library'; then
echo "error: $framework_name is embedded in the app bundle but its executable is not a dynamic library ($(file -b "$framework_binary")); ASC rejects this (ITMS-90208). Strip it from Frameworks/ (static code is already linked into the app executable)." >&2
rm -rf "$workdir"
return 1
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 file -b invoked twice in the error path — once to drive the grep check and again inside the error string. If the framework binary is a large or slow-to-probe file this doubles the I/O, but more importantly the two calls happen at slightly different times and could in theory produce different output (e.g. on a modified-during-scan temp directory). Capturing the output once in a variable — as the strip loop already does with embedded_fw_kind — keeps the verifier consistent with the pattern established 30 lines above and avoids the double stat.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

1 participant