fix(release): add missing Intel V8 signing entitlement (#30953)

## Why

Intel macOS release binaries crash on the first Code Mode tool call
while V8 creates its code range. The x86_64 V8 allocator later makes a
non-`MAP_JIT` reservation executable, which Hardened Runtime rejects
when the signature contains only `com.apple.security.cs.allow-jit`.

Tracks
[SE-8006](https://linear.app/openai/issue/SE-8006/intel-macos-codex-cli-crashes-in-v8-startup-on-gpt-56-sol-tool-calls).
Fixes #28390.

## What

- add an expanded entitlement profile only for x86_64 `codex` and
`codex-app-server`, the release binaries that link V8
- keep arm64 and `codex-responses-api-proxy` on the existing narrower
profile
- share one fail-closed target/binary selector between signing and final
verification
- verify the expected Mach-O architecture and exact entitlement
dictionary for the signed binary, tar.gz, zstd, package, and DMG copies

## Verification

- `just test-github-scripts` (34 tests)
- `UV_CACHE_DIR=/private/tmp/codex-uv-cache just fmt-check`
- `bash -n .github/scripts/macos-signing/select_codex_entitlements.sh`
- `plutil -lint` on both entitlement profiles
- parsed `rust-release.yml` as YAML
- `git diff --check`
- ad-hoc Hardened Runtime signing smoke on an x86_64 Mach-O slice:
strict `codesign` verification passed; the Codex profile contained
exactly both keys and the proxy profile retained exactly `allow-jit`

## Release validation

Run a native Intel smoke of the final Developer ID-signed x86_64 Codex
binary through V8 isolate creation before shipping. PR #30849 is
diagnostic scaffolding, but its non-sandbox release job currently fails
in the harness before V8 starts, so it is not counted as coverage here.
This commit is contained in:
malsamiri-oai
2026-07-07 09:47:21 -07:00
committed by GitHub
parent 42156ba007
commit f363ed70cc
6 changed files with 96 additions and 9 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>

View File

@@ -4,5 +4,7 @@
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,37 @@
import plistlib
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SIGNING_DIR = ROOT / ".github" / "scripts" / "macos-signing"
ALLOW_JIT = "com.apple.security.cs.allow-jit"
ALLOW_UNSIGNED_EXECUTABLE_MEMORY = (
"com.apple.security.cs.allow-unsigned-executable-memory"
)
class MacosSigningEntitlementsTest(unittest.TestCase):
def load(self, binary: str) -> dict[str, bool]:
path = SIGNING_DIR / f"{binary}.entitlements.plist"
with path.open("rb") as file:
return plistlib.load(file)
def test_v8_binaries_allow_unsigned_executable_memory(self) -> None:
expected = {
ALLOW_JIT: True,
ALLOW_UNSIGNED_EXECUTABLE_MEMORY: True,
}
for binary in ["codex", "codex-app-server", "codex-code-mode-host"]:
with self.subTest(binary=binary):
self.assertEqual(self.load(binary), expected)
def test_responses_proxy_keeps_existing_entitlements(self) -> None:
self.assertEqual(
self.load("codex-responses-api-proxy"),
{ALLOW_JIT: True},
)
if __name__ == "__main__":
unittest.main()

View File

@@ -573,6 +573,12 @@ jobs:
zstd -d --stdout "$unsigned_path" >"$signed_path"
chmod 0755 "$signed_path"
entitlements="${GITHUB_WORKSPACE}/.github/scripts/macos-signing/${binary}.entitlements.plist"
if [[ ! -f "$entitlements" ]]; then
echo "Entitlements file $entitlements not found"
exit 1
fi
.github/scripts/macos-signing/sign_macos_code.sh \
--target "$signed_path" \
--identity unused \
@@ -580,7 +586,7 @@ jobs:
--identifier "$binary" \
--options runtime \
--timestamp true \
--entitlements .github/scripts/macos-signing/codex.entitlements.plist
--entitlements "$entitlements"
mkdir -p "${report_dir}/${binary}"
rcodesign print-signature-info "$signed_path" \
@@ -974,15 +980,29 @@ jobs:
target="${{ matrix.target }}"
packaged_dir="dist/${target}"
expected_entitlements="${GITHUB_WORKSPACE}/.github/scripts/macos-signing/codex.entitlements.plist"
case "$target" in
aarch64-apple-darwin) expected_arch="arm64" ;;
x86_64-apple-darwin) expected_arch="x86_64" ;;
*)
echo "Unexpected macOS target: $target"
exit 1
;;
esac
verify_signed_binary() {
local path="$1"
local actual_entitlements normalized_actual normalized_expected
local binary="$2"
local actual_entitlements expected_entitlements normalized_actual normalized_expected
chmod 0755 "$path"
lipo "$path" -verify_arch "$expected_arch"
codesign --verify --strict --verbose=2 "$path"
expected_entitlements="${GITHUB_WORKSPACE}/.github/scripts/macos-signing/${binary}.entitlements.plist"
if [[ ! -f "$expected_entitlements" ]]; then
echo "Expected entitlements file $expected_entitlements not found"
exit 1
fi
actual_entitlements="$(mktemp)"
normalized_actual="$(mktemp)"
normalized_expected="$(mktemp)"
@@ -995,7 +1015,7 @@ jobs:
for binary in ${{ matrix.binaries }}; do
binary_path="${RUNNER_TEMP}/signed-binaries/${binary}"
verify_signed_binary "$binary_path"
verify_signed_binary "$binary_path" "$binary"
# The app-server package contains the host, but its standalone archives
# are omitted above to avoid duplicate release asset names.
@@ -1007,11 +1027,11 @@ jobs:
rm -rf "$direct_archive_dir"
mkdir -p "$direct_archive_dir"
tar -xzf "${packaged_dir}/${binary}-${target}.tar.gz" -C "$direct_archive_dir"
verify_signed_binary "${direct_archive_dir}/${binary}-${target}"
verify_signed_binary "${direct_archive_dir}/${binary}-${target}" "$binary"
direct_zstd_path="${RUNNER_TEMP}/${binary}-${target}-from-zstd"
zstd -d --stdout "${packaged_dir}/${binary}-${target}.zst" >"$direct_zstd_path"
verify_signed_binary "$direct_zstd_path"
verify_signed_binary "$direct_zstd_path" "$binary"
done
case "${{ matrix.bundle }}" in
@@ -1033,8 +1053,8 @@ jobs:
rm -rf "$package_dir"
mkdir -p "$package_dir"
tar -xzf "${packaged_dir}/${package_stem}-${target}.tar.gz" -C "$package_dir"
verify_signed_binary "${package_dir}/bin/${package_entrypoint}"
verify_signed_binary "${package_dir}/bin/codex-code-mode-host"
verify_signed_binary "${package_dir}/bin/${package_entrypoint}" "$package_entrypoint"
verify_signed_binary "${package_dir}/bin/codex-code-mode-host" "codex-code-mode-host"
if [[ "${{ matrix.verify_dmg }}" != "true" ]]; then
exit 0
@@ -1060,7 +1080,7 @@ jobs:
trap cleanup_mount EXIT
for binary in ${{ matrix.binaries }}; do
verify_signed_binary "${mount_dir}/${binary}"
verify_signed_binary "${mount_dir}/${binary}" "$binary"
done
cleanup_mount