Skip to content

Commit 0cc11d1

Browse files
committed
See which machine a Mach-O is for, not just that it is one
The format guard exists because a staged build assembles one SDK out of two machines, and a file belonging to the producer can reach the consumer's tree without anything else noticing. On Apple it could not see that: both machines emit Mach-O, so arm64 and x86_64 were the same answer -- and the one host pair staged across two architectures, x86_64-apple-darwin cross-built on the arm64 runner, was exactly the blind spot. The cputype sits four bytes behind the magic, in the byte order the magic announces. A universal binary carries one per slice and none of its own, so it stays unclassified rather than guessed at. The test drives the check with the real cmake binary against the triplet of the machine it is not, which exercises the Mach-O path on macOS and the ELF one on Linux, and the helper it calls now takes the host to check against instead of hardcoding Windows.
1 parent a90b859 commit 0cc11d1

3 files changed

Lines changed: 87 additions & 7 deletions

File tree

cmake/HostBinaryFormat.cmake

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
include_guard(GLOBAL)
1313

1414
# Decode the leading bytes of an executable, given as an uppercase hex string.
15-
# Sets format to ELF, PE, MachO or the empty string, and machine to the ELF
16-
# e_machine value (decimal) when the header carries one.
15+
# Sets format to ELF, PE, MachO or the empty string, and machine to whichever
16+
# machine number the header carries (decimal): the ELF e_machine, or the
17+
# Mach-O cputype. Empty when the format has no room for one.
1718
function(vitasdk_decode_binary_format hex out_format out_machine)
1819
set(format "")
1920
set(machine "")
@@ -38,6 +39,22 @@ function(vitasdk_decode_binary_format hex out_format out_machine)
3839
set(format PE)
3940
elseif(hex MATCHES "^(FEEDFACE|FEEDFACF|CEFAEDFE|CFFAEDFE|CAFEBABE)")
4041
set(format MachO)
42+
string(SUBSTRING "${hex}" 0 8 magic)
43+
string(LENGTH "${hex}" hex_length)
44+
# A universal binary carries one cputype per slice and none of its
45+
# own, so it is left unclassified rather than guessed at.
46+
if(hex_length GREATER_EQUAL 16 AND NOT magic STREQUAL "CAFEBABE")
47+
string(SUBSTRING "${hex}" 8 8 cpu_hex)
48+
if(magic MATCHES "^(CEFAEDFE|CFFAEDFE)$")
49+
# cputype is stored in the byte order the magic announced.
50+
string(SUBSTRING "${cpu_hex}" 0 2 cpu_byte0)
51+
string(SUBSTRING "${cpu_hex}" 2 2 cpu_byte1)
52+
string(SUBSTRING "${cpu_hex}" 4 2 cpu_byte2)
53+
string(SUBSTRING "${cpu_hex}" 6 2 cpu_byte3)
54+
set(cpu_hex "${cpu_byte3}${cpu_byte2}${cpu_byte1}${cpu_byte0}")
55+
endif()
56+
math(EXPR machine "0x${cpu_hex}")
57+
endif()
4158
endif()
4259

4360
set(${out_format} "${format}" PARENT_SCOPE)
@@ -58,6 +75,16 @@ function(vitasdk_expected_binary_format system_name host_triple out_format out_m
5875
set(format PE)
5976
elseif(system_name STREQUAL "Darwin")
6077
set(format MachO)
78+
# CPU_ARCH_ABI64 | CPU_TYPE_X86, and the same for CPU_TYPE_ARM. Both
79+
# Macs produce Mach-O, so this is the only thing that tells the two
80+
# halves of a staged Apple build apart.
81+
if(host_triple MATCHES "^(x86_64|amd64)")
82+
set(machine 16777223)
83+
elseif(host_triple MATCHES "^i[3-6]86")
84+
set(machine 7)
85+
elseif(host_triple MATCHES "^(aarch64|arm64)")
86+
set(machine 16777228)
87+
endif()
6188
else()
6289
set(format ELF)
6390
if(host_triple MATCHES "^(x86_64|amd64)")
@@ -101,8 +128,8 @@ function(vitasdk_check_binary_directory directory system_name host_triple)
101128
if(NOT expected_machine STREQUAL "" AND NOT machine STREQUAL ""
102129
AND NOT machine EQUAL expected_machine)
103130
message(FATAL_ERROR
104-
"${entry} is built for ELF machine ${machine}, but this SDK "
105-
"targets ${host_triple} (machine ${expected_machine})")
131+
"${entry} is built for ${format} machine ${machine}, but this "
132+
"SDK targets ${host_triple} (machine ${expected_machine})")
106133
endif()
107134
endforeach()
108135
endfunction()

tests/cmake/host-binary-format-check.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ cmake_minimum_required(VERSION 3.16)
55

66
include("${CMAKE_CURRENT_LIST_DIR}/../../cmake/HostBinaryFormat.cmake")
77

8-
vitasdk_check_binary_directory("${DIRECTORY}" Windows x86_64-w64-mingw32)
8+
vitasdk_check_binary_directory("${DIRECTORY}" "${SYSTEM}" "${TRIPLE}")

tests/cmake/host-binary-format.cmake

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ assert_decode("7F454C4601010100000000000000000002002800" ELF 40)
1919
# ELF big endian keeps e_machine in its own byte order.
2020
assert_decode("7F454C4602020100000000000000000000020016" ELF 22)
2121
assert_decode("4D5A90000300000004000000FFFF0000B8000000" PE "")
22-
assert_decode("CFFAEDFE0C000001000000000200000013000000" MachO "")
22+
# Mach-O 64, little endian: the cputype follows the magic in its byte order.
23+
assert_decode("CFFAEDFE0C000001000000000200000013000000" MachO 16777228)
24+
assert_decode("CFFAEDFE07000001030000000200000013000000" MachO 16777223)
25+
assert_decode("FEEDFACF010000070000000300000002000000A0" MachO 16777223)
26+
# A universal binary has a cputype per slice and none of its own.
27+
assert_decode("CAFEBABE0000000201000007000000030000C000" MachO "")
2328
# A shell script is in no executable format and must decode as nothing.
2429
assert_decode("23212F62696E2F73680A6563686F206869" "" "")
2530

@@ -33,7 +38,9 @@ function(assert_expected system triple expected_format expected_machine)
3338
endfunction()
3439

3540
assert_expected(Windows x86_64-w64-mingw32 PE "")
36-
assert_expected(Darwin arm64-apple-darwin MachO "")
41+
assert_expected(Darwin arm64-apple-darwin MachO 16777228)
42+
assert_expected(Darwin aarch64-apple-darwin MachO 16777228)
43+
assert_expected(Darwin x86_64-apple-darwin MachO 16777223)
3744
assert_expected(Linux x86_64-linux-gnu ELF 62)
3845
assert_expected(Linux i686-linux-gnu ELF 3)
3946
assert_expected(Linux aarch64-linux-musl ELF 183)
@@ -67,6 +74,7 @@ file(COPY "${real_cmake}" DESTINATION "${scratch}/bin")
6774
execute_process(
6875
COMMAND ${CMAKE_COMMAND}
6976
-DDIRECTORY=${scratch}/bin
77+
-DSYSTEM=Windows -DTRIPLE=x86_64-w64-mingw32
7078
-P "${CMAKE_CURRENT_LIST_DIR}/host-binary-format-check.cmake"
7179
RESULT_VARIABLE check_result
7280
OUTPUT_QUIET
@@ -79,4 +87,49 @@ if(NOT check_error MATCHES "in a PE SDK")
7987
endif()
8088
file(REMOVE_RECURSE "${scratch}")
8189

90+
# The machine half of the same failure, which format alone cannot see: both
91+
# Macs produce Mach-O, so on the one host pair that is staged across two
92+
# architectures nothing else would catch it. The fixture is the real cmake,
93+
# so the bytes are a machine's own rather than a hand-written header, and the
94+
# foreign triple is picked by what it is not -- script mode knows the host
95+
# system name but not its processor.
96+
set(foreign_triple "")
97+
if(NOT machine STREQUAL "")
98+
foreach(arch x86_64 aarch64)
99+
if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin")
100+
set(candidate ${arch}-apple-darwin)
101+
elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
102+
set(candidate ${arch}-linux-gnu)
103+
else()
104+
break()
105+
endif()
106+
vitasdk_expected_binary_format("${CMAKE_HOST_SYSTEM_NAME}" "${candidate}"
107+
candidate_format candidate_machine)
108+
if(NOT candidate_machine EQUAL machine)
109+
set(foreign_triple ${candidate})
110+
break()
111+
endif()
112+
endforeach()
113+
endif()
114+
if(NOT foreign_triple STREQUAL "")
115+
set(scratch "${temp_root}/vitasdk-host-binary-machine-${fixture_id}")
116+
file(COPY "${real_cmake}" DESTINATION "${scratch}/bin")
117+
execute_process(
118+
COMMAND ${CMAKE_COMMAND}
119+
-DDIRECTORY=${scratch}/bin
120+
-DSYSTEM=${CMAKE_HOST_SYSTEM_NAME}
121+
-DTRIPLE=${foreign_triple}
122+
-P "${CMAKE_CURRENT_LIST_DIR}/host-binary-format-check.cmake"
123+
RESULT_VARIABLE check_result
124+
OUTPUT_QUIET
125+
ERROR_VARIABLE check_error)
126+
if(check_result EQUAL 0)
127+
message(FATAL_ERROR "a binary for another machine must be rejected")
128+
endif()
129+
if(NOT check_error MATCHES "machine ${machine}" OR NOT check_error MATCHES "${foreign_triple}")
130+
message(FATAL_ERROR "the rejection must name the machine, got: ${check_error}")
131+
endif()
132+
file(REMOVE_RECURSE "${scratch}")
133+
endif()
134+
82135
message(STATUS "Host binary format checks passed")

0 commit comments

Comments
 (0)