From c8c0e1a7e72563a6b89b0ba8d0c4b08d5f9c163c Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Mon, 27 Jul 2026 16:40:04 +0000 Subject: [PATCH 13/13] terminal: open PTY slave once before first master ioctl (Darwin) On Darwin, a freshly posix_openpt()'d master isn't fully "live" for ioctls (confirmed for TIOCSWINSZ, the specific case foot hits) until the slave side has been opened at least once -- an undocumented XNU quirk, not something grantpt()/unlockpt() themselves gate (those only chmod/chown the slave device node and flag it unlocked for opening). slave.c's slave_exec() already does grantpt/unlockpt/open(slave) in the forked child right before exec(), but that happens well after term_init()'s initial TIOCSWINSZ call on the master, which needs the slave to have been opened at least once already. Do this unconditionally rather than gating on __APPLE__: opening a PTY slave and closing it immediately afterward is normal, portable, idempotent POSIX behavior (this is exactly what glibc/BSD openpty() does internally), so there's no reason to special-case it to one platform. Skipped when an explicit --pty path was given, since that's a pre-existing PTY device, not one foot itself just allocated. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01U6fuL1PtRJHhr97gAGyS1h --- terminal.c | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/terminal.c b/terminal.c index 292138cf..d02ceecb 100644 --- a/terminal.c +++ b/terminal.c @@ -4,6 +4,7 @@ #include #endif #include +#include #include #include #include @@ -1219,6 +1220,44 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, LOG_ERRNO("failed to open PTY"); goto close_fds; } + + if (pty_path == NULL) { + /* + * On Darwin, a freshly posix_openpt()'d master isn't fully + * "live" for ioctls (e.g. the TIOCSWINSZ below) until the + * slave side has been opened at least once -- an + * undocumented but confirmed XNU quirk, distinct from + * grantpt()/unlockpt()'s actual effect (which only chmod/ + * chown the slave device node and flag it unlocked for + * opening, neither of which gates ioctls on the master). + * slave.c's slave_exec() does grantpt/unlockpt/open(slave) + * again in the forked child before exec(), which is + * necessary there regardless and unaffected by also doing + * it once here first. + */ + if (grantpt(ptmx) == -1) { + LOG_ERRNO("failed to grantpt()"); + goto close_fds; + } + if (unlockpt(ptmx) == -1) { + LOG_ERRNO("failed to unlockpt()"); + goto close_fds; + } + + const char *pts_name = ptsname(ptmx); + if (pts_name == NULL) { + LOG_ERRNO("failed to ptsname()"); + goto close_fds; + } + + int pts = open(pts_name, O_RDWR | O_NOCTTY); + if (pts < 0) { + LOG_ERRNO("failed to open PTY slave"); + goto close_fds; + } + close(pts); + } + if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { LOG_ERRNO("failed to create flash timer FD"); goto close_fds; -- 2.43.0