Skip to content

fix(mac): preserve privileged helper signing identity (#175) - #192

Merged
Noah-Tervalon-Nvidia merged 2 commits into
NVIDIA:developfrom
MylesShannon:fix/macos-helper-signature-validation
Nov 7, 2026
Merged

Noah-Tervalon-Nvidia merged 2 commits into
NVIDIA:developfrom
MylesShannon:fix/macos-helper-signature-validation

Conversation

@MylesShannon

@MylesShannon MylesShannon commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Changelog title

Restore macOS privileged helper communication after upgrades

Changelog body

  • Preserve the helper-specific signing identifier needed to communicate with the macOS background helper and complete setup.
  • Replace an enabled but unreachable helper once per app version after an upgrade, and keep setup incomplete until helper communication and firewall configuration succeed.

Bumps

  • services: minor
  • nvpair-cluster-manager: none
  • nvpair-engine-manager: none
  • nvpair-errors: none
  • nvpair-job-scheduler: none
  • nvpair-manual-nodes: none
  • nvpair-node-info: none
  • nvpair-node-scanner: none
  • nvpair-node-settings: none
  • nvpair-proxy: none
  • nvpair-tui: none
  • nvpair-ui-broker: none
  • nvpair-workload-manager: none

Description

Addresses #175. In PAIR 1.0.0 (build 1.0.0.85), the running helper is signed as com.nvidia.nvpair, while its XPC peer correctly requires com.nvidia.nvpair.helper. Filename-derived signing can strip the .helper suffix, leaving registration enabled but XPC unreachable and setup incomplete.

Embed the daemon's canonical CFBundleIdentifier in both Mach-O architecture slices so standalone and recursive signing preserve its identity. Add a signing regression test and a release validator using the existing shared Swift requirements.

On upgrade, treat an enabled but unreachable helper as stale even when its daemon version cannot be read. Persist the repair attempt and invalidate cached setup completion before unregistering, then register the replacement, re-check status, and configure the firewall. Replacement is limited to once per app version across launches. Registration errors, unreachable helpers, and a remaining version mismatch leave setup incomplete. Pending approval and firewall failures retain the normal next-launch setup path without another replacement cycle.

Scope

macOS helper build metadata, startup recovery, signing and recovery regression coverage, signature validation tooling, and release acceptance documentation. Existing XPC authentication, Developer ID trust, and identifier pinning remain intact. The control tool retains the enclosing application's Bundle.main required by ServiceManagement. No Go service implementation changes.

Validation

  • Linux, Node.js 26.13.0, locked public dependencies: make check and npm --prefix desktop run dead-code:check pass. Includes lint, all TypeScript targets, build-script checks, service contracts, SPDX headers, 297 Vitest tests, and 26 Node tests.
  • Fifteen helper setup tests exercise the real setup wrapper and configuration persistence with the native control tool mocked. Coverage includes the missing daemon version on upgrade, successful replacement, failed unregister/register, unreachable replacement, persisted retry limits, interrupted repair, retry after an app-version change, approval, fresh registration, healthy startup, version refresh, and firewall failure/retry. The upgrade regression failed before the recovery change.
  • git diff --check passes. Prior sh -n validation of both added shell scripts passes; they are unchanged by the recovery update.
  • Prior Apple Silicon macOS testing of the same native implementation: npm run build:helper:mac and npm run test:helper-signing:mac pass for the universal daemon. Without the embedded plist, the regression test fails. Incorrect identifiers in the x86_64 slice are rejected even when arm64 is correct.
  • The full validator rejects the affected installed helper, missing artifacts, and ad hoc signatures; it accepts the affected release's correctly signed control tool and app.
  • The maintainer reported verification of the initial signing change with an internally signed build. The startup recovery update still needs signed-build verification.

Release integration and outstanding checks

Release maintainers must invoke npm run verify:helper-signatures:mac -- "/path/to/NVIDIA PAIR.app" after the final signing pass and fail the release if it exits nonzero. Repeat against each extracted DMG/ZIP. The public source change supplies the validator; release signing integration is still required.

  • Verify an NVIDIA-signed candidate containing the startup recovery update against the complete pinned requirements for every architecture.
  • Upgrade in place from the affected release without unregistering its helper or clearing user data. Verify one replacement attempt, reachable XPC status and matching versions, successful firewall configuration, and setup completion persisting across two quits/relaunches and a reboot.
  • Verify clean installation and approval-pending recovery with the signed candidate.

A development app skips helper setup, so the absence of its prompt is not validation of this fix. The signed-build acceptance procedure is documented in desktop/docs/macos-privileged-helper.md.

Risk

Packaging metadata must survive every signing pass. The first launch of each app version may replace a stale helper and require macOS approval. A failed replacement is not repeatedly attempted for the same version. No signing credentials or private signing implementation are included.

Checklist

  • I have read the Contributing Guidelines.
  • Every commit is signed off (git commit -s), certifying the Developer Certificate of Origin.
  • Relevant automated or manual checks cover the change, as recorded above.
  • Relevant documentation is updated.
  • I checked the diff, changed filenames, and commit messages for credentials, private data, internal URLs, internal issue identifiers, and generated artifacts.
  • I recorded validation commands and results above, including outstanding checks.
  • I declared version bumps in the release-intent block. No Go service implementation changes; all component bumps are none. Version files and changelog are left to automation.

Signed-off-by: Myles Shannon <github@mylesshannon.com>

@Noah-Tervalon-Nvidia Noah-Tervalon-Nvidia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great, but one thing is missing - when someone upgrades from 1.0.0 to whichever version has this they won't get the fix. Since the 1.0.0 version causes the daemon to fail it'll be unreachable, and in desktop/src/electron/services/mac-privilege-service.ts unreachable is treated as healthy so we wouldn't re-register the correctly signed helper.

Pretty sure the fix is just treat enabled && !reachable as stale, run uninstall() + install() once per app version, then re-check status(). Also stop reporting firewallConfigured: true without reaching the daemon (i.e. treat unreachable as failure rather than success).

@Noah-Tervalon-Nvidia

Copy link
Copy Markdown
Collaborator

moving out of draft as well. I have verified this with an internally signed build, with the fix, I'll do another round of verification and then we can merge this in.

@Noah-Tervalon-Nvidia
Noah-Tervalon-Nvidia marked this pull request as ready for review November 6, 2026 16:57
Signed-off-by: Myles Shannon <github@mylesshannon.com>
@MylesShannon

Copy link
Copy Markdown
Contributor Author

I’ve pushed an update addressing the upgrade path. Enabled but unreachable helpers now get one unregister/register attempt per app version, persisted across restarts, followed by another status check. Setup stays incomplete if the helper remains unreachable or firewall configuration fails.

I added regression coverage, and all local checks pass. It’s ready for another round of signed-build testing. Thanks for catching this.

@MylesShannon

Copy link
Copy Markdown
Contributor Author

Testing the signed build exposed another upgrade-recovery case. I opened #201 as a follow-up to #192, with the additional fix isolated in its own commit. #192 should merge first.

@Noah-Tervalon-Nvidia
Noah-Tervalon-Nvidia merged commit 6765d41 into NVIDIA:develop Nov 7, 2026
14 checks passed
pair-release-intent Bot added a commit that referenced this pull request Nov 7, 2026
Apply release intent from PR #192.

Applies-PR: #192
Noah-Tervalon-Nvidia pushed a commit that referenced this pull request Nov 10, 2026
<!-- pair-release-intent:v1 -->
### Changelog title
Recover stale macOS helper registrations after upgrades

### Changelog body
- Allow a delayed, bounded retry when macOS retains the privileged
helper's old signing identity after an upgrade.
- Preserve the retry limit across application restarts and pending
background-item approval.

### Bumps
- services: patch
- nvpair-cluster-manager: none
- nvpair-engine-manager: none
- nvpair-errors: none
- nvpair-job-scheduler: none
- nvpair-manual-nodes: none
- nvpair-node-info: none
- nvpair-node-scanner: none
- nvpair-node-settings: none
- nvpair-proxy: none
- nvpair-tui: none
- nvpair-ui-broker: none
- nvpair-workload-manager: none
<!-- /pair-release-intent:v1 -->

## Summary

Follow-up to #192, related to #175. **Depends on #192.** This branch
includes its two commits and merges the latest `develop`; the new
recovery change is isolated in
[a4dd4bf](MylesShannon@a4dd4bf).
The PR diff currently includes the prerequisite signing fix.

Testing the signed build on my Apple Silicon Mac confirmed that both
native binaries satisfy the pinned signing requirements. However,
launchd retained a launch constraint requiring the old helper
identifier, `com.nvidia.nvpair`, and rejected the correctly signed
`com.nvidia.nvpair.helper`. macOS invalidated the old registration about
10 seconds later, but the job continued failing. An additional
unregister/register cycle recovered XPC communication, firewall
configuration, and setup persistence across app restarts.

The existing once-per-version repair marker prevented that additional
cycle. This change permits at most two replacement attempts per app
version, saving the count and clearing setup completion before each
unregister. Before the second attempt, it waits 12 seconds and checks
status again. A recovered helper is left running, and pending approval
stops recovery until a subsequent launch. A saved version without a
count consumes the first attempt.

## Scope

macOS helper recovery, persisted attempt bookkeeping, regression tests,
and the existing helper documentation. The follow-up leaves signing
requirements, helper identifiers, privileged operations, and
Windows/Linux behavior unchanged. `services: patch` records the desktop
bug fix in release notes; all Go component bumps remain `none` because
their compiled output is unchanged.

## Validation

- `make -o desktop/package-lock.json check`: SPDX headers, build-script
verification, lint, all three TypeScript targets, service contracts, 26
Node tests, and 303 Vitest tests passed. The make option reuses
installed dependencies; the upstream lockfile change only updates the
application version.
- `cd desktop && npm run dead-code:check`: passed.
- `cd desktop && ./node_modules/.bin/vitest run --project unit
tests/modular/mac-privilege-service.test.ts`: 21 tests passed. The five
initial recovery regressions failed against the previous implementation
before the fix.
- Coverage includes waiting before a second unregister, recovery during
the wait, approval before/during recovery, interrupted attempts,
persisted limits across reloads, a new app version, and the previous
version-only repair marker.
- `git diff --check`: passed.
- Manual evidence above is from the signed prerequisite build plus a
controlled CLI recovery, not a signed build of this follow-up.

## Risk and remaining test

The 12-second delay is based on the observed launchd repair window, not
an Apple timing guarantee. A failed retry leaves setup incomplete and
cannot repeat replacement indefinitely. Signature pinning remains
enforced throughout.

- [ ] Validate this commit in an NVIDIA-signed build by upgrading an
affected installation in place, without manually unregistering the
helper or clearing its data. Confirm the automatic second attempt, XPC
status, firewall configuration, and setup persistence across full app
restarts and a reboot. Exercise approval followed by relaunch as well.

## Checklist

- [x] I have read the contributing guidelines.
- [x] Every commit is signed off for the Developer Certificate of
Origin.
- [x] Tests cover the change and relevant documentation is updated.
- [x] I checked the diff, changed filenames, and commit messages for
credentials, private data, internal URLs, internal issue identifiers,
and generated artifacts.
- [x] Validation commands and limitations are recorded above.
- [x] Release intent is declared without editing automation-owned
version files.

---------

Signed-off-by: Myles Shannon <github@mylesshannon.com>
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