From 17775fa857ef4dd9edbe6991dec88dd5ed3bc364 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 19 Oct 2025 13:22:01 -0700 Subject: [PATCH] update comments --- src/cli/macos_disclaim.zig | 34 +++++++++++++++----- src/os/posix_spawn.zig | 64 +++++++++++++++++++++++++++++++------- src/termio/Exec.zig | 13 ++++++-- 3 files changed, 90 insertions(+), 21 deletions(-) diff --git a/src/cli/macos_disclaim.zig b/src/cli/macos_disclaim.zig index ead18524e..015a435cd 100644 --- a/src/cli/macos_disclaim.zig +++ b/src/cli/macos_disclaim.zig @@ -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; } diff --git a/src/os/posix_spawn.zig b/src/os/posix_spawn.zig index c1d673858..46ca3cf60 100644 --- a/src/os/posix_spawn.zig +++ b/src/os/posix_spawn.zig @@ -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); diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 4b3a8cc06..b82d53de2 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -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| {