Skip to content

[audit][bug] System-audio permission probe reports granted even when TCC denies — onboarding shows a false checkmark and recovery UI is unreachable #168

Description

@realproject7

Found by the full-codebase audit of 2026-07-14 (PO manual §8.1: 15 scan agents over 100% of production files, adversarially verified, then PO-verified against source). Part of the v1.3 audit follow-up set under EPIC #134.

Goal

probe_system_audio() infers "granted" from tap-creation success, but tap creation SUCCEEDS regardless of the TCC decision (denied taps yield silence). A user who denies System Audio Recording sees "granted", the Grant/recheck remediation UI can never activate, and live sessions silently caption nothing from the system channel.

Where (verified on main f8eb9ea)

src-tauri/src/permissions.rs:98 (high/likely)

  • Issue: probe_system_audio() (and request_audio_access(), which calls it) treats successful creation of the process tap as "permission granted". But crates/livecap-core/src/audio/system.rs:257-260 explicitly documents that AudioHardwareCreateProcessTap SUCCEEDS regardless of the TCC decision -- "if permission is denied the tap yields silence (all zeros)". So Ok(capture) is returned (and probe_system_audio reports true/"granted") whether the user granted OR denied System Audio Recording, as long as an output device exists and the tap format matches float32 PCM.

  • Impact: src/onboarding.ts:126-147 renders this boolean directly as the granted/denied state and gates the onboarding continue button on access.systemAudio being true. A user who explicitly denies (or never answers) the System Audio Recording TCC prompt still sees "granted" and can proceed past onboarding; later captioning sessions silently receive all-zero system audio with no user-visible signal that permission was actually refused -- the exact opposite of what the permission check is supposed to guarantee. This is the same class of bug the file's own history already fixed for the microphone (the old cpal-probe race, replaced by AVCaptureDevice.requestAccess) but the fix was never applied to the system-audio path.

  • Fix direction: Don't infer permission from tap-creation success. Either (a) after starting the tap, read a few audio buffers over the receiver and check whether they are non-silent (or use whatever real signal SystemAudioCapture exposes for stream health) before reporting granted, or (b) if no reliable programmatic signal exists, keep tap creation only to raise the sheet on first use and stop conflating "tap object exists" with "granted" in the returned status shown to the user.

  • crates/livecap-core/src/audio/system.rs:257-260 — documents that the permission dialog fires on tap creation and a denied tap yields all-zero silence (creation still succeeds).

  • src/onboarding.ts:95-105,126-149 — renders the boolean as granted/denied; the probed && !granted recovery branch (Open System Settings + Check again) is unreachable for a real denial.

  • Contrast: the mic path was already migrated to AVCaptureDevice.requestAccess for exactly this false-signal class (permissions.rs:58-69).

Scope

  1. Stop conflating "tap object created" with "permission granted". Preferred: after starting the probe tap, read buffers briefly and treat all-zero output as not-granted (or use any real stream-health signal SystemAudioCapture can expose). If no reliable programmatic signal exists, change the returned tri-state to reflect uncertainty and make the onboarding copy + recheck affordance honest.
  2. Keep raising the TCC sheet on first probe (current behavior).
  3. Update onboarding.ts rendering so a denied/uncertain state shows the remediation path.

Out of scope

Mic path (already correct). #111 calibration work.

AC

Security invariants

Never log or persist caption/audio content; no new dependencies; simplest fix that fully solves the ticket — no drive-by refactors.

Ticket-review amendments (Batch 36, 2026-07-14 — evidence in QW chat #873-#880)

  • The "TCC ON => probe reports granted" AC is NOT reliably testable from the silence signal: genuinely quiet system output is also all-zero (re1). REVISED contract: the probe/UI state becomes a tri-state — granted only when a positive signal exists, unknown when granted-vs-quiet cannot be distinguished, denied never rendered as granted. Onboarding must show remediation (Open System Settings + Check again) for BOTH denied and unknown. AC drops the unreliable ON-assertion and keeps: denial is never shown as ✓; remediation reachable; TCC on/off matrix verified in the STT live verification & per-model floor calibration on real audio (closes the #92/#93/#94 verification gap) #111 device session.

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/claudeBuilt by local Claude agent (needs this device)bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions