Engineering Practice

Diagnosing iOS signing failures caused by extended attributes

Diagnosing iOS signing failures caused by extended attributes

Archiving succeeds locally, but the same project fails during signing on a cloud Mac with resource fork, Finder information, or similar detritus not allowed. This type of failure is usually not caused by an expired certificate or provisioning profile. More often, an image, script-generated artifact, or copied directory carries macOS extended attributes. Those attributes follow the resource into the App bundle and remain unnoticed until the signing tool performs its strict checks.

Identify which layer is failing

Do not rebuild certificates as soon as you see a signing failure. Start by locating the first codesign error in the complete build log, and record whether the failing object is the main App, a Framework, an extension, or a resource bundle. If the log explicitly mentions FinderInfo, ResourceFork, or detritus, focus the investigation on file metadata.

Extended attributes commonly originate from:

  • Images, fonts, or archives copied into the repository with a graphical file manager;
  • Resources committed immediately after being extracted from a shared directory;
  • Pre-generated directories moved by build scripts using cp -R;
  • Local artifacts that are not under version control but have remained in the working directory;
  • Dependencies restored from cache together with their file metadata.

Deleting DerivedData only removes build output. If the contamination is in the source tree or an external input, the next build will fail in exactly the same way.

Save the failed command, target path, and relevant logs before cleaning anything. Otherwise, even if the build happens to recover, you will not be able to identify the actual source of the contamination.

Establish a read-only scanning baseline

Scan repository inputs

xattr -lr recursively lists paths and their attributes. Write the results to a log first instead of deleting attributes while scanning:

set -euo pipefail

ROOT="${SRCROOT:-$PWD}"
REPORT="${TMPDIR:-/tmp}/ios-xattr-source.log"

xattr -lr "$ROOT" > "$REPORT" 2>&1 || true

grep -E \
  'com\.apple\.(FinderInfo|ResourceFork)' \
  "$REPORT" || true

For a large repository, exclude .git, DerivedData, and dependency caches, then inspect each directory that actually participates in packaging. The number of matches is less important than answering three questions: which file carries the attribute, what produced that file, and which Build Phase copied it into the bundle.

Match location Check first Remediation principle
Assets or resource directory Import method, extraction tool Fix the source file
Script output directory cp and ditto options Rebuild the staging directory
DerivedData Cache restoration, stale artifacts Clear the cache and reproduce
Inside a Framework Download and extraction steps Fetch it again and validate it

Scan the archived bundle

If the archive has already been created, scan the .app directly to confirm whether the contamination reached the deliverable:

ARCHIVE_PATH="$PWD/build/App.xcarchive"
APP_PATH="$ARCHIVE_PATH/Products/Applications/App.app"
REPORT="$PWD/build/xattr-app.log"

test -d "$APP_PATH"
xattr -lr "$APP_PATH" > "$REPORT" 2>&1 || true

if grep -Eq 'com\.apple\.(FinderInfo|ResourceFork)' "$REPORT"; then
  echo "forbidden extended attributes found"
  exit 1
fi

This example uses a fixed path. In a production pipeline, resolve the single .app from the archive directory and fail immediately if no candidate or multiple candidates are found. This prevents the pipeline from checking the wrong bundle.

Preserve traceability while cleaning

Running xattr -cr across the entire workspace should not be the first response. It removes every extended attribute, unnecessarily broadens the scope of changes, and may conceal defects in download, extraction, or cache-restoration steps. A safer approach is to remove only the two attributes already confirmed to break signing:

TARGET="$PWD/StagingPayload"

find "$TARGET" -print0 |
while IFS= read -r -d '' item; do
  xattr -d com.apple.FinderInfo "$item" 2>/dev/null || true
  xattr -d com.apple.ResourceFork "$item" 2>/dev/null || true
done

Ideally, TARGET should point to a disposable staging copy rather than a developer's original workspace. Scan it again after cleaning. If any match remains, stop the build; do not append || true to the final acceptance check.

If the attributes originate from an archive, fix the validation step that runs after extraction. If they come from a build script, make the script create an empty staging directory on every run and copy only an explicit file list. Adding a recursive cleanup command only at the end leaves the contamination in place and allows it to recur in other jobs.

Integrate the checks into the archive workflow

Check inputs before building

The pre-build gate only needs to scan resource directories that will enter the App; it does not need to scan the entire home directory. When a match is found, print the relative path and attribute name, then exit with a nonzero status. This makes the job fail before the time-consuming archive begins and helps identify the most recent change.

Keep the checking script under version control and make its runtime environment deterministic:

  1. Resolve the repository root from the script's own location;
  2. Use null delimiters for paths containing spaces;
  3. Write reports to an artifact directory dedicated to the current job;
  4. Do not modify files during scanning;
  5. Fail immediately if a target directory does not exist.

Verify signatures after archiving

After the extended-attribute scan passes, use the system signing tool to verify the archive:

APP_PATH="$PWD/build/App.xcarchive/Products/Applications/App.app"

codesign --verify \
  --strict \
  --verbose=2 \
  "$APP_PATH"

If the App contains standalone Frameworks or extensions, traverse and verify each nested code object separately. Do not assume internal components are valid merely because the main bundle passes. Save the verification log, archive path, and source commit identifier together so that you can distinguish cases where the source is identical but cached inputs differ.

Close the investigation with an acceptance checklist

A reliable fix should satisfy all of the following conditions:

  • Scan results for source code and external inputs have been archived;
  • The path that produced or copied the contaminated file has been identified;
  • Cleaning affects only the identified attributes or a disposable staging copy;
  • The project can be archived again after clearing DerivedData and related caches;
  • The final App bundle contains neither FinderInfo nor ResourceFork;
  • The main App and all nested code objects pass strict signature verification;
  • The same commit passes repeatedly in a clean working directory.

Extended-attribute failures are difficult to diagnose not because the commands are complicated, but because the problem crosses four layers: source code, filesystem metadata, caches, and signing. Once “record first, locate next, clean afterward, and validate at the end” becomes a mandatory gate, these failures no longer depend on someone logging into a node for an ad hoc fix, and the evidence will not disappear when the working directory changes.

Frequently asked questions

Why can the signing error remain after deleting DerivedData?

If FinderInfo or ResourceFork is attached to a source asset, downloaded input, or directory copied by a script, every clean build imports it again. Scan the inputs before rebuilding DerivedData.

Should I run xattr -cr across the entire repository?

Usually not. Record the affected paths first, then remove only FinderInfo and ResourceFork. Use recursive removal of every attribute only inside a disposable staging copy.

How should I validate the archive after cleanup?

Scan the App bundle for the targeted attributes and run codesign --verify --strict. Keep the scan log, verification output, and archive checksum as the acceptance record.

NowMini M4

Run your next task on a dedicated physical node

Use a cloud Mac with M4, 16GB RAM, and a 256GB SSD. Choose a node in Singapore, Tokyo, Seoul, or Hong Kong based on your project schedule.

Rent a cloud Mac now