update comments

macos-disclaim
Mitchell Hashimoto 2025-10-19 13:22:01 -07:00
parent 5156723070
commit 17775fa857
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
3 changed files with 90 additions and 21 deletions

View File

@ -13,10 +13,24 @@ pub const Options = struct {
};
/// The `_macos-disclaim` command is an internal-only Ghostty command that
/// is only available on macOS. It uses private posix_spawn APIs to
/// make the child process the "responssible process" in macOS so it is
/// in charge of its own TCC (permissions like Downloads folder access or
/// camera) and resource accounting rather than Ghostty.
/// is only available on macOS. It acts as a trampoline that uses private
/// posix_spawn APIs to make the child process the "responsible process" in
/// macOS so it is in charge of its own TCC (permissions like Downloads folder
/// access or camera) and resource accounting rather than Ghostty.
///
/// ## Responsible Process Concept
///
/// In macOS, the "responsible process" is a system-level attribution mechanism
/// used for TCC (Transparency, Consent, and Control) permissions and resource
/// accounting (energy, memory, etc.). When a process spawns children, those
/// children normally inherit the parent's responsible process designation. This
/// means when programs launched from Ghostty request permissions or consume
/// resources, macOS attributes those actions to Ghostty itself.
///
/// By using the private `responsibility_spawnattrs_setdisclaim` API, we make
/// the spawned process responsible for itself rather than being attributed to
/// Ghostty. This ensures that TCC prompts and resource accounting are correctly
/// associated with the actual program being run, not the terminal emulator.
pub fn run(alloc: Allocator) !u8 {
// This helper is only for Apple systems. POSIX in general has posix_spawn
// but we only use it on Apple platforms because it lets us shed our
@ -33,7 +47,7 @@ pub fn run(alloc: Allocator) !u8 {
_ = arg_iter.skip();
_ = arg_iter.skip();
// Collect remaining args for exec
// Collect remaining args for exec.
var args: std.ArrayList(?[*:0]const u8) = .empty;
defer args.deinit(alloc);
while (arg_iter.next()) |arg| try args.append(alloc, arg);
@ -47,7 +61,9 @@ pub fn run(alloc: Allocator) !u8 {
defer posix_spawn.spawn_attr.destroy(&attrs);
{
try posix_spawn.spawn_attr.setflags(&attrs, .{
// Act like exec(): replace this process.
// POSIX_SPAWN_SETEXEC is a macOS extension that makes posix_spawn
// behave like exec(), replacing the current process image with the
// spawned program.
.setexec = true,
});
@ -57,6 +73,8 @@ pub fn run(alloc: Allocator) !u8 {
try posix_spawn.spawn_attr.disclaim(&attrs, true);
}
// On success, this call DOES NOT RETURN because POSIX_SPAWN_SETEXEC
// replaces this process image. On failure, we log and return 1.
_ = posix_spawn.spawnp(
std.mem.span(args.items[0].?),
null,
@ -71,6 +89,8 @@ pub fn run(alloc: Allocator) !u8 {
return 1;
};
// We set the exec flag so we can't reach this point.
// Unreachable because POSIX_SPAWN_SETEXEC replaces this process on success.
// If we reach here, either the spawn failed (handled above) or the platform
// didn't honor SETEXEC (which would be a serious bug).
unreachable;
}

View File

@ -10,15 +10,34 @@ const errno = std.posix.errno;
const fd_t = std.posix.fd_t;
const pid_t = std.posix.pid_t;
// Zig's standard library doesn't yet wrap posix_spawnattr_setsigdefault,
// so we declare it here. This sets the signals that will be set to SIG_DFL
// in the spawned child process. See man 3 posix_spawnattr_setsigdefault
// for details. Only takes effect when used with POSIX_SPAWN_SETSIGDEF flag.
extern "c" fn posix_spawnattr_setsigdefault(
attr: *c.posix_spawnattr_t,
sigdefault: *const std.posix.sigset_t,
) c_int;
// This function is not part of any public Apple header and is not documented
// in man pages. It controls whether a spawned process inherits the "responsible
// process" designation from its parent for purposes of TCC (Transparency, Consent,
// and Control) permissions and resource accounting.
//
// When `disclaim` is true, the spawned process becomes responsible for itself
// rather than being attributed to the spawning process. This is critical for
// terminal emulators to avoid having all child processes' permission requests
// and resource usage attributed to the terminal itself.
//
// References:
// - https://www.qt.io/blog/the-curious-case-of-the-responsible-process
// - Reverse-engineered from various open source projects
extern "c" fn responsibility_spawnattrs_setdisclaim(
attrs: *const c.posix_spawnattr_t,
disclaim: bool,
) c_int;
/// Spawn a new process using PATH resolution.
pub fn spawnp(
path: [:0]const u8,
actions: ?*file_actions.T,
@ -91,6 +110,10 @@ pub const file_actions = struct {
};
}
/// Change working directory in the spawned process.
///
/// Uses the non-portable (_np suffix) addchdir function which is available
/// on Darwin and some other platforms.
pub fn chdir(
actions: *T,
path: [*:0]const u8,
@ -136,6 +159,14 @@ pub const spawn_attr = struct {
};
}
/// Set signals to default (SIG_DFL) in the spawned process.
///
/// This function sets which signals should be reset to their default
/// handlers in the child process. Only takes effect when the
/// POSIX_SPAWN_SETSIGDEF flag is set in the spawn attributes.
///
/// This is typically paired with Flags.setsigdef = true to ensure
/// the child doesn't inherit custom signal handlers from the parent.
pub fn setsigdefault(attr: *T, sigdefault: *const std.posix.sigset_t) UnexpectedError!void {
return switch (errno(posix_spawnattr_setsigdefault(
attr,
@ -146,8 +177,9 @@ pub const spawn_attr = struct {
};
}
/// This is undocumented, private API, so I'll link to some resources
/// here: https://www.qt.io/blog/the-curious-case-of-the-responsible-process
/// Set the "disclaim" flag for macOS responsible process handling.
///
/// See: https://www.qt.io/blog/the-curious-case-of-the-responsible-process
pub fn disclaim(attr: *T, v: bool) UnexpectedError!void {
return switch (errno(responsibility_spawnattrs_setdisclaim(
attr,
@ -159,20 +191,24 @@ pub const spawn_attr = struct {
}
};
/// POSIX spawn flags with Apple/Darwin extensions.
///
/// Note: Several fields are Apple-specific extensions and will not work on
/// other POSIX systems.
pub const Flags = packed struct(c_short) {
resetids: bool = false,
setpgroup: bool = false,
setsigdef: bool = false,
setsigmask: bool = false,
resetids: bool = false, // Reset effective UID/GID to real UID/GID
setpgroup: bool = false, // Set process group
setsigdef: bool = false, // Reset signals to SIG_DFL (see setsigdefault)
setsigmask: bool = false, // Set signal mask in child
_pad1: u2 = 0,
setexec: bool = false,
start_suspended: bool = false,
disable_aslr: bool = false,
setexec: bool = false, // Replace current process image (like exec)
start_suspended: bool = false, // Start process suspended (debugging)
disable_aslr: bool = false, // Disable ASLR for spawned process
_pad2: u1 = 0,
setsid: bool = false,
reslide: bool = false,
setsid: bool = false, // Create new session (process becomes session leader)
reslide: bool = false, // Re-randomize ASLR slide
_pad3: u2 = 0,
cloexec_default: bool = false,
cloexec_default: bool = false, // Default file descriptors to close-on-exec
_pad4: u1 = 0,
/// Integer value of this struct.
@ -223,6 +259,10 @@ test "spawn_attr.setsigdefault" {
}
test "spawn_attr.disclaim" {
// This test uses the private macOS API and will only pass on Darwin
const builtin = @import("builtin");
if (comptime builtin.os.tag != .macos) return error.SkipZigTest;
var attr = try spawn_attr.create();
defer spawn_attr.destroy(&attr);
try spawn_attr.disclaim(&attr, true);

View File

@ -1408,8 +1408,17 @@ fn execCommand(
var args: std.ArrayList([:0]const u8) = .empty;
defer args.deinit(alloc);
// If we're on macOS, we use the posix_spawn trampoline no matter
// what, so prepend that.
// On macOS, we ALWAYS route child processes through our internal
// +_macos-disclaim trampoline command. This trampoline uses posix_spawn
// with POSIX_SPAWN_SETEXEC and the private responsibility_spawnattrs_setdisclaim
// API to ensure the final process is not attributed to Ghostty for TCC
// permissions or resource accounting.
//
// The trampoline immediately replaces itself with the target command, so
// no intermediate process remains. The argv structure becomes:
// [ghostty_path, "+_macos-disclaim", actual_command, actual_args...]
//
// See src/cli/macos_disclaim.zig for the trampoline implementation.
if (comptime builtin.target.os.tag == .macos) {
var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
const exe_bin_path = std.fs.selfExePath(&exe_buf) catch |err| {