Skip to content

fix(macos27): embed Bluetooth usage Info.plist in daemon/CLI, re-sign installed daemon - #41

Merged
HMAKT99 merged 1 commit into
HMAKT99:mainfrom
xiawanghao206-cpu:fix/macos27-ble-info-plist
Oct 30, 2026
Merged

HMAKT99 merged 1 commit into
HMAKT99:mainfrom
xiawanghao206-cpu:fix/macos27-ble-info-plist

Conversation

@xiawanghao206-cpu

Copy link
Copy Markdown
Contributor

fix(macos27): embed Bluetooth usage Info.plist in daemon/CLI, re-sign installed daemon

Summary

Supersedes #37 by @yachoukh, rebased onto v2.0.0 (c4c746f). On macOS 27
("Golden Gate") the shipped v2.0.0 build cannot use Bluetooth from either
SwiftPM executable, which blocks pairing and therefore every privileged
authentication path. Both failures were reproduced on real hardware and fixed
here:

  1. Missing Bluetooth usage Info.plist. Plain SwiftPM executables carry no
    __info_plist section, so CoreBluetooth has nowhere to read
    NSBluetoothAlwaysUsageDescription from. On macOS 27 this leaves the TCC
    authorization request incomplete (TCC complete 0, no manager state
    callback): touchbridged logs Bluetooth powered off, and
    touchbridge-test pair never receives any CBManagerState at all, so the
    CLI silently sits on "Waiting for companion device to connect" with the
    phone stuck on "Scanning for Mac…".
  2. Invalid code signature after installation. Once the executables are
    linked with -sectcreate __TEXT __info_plist, the binary's own
    linker-signed ad-hoc signature is rejected (Code Signature Invalid, hard
    SIGKILL) after being copied to /usr/local/bin on some macOS/Xcode
    versions.

Changes

  • daemon/Package.swift: embed a minimal Info.plist (bundle identifier,
    name, NSBluetoothAlwaysUsageDescription,
    NSBluetoothPeripheralUsageDescription) into the touchbridged and
    touchbridge-test executables via
    -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist. The plist path
    is resolved from #filePath, so builds work from any working directory.
  • daemon/Sources/touchbridged/Supporting/Info.plist (new)
  • daemon/Sources/touchbridge-test/Supporting/Info.plist (new)
  • scripts/install.sh: after copying the daemon to /usr/local/bin, re-sign
    it with a plain ad-hoc codesign --force -s - pass (best effort).

Not carried over from #37

The companion-side CompanionCoordinator.beginPairing() change
(startScanning() → resetConnectionAndScan()) is not included, because
v2.0.0 changed the pairing lifecycle to fix the same failure differently:
beginPairing now calls unpair() first, and unpair() calls
bleClient.stopScanning() + disconnect() before the new service UUID is
scanned. A stale mid-scan state for an old UUID is therefore already cleared
before the next scan. If the maintainers prefer the explicit
resetConnectionAndScan() call as defence in depth, it is a one-line follow-up.

Testing

Environment: macOS 27.0.1, Xcode 27.1, Swift 6.4 (Apple Swift 6.4), real
iPhone 18 Pro (iOS 27.2) with a Personal Team signature.

  • protocol: 25 tests pass (9 XCTest + 16 Swift Testing).
  • daemon: 115 tests pass (18 XCTest + 97 Swift Testing).
  • make -C pam test: native evidence tests + PAM script migration /
    idempotence / symlink fixtures pass.
  • tests/e2e-validation.sh: isolated suite passes; menubar protected-path
    test and disabled Authorization Plugin entry check pass.
  • BLE before/after on macOS 27, same machine:
    • before: TCC available 1, req 1 complete 0, no poweredOn callback;
    • after: Bluetooth powered on → TouchBridge GATT service registered →
      Advertising started (daemon), and the CLI advertises the pairing
      service instead of hanging silently.
  • Installed daemon: codesign --verify --strict passes and the binary runs
    from /usr/local/bin.
  • Real-device acceptance (not CI coverage): QR pairing → explicit root
    fingerprint enrollment (touchbridge-trust enroll) → PAM activation via
    /etc/pam.d/sudo_local → sudo approved by iPhone Face ID; deny/timeout
    falls back to the password prompt and never grants.
  • Patch applies cleanly on c4c746f via git am (verified on a fresh clone).

Note for automated installers

On macOS 27, a root shell obtained through Authorization Services
(osascript … with administrator privileges) was observed to fail creating
files under /etc/pam.d with EPERM, while an interactive Terminal sudo
succeeds. Installers that write PAM hooks through GUI authorization may need
to run that step in a Terminal (or document it). This is an environment
behaviour, not a code change in this PR.

… after install

Plain SwiftPM executables have no Info.plist, so CoreBluetooth cannot read
NSBluetoothAlwaysUsageDescription. On macOS 27 this leaves TCC authorization
incomplete: touchbridged logs 'Bluetooth powered off' and touchbridge-test
never receives a poweredOn callback, silently stalling pairing.

Embed a minimal Info.plist into the touchbridged and touchbridge-test
executables via -sectcreate __TEXT __info_plist. Because the linker-signed
signature of such a binary can be rejected after being copied, install.sh now
runs an ad-hoc codesign pass on the installed daemon.

Mac side of PR HMAKT99#37 by @yachoukh; adapted to v2.0.0 (the companion-side
resetConnectionAndScan change is already covered by beginPairing -> unpair,
which stops scanning before a new service UUID is used).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants