On an Apple Silicon Mac, a freshly compiled arm64 binary dies immediately with "Killed: 9" while the same source runs fine on an Intel Mac. What is happening?
answer
- dies before main runs
- architecture-specific platform rule
- something rewrote the binary
- ad-hoc signing is enough
- sign as the final build step
basics
~20 sApple Silicon requires every arm64 executable to carry a valid code signature. The kernel refuses to execute unsigned or signature-broken arm64 code and kills the process with SIGKILL at exec, before any of its code runs. An ad-hoc signature satisfies the requirement.
solid answer
~50 sOn Apple Silicon the code-signature requirement is not a Gatekeeper policy you can wave through — it is enforced by the kernel at every `exec` of arm64 code, and on x86_64 Macs there was no equivalent rule. If the signature is missing or invalid, the process is killed with SIGKILL before `main` runs, which is why you see "Killed: 9" with no output, no crash log from your program, and nothing in your own logging. Two things commonly cause it: a build that never signed at all, and a build that signed and then modified the binary afterwards — `strip`, `install_name_tool` and anything that rewrites load commands invalidate the seal. The requirement is weak: an ad-hoc signature from `codesign -s -` is enough, since the rule is "must be signed", not "must be signed by a known developer". Re-sign as the last step of the build.
code
bash · 10 lines# Reproduce: sign, then modify, then run
clang -o mytool mytool.c
codesign --force --sign - mytool
install_name_tool -add_rpath @executable_path/../lib mytool # breaks the seal
./mytool # zsh: killed ./mytool
# Diagnose and fix
codesign --verify --verbose=2 mytool
codesign --force --sign - mytool # re-sign as the LAST step
./mytoolgo deeper
Know that Apple Silicon Macs will not run unsigned arm64 binaries at all, and that codesign -s - applies the minimal ad-hoc signature that lets a locally built tool start.
Explain that the check is in the kernel at exec rather than in Gatekeeper, so it produces SIGKILL with no dialog and no output, and that the requirement is only that a valid signature exists.
Trace it to the build: name the post-signing steps that invalidate a seal, verify with codesign --verify, order signing last and sign bundles inside-out, and separate this failure from a Gatekeeper prompt or an architecture mismatch.
Own the release chain: where ad-hoc signing is acceptable for internal tooling versus where Developer ID and notarization are mandatory, how signing identities are protected in CI, and how the pipeline verifies artifacts so a broken seal fails the build rather than the user.
## Two different signature checks, easily confused macOS applies code-signature checks at two very different altitudes, and mixing them up is the reason this failure baffles people. 1. **Gatekeeper** evaluates *quarantined* downloads on first launch and asks about provenance: a Developer ID, a notarization ticket. It produces a user-facing dialog, and it can be bypassed by clearing the quarantine attribute. 2. **Kernel signature enforcement on Apple Silicon** asks only "does this arm64 image carry a valid signature at all?" It applies to every execution, quarantined or not, downloaded or locally built. It produces no dialog, no prompt, and no bypass — the process is sent SIGKILL during `exec`. The second check is what kills a locally compiled binary. Because it fires before your code runs, none of your instrumentation helps: no `main`, no atexit handler, no log line. The shell reports `Killed: 9` and you are left with nothing. ## Why the seal breaks in real build pipelines Apple's toolchain applies an ad-hoc signature automatically when linking arm64 code, so "never signed" is actually the less common cause on a Mac. The frequent cause is **post-signing modification**. A code signature covers the bytes of the Mach-O image, so anything that rewrites it invalidates the seal: - `strip` removing symbols - `install_name_tool` rewriting an install name or an rpath — the classic step in relocatable-bundle scripts - Post-processing that patches a version string, appends data, or concatenates a payload - Copying a bundle in a way that drops extended attributes, or unarchiving with a tool that does not preserve them Everything works on the build machine right up to the point where the last packaging step runs, and only then does the artifact start dying — which is why the failure often shows up in CI rather than locally. ```sh $ ./mytool zsh: killed ./mytool $ codesign --verify --verbose=2 ./mytool ./mytool: invalid signature (code or signature have been modified) $ codesign --force --sign - ./mytool # ad-hoc re-sign $ ./mytool hello ``` `codesign -s -` is the ad-hoc identity: it computes and attaches the hashes with no certificate and no identity attached. The kernel requirement is satisfied because a valid signature exists; nothing is claimed about who produced it. That is the right fix for local development and internal tools, and explicitly *not* a substitute for Developer ID signing plus notarization on anything you distribute. ## The correct ordering The rule that falls out of this is simple and worth stating in an interview: **signing is the last step**. Any tool that touches the binary must run before it, and the signature must be re-applied after any change. In a bundle, sign inside-out — nested frameworks and helper tools first, then the outer bundle, since the outer signature seals its contents. ## Adjacent symptoms that look the same A process that dies at startup with SIGKILL and no output is also produced by a couple of neighbouring conditions, and a good answer distinguishes them: - A **library** with a broken or mismatched signature can fail to load into a process running with library validation, which produces a linker-level failure naming the library rather than a bare kill of your executable. - A **quarantined** binary with a bad signature produces a Gatekeeper dialog, not a silent kill — the presence or absence of a dialog is a useful discriminator. - The system log subsystem records signature-validation failures with the offending path, which is the fastest way to confirm rather than guess when the shell output is empty. ## Why interviewers ask it This is a question you can only answer well if you have shipped or built something on Apple Silicon. The wrong answers are recognisable: blaming the compiler, blaming architecture mismatch (which produces a clear "bad CPU type" error, not a kill), or reaching for SIP. The right answer identifies a *platform invariant* — arm64 code must be signed — and traces the failure to the build step that broke the seal.
- Why does clearing the quarantine attribute not fix this?Because the quarantine attribute only controls whether Gatekeeper assesses provenance on first launch. The kill here comes from the kernel's requirement that arm64 code carry a valid signature, which applies to every execution of every binary regardless of where it came from. A locally compiled file has no quarantine attribute in the first place, and it still dies.
- An ad-hoc signature carries no identity. Why does the system accept it?The two requirements are separate. The kernel requires that the image be *sealed*, so its pages can be verified as unmodified while it runs — that works with a hash-only ad-hoc signature. Trusting the *origin* is a distinct question answered by Developer ID and notarization, and only Gatekeeper asks it. Ad-hoc satisfies integrity, not provenance.
- How should a build script that rewrites load commands be ordered?Every mutation first, signing last. Run `install_name_tool`, `strip` and any patching step, then sign nested frameworks and helper executables, then sign the enclosing bundle so the outer signature seals the final contents. Finish with `codesign --verify --strict` in CI so a broken seal fails the pipeline instead of shipping.
- How would you confirm the diagnosis when the shell prints nothing but "Killed: 9"?Run `codesign --verify --verbose=2` on the binary — it reports plainly whether the signature is missing or whether code or signature have been modified. Cross-check the system log around the launch, which records signature validation failures with the offending path. Between the two you get a definitive answer rather than inferring from an empty terminal.
saying these in an interview costs you the question
- Blaming Gatekeeper or the quarantine attribute
- Suggesting chmod +x or sudo will fix it
- Thinking an architecture mismatch causes a silent kill
- Re-signing before the tool that rewrites the binary
- Assuming ad-hoc signing is enough for distribution