[Chrome] arm64: Copying a new Mach-O executable to an inode that previously contained a different Mach-O executable produces something that incorrectly fails code signature verification and won’t run

Originator:mark
Number:rdar://FB8914243 Date Originated:2020-11-23
Status:Open Resolved:
Product:macOS Product Version:11.0.1 20B29
Classification:Application Crash Reproducible:Always
 
If a vnode contains a Mach-O executable, replacing the contents with a different Mach-O executable produces a file that incorrectly fails code signature verification in xnu and won’t run. This happens when “kill” semantics are enforced, which is the default on arm64.

Simplified test case:

mark@arm-and-hammer zsh% rm -f /tmp/test
mark@arm-and-hammer zsh% cp /usr/bin/false /tmp/test; echo $?
mark@arm-and-hammer zsh% /tmp/test
1
mark@arm-and-hammer zsh% cp /usr/bin/true /tmp/test; echo $?
mark@arm-and-hammer zsh% /tmp/test
zsh: killed     /tmp/test
137

Detailed test case:

1. Create a Mach-O executable and make sure that xnu is aware of it and its code signature by running it. Note its inode number, modification timestamp, and code directory hash.

In this example, I’m using my own executables for “true” and “false” in place of the ones that ship in /usr/bin, because the kernel produces a more valuable log in this case. However, per the simplified test case above, the same behavior (minus the log messages) occurs when using the prebuilt /usr/bin/true and /usr/bin/false in place of the compiled /tmp/true and /tmp/false.

mark@arm-and-hammer zsh% rm -f /tmp/test
mark@arm-and-hammer zsh% clang -x c - -o /tmp/false <<< 'int main() { return 1; }'
mark@arm-and-hammer zsh% cp /tmp/false /tmp/test
mark@arm-and-hammer zsh% stat -f '%i %Fm' /tmp/test
6951173 1606151966.467082817
mark@arm-and-hammer zsh% codesign --display --verbose=5 /tmp/test 2>&1 | grep ^CDHash=
CDHash=a66099928a3c71e1e76865b010bacd46b5c53358
mark@arm-and-hammer zsh% /tmp/test; echo $?
1

2. Replace the contents of the file with a different executable. Verify that the inode has remained the same.

mark@arm-and-hammer zsh% clang -x c - -o /tmp/true <<< 'int main() { return 0; }'
mark@arm-and-hammer zsh% cp /tmp/true /tmp/test
mark@arm-and-hammer zsh% stat -f '%i %Fm' /tmp/test
6951173 1606152024.837585424
mark@arm-and-hammer zsh% codesign --display --verbose=5 /tmp/test 2>&1 | grep ^CDHash=
CDHash=485dec78a904ee3c8301d10037caebe65feec83d
mark@arm-and-hammer zsh% /tmp/test; echo $?

Expected behavior:

The executable should run.

mark@arm-and-hammer zsh% /tmp/test; echo $?
0

Observed behavior:

The executable does not run. It’s killed by the kernel with a SIGKILL.

mark@arm-and-hammer zsh% /tmp/test; echo $?
zsh: killed     /tmp/test
137

Observing the system log during the above test, these messages are visible:

admin@arm-and-hammer zsh% sudo log stream --predicate 'sender = "kernel"'
Filtering the log data using "sender == "kernel""
Timestamp                       Thread     Type        Activity             PID    TTL  
2020-11-23 12:20:36.817603-0500 0x1077     Default     0x0                  0      0    kernel: ignoring detached code signature on 'test' with cdhash 'a66099928a3c71e1e76865b010bacd46b5c53358' because it is invalid, or not a simple adhoc signature.
2020-11-23 12:20:36.825652-0500 0x1078     Default     0x0                  0      0    kernel: CODE SIGNING: process 398[test]: rejecting invalid page at address 0x104b44000 from offset 0x0 in file "/private/tmp/test" (cs_mtime:1606151966.467082817 != mtime:1606152024.837585424) (signed:1 validated:1 tainted:1 nx:0 wpmapped:0 dirty:0 depth:0)

Of note:

a66099928a3c71e1e76865b010bacd46b5c53358 is the CDHash from /tmp/false and the original version of /tmp/test. It is not the CDHash of the version of /tmp/test that I was expecting to run the second time (485dec78a904ee3c8301d10037caebe65feec83d)

1606151966.467082817 is the mtime of the original version of /tmp/test, but not the mtime of the version of /tmp/test that I was expecting to run the second time (1606152024.837585424).

System information:

mark@arm-and-hammer zsh% sw_vers
ProductName:	macOS
ProductVersion:	11.0.1
BuildVersion:	20B29
mark@arm-and-hammer zsh% xcodebuild -version
Xcode 12.2
Build version 12B45b
mark@arm-and-hammer zsh% uname -m
arm64
mark@arm-and-hammer zsh% system_profiler SPHardwareDataType | grep 'Model Identifier'
      Model Identifier: ADP3,2

This occurs on shipping M1-based hardware as well.

To reproduce on x86_64, set the “kill” flag in the code signature.

mark@sweet16 zsh% rm -f /tmp/test
mark@sweet16 zsh% clang -x c - -o /tmp/false <<< 'int main() { return 1; }'
mark@sweet16 zsh% codesign --sign=- --options=kill /tmp/false
mark@sweet16 zsh% cp /tmp/false /tmp/test
mark@sweet16 zsh% /tmp/test; echo $?                                      
1
mark@sweet16 zsh% clang -x c - -o /tmp/true <<< 'int main() { return 0; }'
mark@sweet16 zsh% codesign --sign=- --options=kill /tmp/true              
mark@sweet16 zsh% cp /tmp/true /tmp/test
mark@sweet16 zsh% /tmp/test; echo $?                        
zsh: killed     /tmp/test
137

mark@sweet16 zsh% sw_vers
ProductName:	Mac OS X
ProductVersion:	10.15.7
BuildVersion:	19H15
mark@sweet16 zsh% xcodebuild -version
Xcode 12.2
Build version 12B45b
mark@sweet16 zsh% uname -m
x86_64
mark@sweet16 zsh% system_profiler SPHardwareDataType | grep 'Model Identifier'
      Model Identifier: MacBookPro16,1

Additional information:

Feedback FB8914231 is related. Unlike in that report, purge and msync(…, MS_INVALIDATE) are not viable workarounds here.

This bug was discovered during the course of building and testing open-source llvm/clang on mac-arm64, tracked at https://bugs.llvm.org/show_bug.cgi?id=46644#c7.

Comments


Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!