macosclangmach-ootoolosx-gatekeeper

Why does macOS kill static executables created by clang?


I have a minimal c program for the m1 arm cpu that returns 42:

void _start() {
    asm("mov x0, #42;");
    asm("mov x16, #1;");
    asm("svc 0x80;");
}

This code compiles after telling clang to use the _start symbol and returns the correct value.

clang -Wl,-e, -Wl,__start test.c -o dyn.out
./dyn.out ; echo $?
42

However this binary still has dynamic links according to otool:

otool -L ./dyn.out 
./dyn.out:
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.100.3)

After telling clang to produce a unsigned static binary however macOS then immediately kills the binary when trying to run.

clang -Wl,-e, -Wl,__start -static -nostdlib test.c -o static_no_sign.out
zsh: killed     ./static_no_sign.out

Signing the binary before running also produces the same problem.

clang -Wl,-e, -Wl,__start -static -nostdlib test.c -o static_sign.out 
codesign -s - static_sign.out 
./static_sign.out 
zsh: killed     ./static_sign.out

The following messages are produced in Console:

taskgated: UNIX error exception: 3
taskgated: no signature for pid=93166 (cannot make code: UNIX[No such process])

But codesign can verify the signature

codesign -v -v static_sign.out 
static_sign.out: valid on disk
static_sign.out: satisfies its Designated Requirement

Can anyone clarify why macOS is deciding to kill the clang produced binaries?


Solution

  • Because static binaries are explicitly disallowed on any architecture other than x86_64.
    XNU contains this code piece in the Mach-O loader:

    case MH_EXECUTE:
        if (depth != 1 && depth != 3) {
            return LOAD_FAILURE;
        }
        if (header->flags & MH_DYLDLINK) {
            /* Check properties of dynamic executables */
            if (!(header->flags & MH_PIE) && pie_required(header->cputype, header->cpusubtype & ~CPU_SUBTYPE_MASK)) {
                return LOAD_FAILURE;
            }
            result->needs_dynlinker = TRUE;
        } else if (header->cputype == CPU_TYPE_X86_64) {
            /* x86_64 static binaries allowed */
        } else {
            /* Check properties of static executables (disallowed except for development) */
    #if !(DEVELOPMENT || DEBUG)
            return LOAD_FAILURE;
    #endif
        }
        break;
    

    If you do the exact same thing on x86_64, it works:

    void _start()
    {
        __asm__ volatile
        (
            ".intel_syntax noprefix\n"
            "mov eax, 0x2000001\n"
            "mov edi, 42\n"
            "syscall"
        );
    }
    
    % clang -Wl,-e,__start -static -nostdlib t.c -o t -arch x86_64
    % ./t
    % echo $?
    42