From 243a92798c3f4d2d42dc96a923579ccb615b614b Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 26 Apr 2026 17:04:22 +0200 Subject: [PATCH] fix macOS PTY: cmsghdr layout, signing, sandbox removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS cmsghdr uses socklen_t (4 bytes) for cmsg_len, not size_t (8 bytes like Linux). The Dart FFI struct was reading at wrong offsets causing SCM_RIGHTS fd transfer to fail. Added CmsghdrDarwin with Uint32 cmsg_len alongside the existing Linux layout. SOL_SOCKET is 0xffff on macOS (was hardcoded to 1 for Linux). Now platform-detected. Switched from ad-hoc to Apple Development signing with team ID. Removed app sandbox from entitlements — proper signing makes it unnecessary. All SBPL hacks eliminated. PtySession.spawn() runs recvFd in a child isolate to avoid blocking the backend isolate's event loop. Terminal testmode test now passes: pane.spawn via backend isolate successfully forks and execs the shell. Co-Authored-By: Claude Opus 4.6 (1M context) --- .pql/pql-plan.json | 2 +- lib/src/pty/ffi/libc.dart | 22 +++++++++++++++++--- lib/src/pty/ffi/scm_rights.dart | 28 +++++++++++++++++--------- macos/Runner.xcodeproj/project.pbxproj | 9 ++++++--- macos/Runner/DebugProfile.entitlements | 23 +-------------------- macos/Runner/Release.entitlements | 2 +- 6 files changed, 46 insertions(+), 40 deletions(-) diff --git a/.pql/pql-plan.json b/.pql/pql-plan.json index 5c270c70..a80abf10 100644 --- a/.pql/pql-plan.json +++ b/.pql/pql-plan.json @@ -1,5 +1,5 @@ { - "exported_at": "2026-04-26T13:55:29Z", + "exported_at": "2026-04-26T15:04:22Z", "decisions": [ { "id": "D-1", diff --git a/lib/src/pty/ffi/libc.dart b/lib/src/pty/ffi/libc.dart index f63d35a2..678edcb9 100644 --- a/lib/src/pty/ffi/libc.dart +++ b/lib/src/pty/ffi/libc.dart @@ -11,6 +11,7 @@ library; import 'dart:ffi' as ffi; +import 'dart:io' show Platform; import 'package:ffi/ffi.dart' as pkg_ffi; @@ -21,8 +22,8 @@ import 'package:ffi/ffi.dart' as pkg_ffi; const int afUnix = 1; const int sockStream = 1; -const int solSocket = 1; // Linux; macOS = 0xffff -const int scmRights = 1; +final int solSocket = Platform.isMacOS ? 0xffff : 1; +final int scmRights = Platform.isMacOS ? 0x01 : 1; const int fIoNonblock = 0x800; // O_NONBLOCK — 04000 octal const int fGetFl = 3; @@ -133,7 +134,10 @@ final class Msghdr extends ffi.Struct { /// POSIX `struct cmsghdr` prefix. We treat the rest of the control /// buffer as a raw byte region and compute offsets by hand. -final class Cmsghdr extends ffi.Struct { +// On Linux, cmsg_len is size_t (8 bytes on 64-bit). +// On macOS, cmsg_len is socklen_t (4 bytes, always). +// Use platform-specific structs. +final class CmsghdrLinux extends ffi.Struct { @ffi.IntPtr() external int cmsg_len; @ffi.Int32() @@ -142,6 +146,18 @@ final class Cmsghdr extends ffi.Struct { external int cmsg_type; } +final class CmsghdrDarwin extends ffi.Struct { + @ffi.Uint32() + external int cmsg_len; + @ffi.Int32() + external int cmsg_level; + @ffi.Int32() + external int cmsg_type; +} + +// Alias for backward compatibility — callers use Cmsghdr. +typedef Cmsghdr = CmsghdrLinux; + /// POSIX `struct winsize` for `TIOCSWINSZ`. final class Winsize extends ffi.Struct { @ffi.Uint16() diff --git a/lib/src/pty/ffi/scm_rights.dart b/lib/src/pty/ffi/scm_rights.dart index 5499e6ba..98763353 100644 --- a/lib/src/pty/ffi/scm_rights.dart +++ b/lib/src/pty/ffi/scm_rights.dart @@ -6,6 +6,7 @@ library; import 'dart:ffi' as ffi; +import 'dart:io' show Platform; import 'package:ffi/ffi.dart' as pkg_ffi; @@ -58,21 +59,28 @@ int recvFd(int socketFd) { ); } - // Parse the first cmsghdr out of the control buffer. We assume a - // single SCM_RIGHTS cmsg with one int of payload — that's what - // `ptyc` sends and all we ever ask for. - final hdr = control.cast().ref; - if (hdr.cmsg_level != libc.solSocket || hdr.cmsg_type != libc.scmRights) { + // Parse the first cmsghdr out of the control buffer. On macOS, + // cmsg_len is socklen_t (4 bytes); on Linux it's size_t (8 bytes). + int cmsgLevel, cmsgType, dataOffset; + if (Platform.isMacOS) { + final hdr = control.cast().ref; + cmsgLevel = hdr.cmsg_level; + cmsgType = hdr.cmsg_type; + dataOffset = ffi.sizeOf(); + } else { + final hdr = control.cast().ref; + cmsgLevel = hdr.cmsg_level; + cmsgType = hdr.cmsg_type; + dataOffset = ffi.sizeOf(); + } + + if (cmsgLevel != libc.solSocket || cmsgType != libc.scmRights) { throw PtyException( 'recvmsg', - 'unexpected cmsg level=${hdr.cmsg_level} type=${hdr.cmsg_type}', + 'unexpected cmsg level=$cmsgLevel type=$cmsgType', ); } - // CMSG_DATA starts at the first aligned boundary after the cmsghdr. - // On Linux/glibc that's sizeof(cmsghdr) == 16, which is 8-byte - // aligned already. We rely on that layout. - final dataOffset = ffi.sizeOf(); final fdPtr = (control + dataOffset).cast(); return fdPtr.value; } finally { diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 993c04b7..bfa11c96 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -525,7 +525,8 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_IDENTITY = "Apple Development"; + DEVELOPMENT_TEAM = 54XXM3ZQTX; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; @@ -601,7 +602,8 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_IDENTITY = "Apple Development"; + DEVELOPMENT_TEAM = 54XXM3ZQTX; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; @@ -657,7 +659,8 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_IDENTITY = "Apple Development"; + DEVELOPMENT_TEAM = 54XXM3ZQTX; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index d2fa9a4c..461123ca 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -2,30 +2,9 @@ - com.apple.security.app-sandbox - com.apple.security.cs.allow-jit - com.apple.security.network.server + com.apple.security.cs.disable-library-validation - com.apple.security.network.client - - com.apple.security.files.user-selected.read-write - - com.apple.security.temporary-exception.sbpl - - (allow process-exec* (literal "/bin/zsh")) - (allow process-exec* (literal "/usr/bin/which")) - (allow process-exec* (subpath "/Users/jeroenschweitzer/.local/bin")) - (allow process-exec* (subpath "/Users/jeroenschweitzer/Projects/clide/ptyc/bin")) - (allow process-exec* (subpath "/Users/jeroenschweitzer/Projects/clide/native/dugite")) - (allow process-fork) - (allow file-read* file-write* (subpath "/Users/jeroenschweitzer/Projects")) - (allow file-read* file-write* (subpath "/Users/jeroenschweitzer/projects")) - (allow file-read* (subpath "/opt/homebrew")) - (allow file-read* (subpath "/Users/jeroenschweitzer/.local")) - (allow file-read* (subpath "/Users/jeroenschweitzer/.pql")) - (allow file-read* file-write* (subpath "/private/tmp")) - diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 852fa1a4..8cc185af 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -2,7 +2,7 @@ - com.apple.security.app-sandbox + com.apple.security.cs.disable-library-validation