From fce2d4fbe07536031491b78bdaeb28467c0d2828 Mon Sep 17 00:00:00 2001 From: SB Date: Tue, 17 Mar 2026 19:13:41 +0300 Subject: [PATCH 1/2] fix(attach): replace RIS with explicit clear sequences to avoid terminal state leak ESC c (RIS) resets the entire terminal state (fonts, colors, modes), which leaks host terminal settings into the attached session. Use CSI H + CSI 2J + CSI 3J instead to clear screen and scrollback without resetting terminal configuration. Co-Authored-By: Claude Opus 4.6 (1M context) --- attach.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/attach.c b/attach.c index 2de74ac..58ad7f5 100644 --- a/attach.c +++ b/attach.c @@ -373,7 +373,7 @@ int attach_main(int noerror) ** the separator: the log ends at the exact pty cursor position, so ** the prompt is already visible and correctly placed. */ if (clear_method == CLEAR_MOVE && !no_ansiterm) { - write_buf_or_fail(1, "\033c", 2); + write_buf_or_fail(1, "\033[H\033[2J\033[3J", 14); } else if (!quiet && !skip_ring) { write_buf_or_fail(1, "\r\n", 2); } From 96d8f4328d50f03addaec8ac8b24cdc3307867e8 Mon Sep 17 00:00:00 2001 From: SB Date: Wed, 18 Mar 2026 18:40:57 +0300 Subject: [PATCH 2/2] fix: detach notification, log replay, self-attach check, and umask leak - Send MSG_DETACH packet to master before exiting on detach keypress - Seek to last SCROLLBACK_SIZE bytes when replaying large session logs - Move connect_socket before self-attach check so stale env vars from dead sessions don't block new attaches - Handle ENOENT separately from replay path for clearer error messages - Restore umask on every error path in create_socket, not just success Co-Authored-By: Claude Opus 4.6 (1M context) --- attach.c | 68 +++++++++++++++++++++++++++++++------------------------- master.c | 10 +++++++-- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/attach.c b/attach.c index 58ad7f5..6c66ca1 100644 --- a/attach.c +++ b/attach.c @@ -203,6 +203,8 @@ static void process_kbd(int s, struct packet *pkt) else if (pkt->u.buf[0] == detach_char) { char age[32]; session_age(age, sizeof(age)); + pkt->type = MSG_DETACH; + write_packet_or_fail(s, pkt); printf("%s[%s: session '%s' detached after %s]\r\n", clear_csi_data(), progname, session_shortname(), age); exit(0); @@ -239,6 +241,10 @@ int replay_session_log(int saved_errno) { unsigned char rbuf[BUFSIZE]; ssize_t n; + struct stat st; + + if (fstat(logfd, &st) == 0 && st.st_size > SCROLLBACK_SIZE) + lseek(logfd, st.st_size - SCROLLBACK_SIZE, SEEK_SET); while ((n = read(logfd, rbuf, sizeof(rbuf))) > 0) write(1, rbuf, (size_t)n); @@ -263,10 +269,40 @@ int attach_main(int noerror) fd_set readfds; int s; + /* Attempt to open the socket. Don't display an error if noerror is + ** set. */ + s = connect_socket(sockname); + if (s < 0) { + int saved_errno = errno; + const char *name = session_shortname(); + + if (!noerror) { + if (saved_errno == ENOENT) { + printf + ("%s: session '%s' does not exist\n", + progname, name); + } else if (!replay_session_log(saved_errno)) { + if (saved_errno == ECONNREFUSED) + printf + ("%s: session '%s' is not running\n", + progname, name); + else if (saved_errno == ENOTSOCK) + printf + ("%s: '%s' is not a valid session\n", + progname, name); + else + printf("%s: %s: %s\n", progname, + sockname, strerror(saved_errno)); + } + } + return 1; + } + /* Refuse to attach to any session in our ancestry chain (catches both * direct self-attach and indirect loops like A -> B -> A). * SESSION_ENVVAR is the colon-separated chain, so scanning it covers - * all ancestors. */ + * all ancestors. The check is done after connect so that a stale env + * var from a dead session does not block new attaches. */ { const char *tosearch = getenv(SESSION_ENVVAR); @@ -281,6 +317,7 @@ int attach_main(int noerror) if (tlen == slen && strncmp(p, sockname, tlen) == 0) { + close(s); if (!noerror) printf ("%s: cannot attach to session '%s' from within itself\n", @@ -295,35 +332,6 @@ int attach_main(int noerror) } } - /* Attempt to open the socket. Don't display an error if noerror is - ** set. */ - s = connect_socket(sockname); - if (s < 0) { - int saved_errno = errno; - const char *name = session_shortname(); - - if (!noerror) { - if (!replay_session_log(saved_errno)) { - if (saved_errno == ENOENT) - printf - ("%s: session '%s' does not exist\n", - progname, name); - else if (saved_errno == ECONNREFUSED) - printf - ("%s: session '%s' is not running\n", - progname, name); - else if (saved_errno == ENOTSOCK) - printf - ("%s: '%s' is not a valid session\n", - progname, name); - else - printf("%s: %s: %s\n", progname, - sockname, strerror(saved_errno)); - } - } - return 1; - } - /* Replay the on-disk log so the user sees full session history. ** Skip if already replayed by the error path (exited-session case). */ int skip_ring = 0; diff --git a/master.c b/master.c index 16f29e3..6fbb814 100644 --- a/master.c +++ b/master.c @@ -248,28 +248,34 @@ static int create_socket(char *name) omask = umask(077); s = socket(PF_UNIX, SOCK_STREAM, 0); - umask(omask); /* umask always succeeds, errno is untouched. */ - if (s < 0) + if (s < 0) { + umask(omask); return -1; + } sockun.sun_family = AF_UNIX; memcpy(sockun.sun_path, name, strlen(name) + 1); if (bind(s, (struct sockaddr *)&sockun, sizeof(sockun)) < 0) { close(s); + umask(omask); return -1; } if (listen(s, 128) < 0) { close(s); + umask(omask); return -1; } if (setnonblocking(s) < 0) { close(s); + umask(omask); return -1; } /* chmod it to prevent any surprises */ if (chmod(name, 0600) < 0) { close(s); + umask(omask); return -1; } + umask(omask); return s; }