Hello, appreciate any help here.
Objective: perform a scoped write to a removable block device (using low-level system frameworks in C).
Issue: launchd-run privileged helper (as root) denied permission to open block device. Manual 'sudo ./helper'
call succeeds, however.
Importantly: the entire process works flawlessly if the main app is granted Full Disk Access in Privacy & Security. However, this should be completely unnecessary for this objective, as scoped access should be sufficient, and FDA is in fact not required for other apps which perform this task.
Architecture and flow:
- Main GUI process collects ISO path and target removable device path (queried via IOKit).
- Main GUI process installs a Privileged Helper via SMJobBless.
- The Privileged Helper is started on demand by launchd as root (UID 0, EUID 0).
- Main GUI process communicates selected ISO and device paths to Privileged Helper via XPC.
- Privileged Helper conducts security and sanity checks, unmounts volumes from target device via DiskArbitration.
- Privileged Helper obtains file handles to ISO and target block device (e.g.: "/dev/disk4").
- Privileged Helper performs a byte-by-byte write to the target block device.
Problematic area:
Simplified example using C syscalls (via Zig):
const path = "/dev/disk5";
// Note that even with readonly flag this fails
const fd = c.open(path, c.O_RDONLY, @as(c_uint, 0));
defer _ = c.close(fd);
if (fd < 0) {
const err_num = c.__error().*;
const err_str = c.strerror(err_num);
log("open() failed with errno {}: {s}", .{ err_num, err_str });
}
Output (when run by launchd - UID 0, EUID 0, domain: system):
open() failed with errno 1: Operation not permitted
Simplified example with Zig open interface:
const directory = try std.fs.openDirAbsolute(deviceDir, .{ .no_follow = true });
const device = try directory.openFile("/dev/disk5", .{ .mode = .read_write, .lock = .exclusive });
errdefer device.close();
Output (when run by launchd - UID 0, EUID 0, domain: system):
Error: error.AccessDenied
Running the same examples by manually launching the binary with a test argument succeeds:
sudo ./helper "/dev/disk5"
...
Notable points:
- Both Main GUI process and the Privileged Helper binary are codesigned (via
codesign ...
). - Privileged Helper has both Info.plist and Launchd.plist symbols exported into its binary.
- Privileged Helper has no codesign flags (e.g.: for hardened runtime or others):
CodeDirectory v=20400 size=8130 flags=0x0(none) hashes=248+2 location=embedded
- Output of
sudo launchctl print system/<helper-bundle-id>
shows nothing of interest to indicate any security restrictions.
Appreciate any advice here!
I think I've finally and by accident achieved a solution. For those who come after...
- First, trigger the OS to intercept a C syscall on the main GUI process:
const fd = c.open("/dev/disk4", c.O_RDWR, @as(c_uint, 0));
// this should return an access denied error, that's OK
if (fd < 0) do_nothing() else _ = c.close(fd);
At this point, the OS should trigger an interactive dialog, requesting scoped permissions to Removable Volumes.
- At this point, the Privileged Helper should inherit the permission from the main GUI process and be permitted to write to block device. Repeat C
open()
syscall procedure on the helper for good measure. Here, however, do not ignoreAccess Denied
error.