Open PR #1 open bug report

macOS: Adom Pup.app wrapper drops Chrome's usage strings, so Google sign-in (WebAuthn hybrid, Bluetooth) gets the browser killed by TCC

Drew Owens · 1d ago

Versions: pup (puppeteer bridge) 3.0.1 (latest release) · Chrome for Testing 157.0.8103.38 inside the "Adom Pup.app" wrapper (bundle inc.adom.pup.browser) · Adom Bridge 1.9.232 · macOS 27.2 (26B123) · MacBook Air, Apple silicon (Mac16,12) · profile adom-you, extension lane, default taskbarMode (tiles)

Repro

  1. pup_open_window on a site with a "Sign in with Google" button (here JLCPCB's sign-in page, passport.jlcpcb.com).
  2. Click Sign in with Google.
  3. macOS kills the browser. It happened twice in a row: 09:56:28 local (SIGABRT) and 09:57:10 local (SIGKILL, on the relaunched browser 36 s after it started).

Expected: the Google sign-in page loads (at worst macOS asks once for Bluetooth permission).

Crash report (trimmed: report 2 shown; report 1 is the same with SIGABRT and the same TCC frames)

"bundleID":"inc.adom.pup.browser", "app_version":"157.0.8103.38", "os_version":"macOS 27.2 (26B123)"
procPath:   /Users/USER/Library/Caches/*/Adom Pup.app/Contents/MacOS/Google Chrome for Testing
parentProc: launchd
codeSigningID: inc.adom.pup.browser   codeSigningTeamID: ""   (ad-hoc)
exception:  EXC_CRASH (SIGKILL)
termination: namespace TCC
  "This app has crashed because it attempted to access privacy-sensitive data without a usage
   description. The app's Info.plist must contain an NSBluetoothAlwaysUsageDescription key with
   a string value explaining to the user how the app uses this data."
faulting thread:
  TCC  __TCC_CRASHING_DUE_TO_PRIVACY_VIOLATION__
  TCC  __TCCAccessRequest_block_invoke
  libxpc _xpc_connection_reply_callout
Bluetooth caller in report 1 (ThreadPoolBackgroundWorker):
  IOBluetooth +[IOBluetoothDevice pairedDevices]
  Google Chrome for Testing Framework  ChromeMain ...

Root cause: the wrapper drops Chrome's usage descriptions

Chromium does not keep its privacy usage strings in Info.plist. They live in Contents/Resources/*.lproj/InfoPlist.strings. The 3.0.1 wrapper copies MacOS, Frameworks, Info.plist and PkgInfo, but Resources gets only app.icns, so every .lproj folder is gone. Read on the affected Mac:

Bundle Contents/Resources NSBluetoothAlwaysUsageDescription
Stock CfT 157 (~/.cache/puppeteer/.../Google Chrome for Testing.app) 59 entries incl. en.lproj/InfoPlist.strings present ("Once Chromium has access, websites will be able to ask you for access.")
Wrapper (~/Library/Caches/adom-pup-wrappers/<id>/adom-pup/Adom Pup.app) 1 entry (app.icns) missing

The wrapper's Info.plist matches stock CfT except for CFBundleIdentifier, CFBundleName, CFBundleDisplayName and the removed CFBundleIconName. Neither plist has any NS*UsageDescription key, because they live only in the .lproj strings.

Why it fires: pup launches the wrapper through open, so the browser's parent is launchd and it is its own responsible process. Chromium then assumes it has Bluetooth metadata (base::DoesResponsibleProcessHaveBluetoothMetadata() returns true for a self-responsible app, "Because Chrome itself declares Bluetooth metadata in Info.plist"). Google's sign-in page makes a WebAuthn request, the hybrid (phone as a security key) transport is available, FidoRequestHandlerBase builds a BleAdapterManager, and BluetoothAdapterMac::TriggerSystemPermissionPrompt() calls +[IOBluetoothDevice pairedDevices]. TCC finds no usage string and kills the process.

The same missing strings cover camera, microphone, audio capture, location, local network and NSWebBrowserPublicKeyCredentialUsageDescription. I have not tested those, but any page that asks for the camera or microphone in a branded pup window probably dies the same way.

Second bug: the crash was logged as a user close

pup's log after the kill:

[health] the lane browser exited cleanly (exit code 0: its last window was closed by the user): forgetting 1 session(s), nothing restored (pup windows are fleeting)
[lane] "jlc-cart": the lane browser exited cleanly (exit code 0: its last window was closed by the user); forgotten, nothing restored

The user did not close anything, so sleepRecovery never ran and the session was dropped. Probable reason: the browser is launched via open and is parented by launchd, so the exit code pup sees is not the browser's own. A new ~/Library/Logs/DiagnosticReports/Google Chrome for Testing-*.ips for inc.adom.pup.browser (or a missing windows.onRemoved before the process went away) would tell a crash apart from a close.

Related, from the same run (minor): the window was off-screen (isOnscreen:false), so notify_user with focus:{hwnd} and focus:{app:"inc.adom.pup.browser"} could not bring it forward when we asked the user to sign in.

Suggested fix

In the wrapper build (host-mac brand.rs), either:

  • copy (or clonefile) the whole Resources folder, or at least every *.lproj, and then replace the icon; or
  • plutil -insert the usage keys into the wrapper's Info.plist before the ad-hoc codesign: NSBluetoothAlwaysUsageDescription, NSBluetoothPeripheralUsageDescription, NSCameraUsageDescription, NSMicrophoneUsageDescription, NSAudioCaptureUsageDescription, NSLocationUsageDescription, NSLocalNetworkUsageDescription, NSWebBrowserPublicKeyCredentialUsageDescription.

Copying .lproj keeps parity with whatever keys a future Chrome adds, so it is the more robust option. A check when the wrapper is built, comparing the wrapper's usage keys with the source bundle's, would catch this if it comes back.

Patching the installed wrapper by hand breaks its ad-hoc signature, and pup rebuilds the wrapper anyway, so this has to ship in pup.

Workarounds until then (none tested on this Mac)

  1. Do the Google OAuth step in the user's real Chrome (nbrowser), not in pup. This is what we did, and it worked.
  2. Sign in to the site with email and password inside pup instead of "Sign in with Google". This avoids Google's WebAuthn page.
  3. pup_configure {"taskbarMode":"plain"}, then let the browser restart. Per the 3.0.1 changelog, plain launches stock Chrome for Testing, and that bundle still has its .lproj strings, so macOS should ask for Bluetooth permission instead of killing the browser. The cost is losing the Adom Pup Dock identity. (The pup-settings skill still says taskbarMode is Windows-only on macOS, which 3.0.1 changed.)
  4. Not available today: pup has no setting for extra Chrome switches. Also, Chromium M157 has no --disable-features value that turns off hybrid (nothing in device/fido/public/features.cc). --disable-blink-features=WebAuth only hides PublicKeyCredential (navigator.credentials.get({publicKey}) is not gated by it), so it helps only on sites that feature-detect.

1 Reply

Drew Owens · 1d ago

Follow-up with a tested fix, for whoever holds the macOS source (it is not in this page's Files tab, so I could not open a PR against it). The fix below was proven on the affected Mac with a control and a patched copy of the 3.0.1 wrapper; results are in the Evidence section.

The change (host-mac brand.rs, wrapper build)

The 3.0.1 wrapper build, as seen in the binary and on disk, does: cp -c -R of Contents/MacOS and Contents/Frameworks, copies Info.plist and PkgInfo, plutil sets CFBundleIdentifier inc.adom.pup.browser, CFBundleName and CFBundleDisplayName and removes CFBundleIconName, sips writes Resources/app.icns, then codesign --force --sign -. Contents/Resources ends up with only app.icns.

  1. Carry the source bundle's Resources. Before writing the icon, clone every entry of <CfT>.app/Contents/Resources into the wrapper's Resources, except app.icns (which pup replaces). This is the same cp -c -R the build already uses for MacOS and Frameworks, so it costs nothing on APFS. The keys that matter live in *.lproj/InfoPlist.strings: NSBluetoothAlwaysUsageDescription, NSBluetoothPeripheralUsageDescription, NSCameraUsageDescription, NSMicrophoneUsageDescription, NSAudioCaptureUsageDescription, NSLocationUsageDescription, NSLocalNetworkUsageDescription, NSWebBrowserPublicKeyCredentialUsageDescription. Copying all of Resources (not just en.lproj) keeps every locale and any key a later Chrome adds.
  2. Rebuild wrappers that 3.0.1 already cached. The cache folder holds adom-pup.json (display, stamp, source, builtAt). Fold a wrapper layout version into the stamp (for example a WRAPPER_LAYOUT: u32 = 2 constant hashed with the existing inputs) so every 3.0.1 wrapper fails the stamp check and is rebuilt on the next browser start.
  3. Guard it. After building, check that the wrapper's Resources/en.lproj/InfoPlist.strings exists and contains NSBluetoothAlwaysUsageDescription (whenever the source has it). If not, log it and launch the stock bundle instead of a wrapper that macOS will kill.

Adding the keys straight into the wrapper's Info.plist with plutil -insert also stops the crash, but it hard-codes one English string per key and has to track Chrome's list by hand. Copying Resources is the smaller and safer change.

The misreported exit (second bug): the browser is started with open, so launchd is its parent and pup does not see its real exit status. Before deciding "closed by the user", check for a ~/Library/Logs/DiagnosticReports/Google Chrome for Testing-*.ips with bundle id inc.adom.pup.browser written since the browser started. If one exists, treat it as a crash (sleepRecovery) and say so in _cleanupReport.

Evidence (sandbox test on the affected Mac)

Two copies of the installed 3.0.1 wrapper in a scratch folder, each with its own test bundle id (inc.adom.pup.test.control, inc.adom.pup.test.patched), ad-hoc re-signed, launched with open -n -a and a throwaway --user-data-dir, on a page that calls navigator.credentials.get with a hybrid-only allowCredentials entry and hints: ['hybrid'].

copy Resources result
control (3.0.1 layout, a byte copy of the installed wrapper) 1 entry (app.icns), no en.lproj Killed 1.2 s after the WebAuthn request started, with no click: EXC_CRASH (SIGKILL), termination namespace TCC, "...The app's Info.plist must contain an NSBluetoothAlwaysUsageDescription key...". Same faulting frames as the original reports (__TCC_CRASHING_DUE_TO_PRIVACY_VIOLATION__ from __TCCAccessRequest_block_invoke).
patched (fix 1 above) 59 entries, same as stock CfT No crash. macOS showed "Allow "Pup Test patched" to use Bluetooth? Once Chromium has access, websites will be able to ask you for access." with Don't Allow / Allow. The browser stayed up (page heartbeat every 3 s) for the 54 s until I closed it, and no crash report was written.

Control crash report (trimmed):

"bundleID":"inc.adom.pup.test.control", "app_version":"157.0.8103.38", "os_version":"macOS 27.2 (26B123)"
procPath:   /Users/USER/Library/Caches/*/Pup Test control.app/Contents/MacOS/Google Chrome for Testing
parentProc: launchd
exception:  EXC_CRASH (SIGKILL)
termination: namespace TCC
  "This app has crashed because it attempted to access privacy-sensitive data without a usage
   description. The app's Info.plist must contain an NSBluetoothAlwaysUsageDescription key with
   a string value explaining to the user how the app uses this data."

Both copies were built the way 3.0.1 builds the wrapper (cp -c -R, plutil for the bundle id and name, codesign --force --sign -, verified with codesign --verify --strict); the only difference between them is the carried Resources. Test page: navigator.credentials.get with one allowCredentials entry whose transports are ['hybrid'], plus hints: ['hybrid']; Chrome goes straight to the phone step and asks macOS for Bluetooth.

macOS 27.2 (26B123), Apple silicon, Chrome for Testing 157.0.8103.38, pup 3.0.1, Adom Bridge 1.9.232. The live pup and its wrapper cache were not touched; both test copies were removed afterwards.

Workarounds until a fixed build ships

  • pup_configure {"taskbarMode":"plain"}, then a browser restart: stock Chrome for Testing keeps its .lproj strings (the patched copy above has the same Resources, so it stands in for this case; plain mode itself was not run).
  • Do the Google step in the user's own Chrome through the native browser bridge, or sign in to the site with email and password.

patched-bluetooth-prompt-cropped

Drew Owens mentioned this in pull request #1 · 1d ago

Log in to reply.