fix macOS PTY: cmsghdr layout, signing, sandbox removal

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) <noreply@anthropic.com>
This commit is contained in:
Jeroen Schweitzer
2026-04-26 17:04:22 +02:00
co-authored by Claude Opus 4.6
parent 45e8132a41
commit 243a92798c
6 changed files with 46 additions and 40 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"exported_at": "2026-04-26T13:55:29Z",
"exported_at": "2026-04-26T15:04:22Z",
"decisions": [
{
"id": "D-1",
+19 -3
View File
@@ -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()
+18 -10
View File
@@ -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<libc.Cmsghdr>().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<libc.CmsghdrDarwin>().ref;
cmsgLevel = hdr.cmsg_level;
cmsgType = hdr.cmsg_type;
dataOffset = ffi.sizeOf<libc.CmsghdrDarwin>();
} else {
final hdr = control.cast<libc.CmsghdrLinux>().ref;
cmsgLevel = hdr.cmsg_level;
cmsgType = hdr.cmsg_type;
dataOffset = ffi.sizeOf<libc.CmsghdrLinux>();
}
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<libc.Cmsghdr>();
final fdPtr = (control + dataOffset).cast<ffi.Int32>();
return fdPtr.value;
} finally {
+6 -3
View File
@@ -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";
+1 -22
View File
@@ -2,30 +2,9 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.temporary-exception.sbpl</key>
<array>
<string>(allow process-exec* (literal "/bin/zsh"))</string>
<string>(allow process-exec* (literal "/usr/bin/which"))</string>
<string>(allow process-exec* (subpath "/Users/jeroenschweitzer/.local/bin"))</string>
<string>(allow process-exec* (subpath "/Users/jeroenschweitzer/Projects/clide/ptyc/bin"))</string>
<string>(allow process-exec* (subpath "/Users/jeroenschweitzer/Projects/clide/native/dugite"))</string>
<string>(allow process-fork)</string>
<string>(allow file-read* file-write* (subpath "/Users/jeroenschweitzer/Projects"))</string>
<string>(allow file-read* file-write* (subpath "/Users/jeroenschweitzer/projects"))</string>
<string>(allow file-read* (subpath "/opt/homebrew"))</string>
<string>(allow file-read* (subpath "/Users/jeroenschweitzer/.local"))</string>
<string>(allow file-read* (subpath "/Users/jeroenschweitzer/.pql"))</string>
<string>(allow file-read* file-write* (subpath "/private/tmp"))</string>
</array>
</dict>
</plist>
+1 -1
View File
@@ -2,7 +2,7 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>