Skip to content

privacy(hid): UlanziDialMacOSManager unconditionally requests Input Monitoring permission on every macOS launch #3257

Description

@K5PTB

Summary

UlanziDialMacOSManager::start() is called unconditionally at every AetherSDR startup on macOS, regardless of whether the user has ever connected or configured a Ulanzi Dial. This causes macOS to present the Input Monitoring privacy permission dialog to every macOS user on first launch of any build that includes #3236.

Root cause

MainWindow constructs m_dialBackend (aliased to UlanziDialMacOSManager on macOS) and calls start() with no guard:

// MainWindow.cpp ~line 4073
m_dialBackend = new UlanziDialBackend;          // always created
// ...
QMetaObject::invokeMethod(m_dialBackend, &UlanziDialBackend::start, ...); // always started

Inside start():

IOHIDManagerOpen(mgr, kIOHIDOptionsTypeSeizeDevice);  // triggers Input Monitoring prompt

kIOHIDOptionsTypeSeizeDevice is what macOS uses as the trigger for the Input Monitoring permission gate — it means "take exclusive control of this device, preventing the OS from seeing its events." macOS correctly treats any app that requests this flag as potentially capable of intercepting user input, and requires the user to explicitly grant permission in System Settings → Privacy & Security → Input Monitoring.

Why this matters

Input Monitoring is one of macOS's highest-privilege privacy gates — it allows an application to capture input from HID devices even while the app is in the background. The makeMatchDict() function does correctly narrow the device matching to product name "Ulanzi Dial", which limits which device is seized at the IOKit level. However:

  1. The OS-level permission is app-wide, not device-scoped. Once a user grants Input Monitoring to AetherSDR, the permission cannot be restricted to only the Ulanzi Dial code path by the OS.
  2. The vast majority of AetherSDR users do not own a Ulanzi Dial and will never benefit from this feature. Prompting them for a sensitive privacy permission they don't need erodes trust and encourages cargo-cult permission grants.
  3. A user who grants the permission expecting it to be necessary for normal radio operation has no way to know that it is only required for an optional peripheral.

The intent of the code is entirely benign — kIOHIDOptionsTypeSeizeDevice is the correct pattern for preventing dial events from leaking into other apps as spurious keystrokes. The problem is that it fires unconditionally.

Proposed fix

Add a user-visible setting (e.g. Enable Ulanzi Dial, default off) and gate the start() call on it:

if (AppSettings::instance().ulanziDialEnabled())
    QMetaObject::invokeMethod(m_dialBackend, &UlanziDialBackend::start, ...);

Users who enable the feature will see the Input Monitoring prompt exactly once, understand why it is needed, and can grant it knowingly. Users who never enable it will never see it.

The same audit should be applied to HidEncoderManager (StreamDeck+ support), which also calls hid_open() — HIDAPI uses kIOHIDOptionsTypeSeizeDevice internally on macOS when opening a device.

Reproduction

Build and run AetherSDR from any commit after #3236 on macOS. On first launch, macOS presents the Input Monitoring permission dialog for AetherSDR.app. No Ulanzi Dial hardware is required to trigger the prompt.

Workaround

Deny the permission. AetherSDR functions normally without it unless a Ulanzi Dial is connected and in use.


Regression introduced by #3236.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingexternal devicesFlexControl, MIDI, serial port, USB peripheralsmacOSmacOS-specific issuemaintainer-reviewRequires maintainer review before any action is takenpriority: highHigh priority

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions