macOSUpdater ships a root LaunchDaemon that runs softwareupdate on behalf
of a user-owned trigger file. Security is the primary design goal: a bug in the
daemon potentially runs as root. This document describes what is defended, what
is explicitly out of scope, and how to report a vulnerability.
| Version | Supported |
|---|---|
| 1.0.0 | ✅ |
| < 1.0.0 | ❌ (pre-release snapshots) |
Please report security issues privately, not via public issues:
- Email:
micha.barth@bigas.ch
Include reproduction steps and the affected file/line. You will get an acknowledgement; fixes for confirmed root-relevant issues are prioritised.
The dangerous primitive is the root daemon consuming a user-controlled trigger. The boundary defended is: an unprivileged or non-owner local user must not be able to escalate to root through the daemon, its installer, or its uninstaller.
- TOCTOU on the trigger read. The daemon opens the trigger with
sysopen O_NOFOLLOW,fstats the file descriptor and reads from the same fd. Check (owner of the opened inode == expected install owner) and use hit the same inode — no symlink/inode-swap window. Symlink, missing file, owner mismatch and unresolvable owner each abort with a logged event. - Owner-verified constants source. The daemon sources
_constants.shonly after the sameO_NOFOLLOW+fstatclosure with owner == daemon EUID (root in production); the verified bytes are sourced from the fd itself. The daemon defends itself independently of where it is deployed. - Action whitelist. Only
macos_sw,macos_sw_restartandallare dispatched; any other payload logsunknown_actionand exits. Noeval, no command substitution on the trigger content — no shell injection as root. - PATH pinning. The daemon forces
PATH=/usr/bin:/bin:/usr/sbin:/sbin, closing PATH injection via user-writable/opt/homebrew/binetc. All external commands are called by absolute path. - Fixed install owner. The expected owner is baked at install time, not
derived from
$USER; another logged-in user cannot redirect the trigger. - Installer hardening. The owner name is validated against
^[A-Za-z0-9_.-]+$before being injected into the daemon and plist (sed-injection guard); symlink / root-owned pre-checks guard every privileged write. - Uninstaller allowlist. Every destructive
rm/rmdirvalidates its target (rejects empty input, path traversal, and protected system/top-level paths) before anysudo rmruns — a poisoned_constants.shcannot steer asudo rm -rfonto system paths. - Injection-safe logging. NDJSON output escapes free-text
msg, keepsuidnumeric, and emitsevent/actionas literal/whitelisted values.
- An attacker who already has write access to the user's tool files. Replacing the CLI/daemon source itself is the same trust boundary; the uninstaller allowlist mitigates the worst outcome but this is not treated as a crossed boundary.
- Compromise of root itself. If root is already controlled, the daemon offers no additional protection — that is outside the model.
- The log is world-readable (
/var/log/macOSUpdater.log, mode 644). It contains the triggering uid and an action history, but no secrets. Readable logging is intentional (forensics by design).