136 lines
7.1 KiB
Bash
Executable File
136 lines
7.1 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# Lane B — deterministic datastore policy grants (docs/MACOS_VM_POLICY_ALTERNATIVE.md).
|
|
#
|
|
# Runs INSIDE a macOS guest, as root, during a base build's dedicated SIP-OFF boot. It writes the
|
|
# in-guest agent's TCC grants directly into the system TCC.db (impossible with SIP on), disables the
|
|
# notification SOURCES the base must stay quiet about, and reads everything back so the caller can gate
|
|
# on verified state rather than an exit code. It is idempotent and safe to re-run.
|
|
#
|
|
# The host runs it over the vsock exec channel (no share mount, so it can't trip the very Network
|
|
# Volumes prompt it is granting): the whole file is base64'd on the host and piped into `sudo bash`,
|
|
# with parameters passed as environment variables — never on a command line an `ps` could read:
|
|
#
|
|
# printf %s "<base64>" | base64 --decode \
|
|
# | sudo NUCLEIC_VMAGENT_CSREQ_HEX=<hex> NUCLEIC_REQUIRE_CSREQ=1 bash
|
|
#
|
|
# Environment (all optional; sane defaults):
|
|
# NUCLEIC_VMAGENT_CSREQ_HEX hex of the agent's compiled designated-requirement blob (csreq), or
|
|
# empty → write csreq NULL rows (still allowed by bundle id on current
|
|
# builds, but not pinned to the code signature).
|
|
# NUCLEIC_REQUIRE_CSREQ 1 → a non-NULL csreq is REQUIRED for the run to report OK (set by the
|
|
# host only when it actually derived a blob, i.e. a signed build); 0/unset
|
|
# → a NULL csreq is acceptable (ad-hoc dev build with no requirement).
|
|
# NUCLEIC_AGENT_USER the auto-login account (default: agent) — for the per-user launchctl UID.
|
|
# NUCLEIC_POLICY_GRANTS_VERIFY_ONLY 1 → skip all writes; only read back + report (the post-SIP-re-enable
|
|
# survival check).
|
|
#
|
|
# Machine-readable markers on stdout (the host parses these, never the exit code):
|
|
# SIP=<0|1> 0 = SIP enabled, 1 = disabled (as csrutil reports it this boot)
|
|
# GRANT <service> auth=<n> csreq=<0|1> one per expected service
|
|
# NOTIF <label>=<disabled|present|error> one per notification source
|
|
# POLICY_GRANTS_OK / POLICY_GRANTS_FAIL: <reason>
|
|
|
|
set -u
|
|
|
|
BUNDLE_ID="xyz.blakeslee.nucleic.vmagent"
|
|
TCC_DB="/Library/Application Support/com.apple.TCC/TCC.db"
|
|
AGENT_USER="${NUCLEIC_AGENT_USER:-agent}"
|
|
CSREQ_HEX="${NUCLEIC_VMAGENT_CSREQ_HEX:-}"
|
|
REQUIRE_CSREQ="${NUCLEIC_REQUIRE_CSREQ:-0}"
|
|
VERIFY_ONLY="${NUCLEIC_POLICY_GRANTS_VERIFY_ONLY:-0}"
|
|
|
|
# The full set Lane B grants the agent. FDA + Network Volumes remove the "Data Access Blocked" banner
|
|
# and the virtioFS custom-mount prompt (the "mac_vm_exec never returns" hang); Screen Recording is the
|
|
# one grant NO MDM PPPC payload can ever deliver silently; Accessibility + PostEvent are the semantic
|
|
# AX agent's control + input. All keyed by BUNDLE ID (client_type=0) with the agent's csreq blob.
|
|
SERVICES=(
|
|
kTCCServiceSystemPolicyAllFiles
|
|
kTCCServiceSystemPolicyNetworkVolumes
|
|
kTCCServiceScreenCapture
|
|
kTCCServiceAccessibility
|
|
kTCCServicePostEvent
|
|
)
|
|
|
|
# SIP as this boot sees it. Reported for the transcript; the write below simply fails (and is caught by
|
|
# the read-back) if SIP is actually on, so this is diagnostic, not a gate.
|
|
if csrutil status 2>/dev/null | grep -qi disabled; then echo "SIP=1"; else echo "SIP=0"; fi
|
|
|
|
# ── Writes (skipped in verify-only) ───────────────────────────────────────────────────────────────
|
|
if [ "$VERIFY_ONLY" != "1" ]; then
|
|
# tccd holds the db open; stop it so the direct write can't race its cache, then it relaunches and
|
|
# reloads on next access. Both the system daemon and any per-user instance.
|
|
killall tccd 2>/dev/null || true
|
|
|
|
# Compile the csreq literal once: X'<hex>' when the host supplied a blob, else NULL.
|
|
if [ -n "$CSREQ_HEX" ]; then CSREQ_SQL="X'${CSREQ_HEX}'"; else CSREQ_SQL="NULL"; fi
|
|
|
|
# Columns are ALWAYS named: macOS keeps adding columns to `access` across releases, so a positional
|
|
# INSERT breaks on the next OS. INSERT OR REPLACE so a re-run refreshes rather than duplicates.
|
|
# client_type = 0 → client identified by BUNDLE ID
|
|
# auth_value = 2 → ALLOWED (0 denied, 1 unknown, 2 allowed, 3 limited)
|
|
# auth_reason = 3 → a plausible provenance for a pre-grant
|
|
# auth_version= 1 → current row schema version
|
|
for svc in "${SERVICES[@]}"; do
|
|
sqlite3 "$TCC_DB" \
|
|
"INSERT OR REPLACE INTO access
|
|
(service,client,client_type,auth_value,auth_reason,auth_version,csreq,policy_id,indirect_object_identifier_type,indirect_object_identifier,indirect_object_code_identity,flags,last_modified)
|
|
VALUES('$svc','$BUNDLE_ID',0,2,3,1,$CSREQ_SQL,NULL,0,'UNUSED',NULL,0,strftime('%s','now'));" \
|
|
2>/dev/null || echo " (write of $svc reported an error — the read-back below is authoritative)" >&2
|
|
done
|
|
killall tccd 2>/dev/null || true
|
|
|
|
# ── Notification sources (disable the SOURCE, not the alert — docs/MACOS_VM_POLICY_ALTERNATIVE.md).
|
|
# `launchctl disable` writes a PERSISTENT override into the base image, so every clone inherits it.
|
|
# Tips posts "Learn how to take a screenshot"; BTMNotificationAgent posts "Multiple Extensions
|
|
# Added". Disabling these two agents stops those banners at the source without touching the
|
|
# notification framework a tester's own app may use, and without the version-fragile ncprefs format.
|
|
AGENT_UID="$(id -u "$AGENT_USER" 2>/dev/null || echo 0)"
|
|
for label in com.apple.tipsd com.apple.BTMNotificationAgent; do
|
|
launchctl disable "gui/$AGENT_UID/$label" 2>/dev/null || true
|
|
launchctl bootout "gui/$AGENT_UID/$label" 2>/dev/null || true
|
|
done
|
|
fi
|
|
|
|
# ── Read-back (always) — the authoritative verification the host gates on ─────────────────────────
|
|
ALL_OK=1
|
|
FAIL_REASON=""
|
|
for svc in "${SERVICES[@]}"; do
|
|
auth="$(sqlite3 "$TCC_DB" \
|
|
"SELECT auth_value FROM access WHERE service='$svc' AND client='$BUNDLE_ID' AND client_type=0;" \
|
|
2>/dev/null)"
|
|
hascsreq="$(sqlite3 "$TCC_DB" \
|
|
"SELECT (csreq IS NOT NULL) FROM access WHERE service='$svc' AND client='$BUNDLE_ID' AND client_type=0;" \
|
|
2>/dev/null)"
|
|
auth="${auth:-none}"
|
|
hascsreq="${hascsreq:-0}"
|
|
echo "GRANT $svc auth=$auth csreq=$hascsreq"
|
|
if [ "$auth" != "2" ]; then
|
|
ALL_OK=0
|
|
FAIL_REASON="${FAIL_REASON} $svc(auth=$auth)"
|
|
elif [ "$REQUIRE_CSREQ" = "1" ] && [ "$hascsreq" != "1" ]; then
|
|
ALL_OK=0
|
|
FAIL_REASON="${FAIL_REASON} $svc(csreq-null)"
|
|
fi
|
|
done
|
|
|
|
# Notification read-back (never gates — reported for the transcript only).
|
|
if [ "$VERIFY_ONLY" != "1" ]; then
|
|
AGENT_UID="$(id -u "$AGENT_USER" 2>/dev/null || echo 0)"
|
|
disabled_list="$(launchctl print-disabled "gui/$AGENT_UID" 2>/dev/null || true)"
|
|
for label in com.apple.tipsd com.apple.BTMNotificationAgent; do
|
|
if printf '%s' "$disabled_list" | grep -q "\"$label\" => \(disabled\|true\)"; then
|
|
echo "NOTIF $label=disabled"
|
|
else
|
|
echo "NOTIF $label=present"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ "$ALL_OK" = "1" ]; then
|
|
echo "POLICY_GRANTS_OK"
|
|
else
|
|
echo "POLICY_GRANTS_FAIL:${FAIL_REASON}"
|
|
fi
|
|
exit 0
|