Bug 2073303 - grant Screen Recording during provisioning - #76
Merged
Merged
Conversation
EACS re-enables SIP and wipes TCC, so a reprovisioned host comes back without the ScreenCapture grant and silently fails every getDisplayMedia() call with SCStreamErrorUserDeclined (-3801) for the rest of its life. The only symptom is an intermittent orange on a random-looking subset of the pool: 42 of the 174 hosts in gecko-t-osx-1500-m4 were in that state, which is why bug 1937556 reached 499 failures in 30 days and was sheriff-escalated before anyone established it was the hosts and not Gecko. Those 42 have been granted by hand and verified (0 occurrences of -3801 across a 40-run try push). Without this step that fix decays one host at a time, invisibly, as machines go through refresh waves. This belongs here rather than in ronin for two reasons. The approval needs an administrator-authenticated click in System Settings and puppet has no credential for it; and the lifecycle is exactly this one -- the grant is destroyed by the wipe we perform, and has to be re-applied after the bootstrap, once the worker binaries exist and cltbld owns the console session. ronin gets the detector half instead (macos_screencapture_check, ronin_puppet#1412). MDM cannot supply the grant. Apple permits only AllowStandardUserToSetSystemService for this service in a PPPC payload, which authorises a standard user to approve it rather than approving it -- and an approval made while such a profile is installed is recorded as MDM-managed (flags 12) and then ignored by TCC. There is no consent dialog to automate either: tccd logs "Service kTCCServiceScreenCapture does not allow prompting; returning denied". Driving the Screen Recording pane is the only route, which is why this is GUI automation rather than a defaults write. The payload enforces four preconditions, each of which was learned by getting it wrong on the fleet: - SIP on. SIP-off hosts are already covered by macos_tcc_perms. - Developer-ID-signed worker binaries. An ad-hoc binary is Identifier=a.out with no TeamIdentifier and cannot satisfy a code requirement, so the grant is stored and ignored. - No ScreenCapture PPPC override, or the resulting row is flags=12 and inert. - Host idle. A running test owns the GUI session: the click does not land, and opening System Settings can corrupt that test. 16 of 18 failures on the first fleet pass were mid-mochitest. Exit 3 from the payload means "not applicable / not now" and is reported rather than raised, so a host that is merely busy does not abort a reprovision. A genuine failure -- unsigned binary, override present, flags=12, or a row that never reached auth_value 2 -- raises, because handing back a host that cannot screen-capture and saying nothing is the failure mode this whole bug is made of. The credential is substituted from the vault at fire time and written over ssh, same as _os_upgrade_script, so it never sits in SimpleMDM. On the host it goes into a 0600 file owned by cltbld, read once by the AppleScript and deleted immediately, so it is never an argv and never in the environment. The staged script is removed afterwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI caught what the local run could not. tests/test_provision.py patches the step list but did not know about step_screencapture_grant, so the real step ran and tried to resolve the admin password from 1Password. That passes on a laptop with `op` installed and authenticated, and fails on a runner without it -- a false green of exactly the kind this suite exists to prevent. Mocked in all five provision() sequence tests, and the ordering test now asserts the grant runs last, after the bootstrap sentinel: it needs the worker binaries in place and cltbld owning the console session, neither of which exists before puppet has run. Verified by reproducing the CI condition locally -- running the suite with the 1Password CLI removed from PATH. 285 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why this, and why here
EACS re-enables SIP and wipes TCC, so a reprovisioned host comes back without the ScreenCapture grant and silently fails every
getDisplayMedia()call withSCStreamErrorUserDeclined(-3801) for the rest of its life.The only symptom is an intermittent orange on a random-looking subset of the pool. 42 of the 174 hosts in
gecko-t-osx-1500-m4were in that state — which is why bug 1937556 reached 499 failures in 30 days and was sheriff-escalated before anyone established it was the hosts, not Gecko.Those 42 have now been granted by hand and verified: 0 occurrences of -3801 across a 40-run try push. Without this step, that fix decays one host at a time, invisibly, as machines go through refresh waves.
It belongs in this repo rather than ronin for two reasons:
ronin gets the detector half: ronin_puppet#1412.
Why GUI automation rather than something sane
MDM cannot supply this grant. Apple permits only
AllowStandardUserToSetSystemServiceforkTCCServiceScreenCapturein a PPPC payload, which authorises a standard user to approve it rather than approving it — and an approval made while such a profile is installed is recorded as MDM-managed (flags 12) and then ignored by TCC. It looks correct in System Settings and in the access table, and does not work.There is no consent dialog to automate either:
Driving the Screen Recording pane is the only remaining route.
Preconditions the payload enforces
Each of these was learned by getting it wrong on the fleet:
macos_tcc_permsIdentifier=a.out, no TeamIdentifier — grant is stored and ignoredflags=12and is inertFailure semantics
Exit 3 means "not applicable / not now" (SIP off, busy, no console session) and is reported, not raised — a host that is merely busy must not abort a reprovision.
A genuine failure — unsigned binary, override present,
flags=12, or a row that never reachedauth_value 2— raises. Handing back a host that cannot screen-capture and saying nothing is the failure mode this entire bug is made of.Credential handling
Substituted from the vault at fire time and written over ssh, same as
_os_upgrade_script, so it never sits in SimpleMDM. On the host it goes into a 0600 file owned by cltbld, read once by the AppleScript and deleted immediately — never an argv, never in the environment. The staged script is removed afterwards (asserted in tests).Testing
reprovision()andprovision().test_mint.pysequence assertions extended to require the grant runs after the bootstrap sentinel.ruff checkandruff formatclean.osacompile), and the payload passesshellcheck.🤖 Generated with Claude Code