This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch tymux
in repository terminology.
View the commit online.
commit 5005b72f0492de2e311a5fafd3ba138a915d973b
Author: [email protected] <[email protected]>
AuthorDate: Wed Mar 18 22:24:02 2026 -0600
fix: address memory leaks, race conditions, and protocol issues from code review
Multiple fixes to tymux/termsrv protocol handling, session lifecycle, and error
paths discovered in comprehensive code review:
- Memory leak: validate msg type before malloc to avoid leak on invalid type
- Client race: use MSG_WAITALL in accept handler to prevent EAGAIN on ATTACH
- Stale socket: probe with real connect() instead of stat() to detect crashed daemons
- Protocol: NUL-terminate TITLE payload to prevent buffer overread
- Efficiency: remove redundant flush after resize (already flushed by callback)
- Robustness: use MSG_NOSIGNAL on attach message send; detect partial sendmsg
- API cleanup: remove unused ts param from _shadow_pty_new
- Environment: export TYMUX_SESSION before exec for nested session detection
Changes:
- termsrv.h: move TERMSRV_MAX_MSG_PAYLOAD to shared header for reuse in tymuxd
- termsrv.c: validate type before malloc, add MSG_NOSIGNAL, check exact sendmsg bytes
- tymux_session.c: free payload on recv error, remove double flush on resize
- tymuxd.c: use MSG_WAITALL for ATTACH handshake, drain payload with bounds check
- termsession.c: replace stat() probing with real connect() in _ensure_daemon,
NUL-terminate TITLE payload, remove unused parameter from _shadow_pty_new
- tymux.c: setenv TYMUX_SESSION before exec in both new and attach paths
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---
src/bin/termsession.c | 65 ++++++++++++++++++++++++++++++++++++++-----------
src/bin/termsrv.c | 15 ++++++------
src/bin/termsrv.h | 6 +++++
src/bin/tymux.c | 6 +++++
src/bin/tymux_session.c | 5 +++-
src/bin/tymuxd.c | 31 ++++++++++++++++-------
6 files changed, 96 insertions(+), 32 deletions(-)
diff --git a/src/bin/termsession.c b/src/bin/termsession.c
index 6544739c..81438614 100644
--- a/src/bin/termsession.c
+++ b/src/bin/termsession.c
@@ -18,7 +18,6 @@
#include <fcntl.h>
#include <stdatomic.h>
#include <time.h>
-#include <sys/stat.h>
/* ── write_cb: forward input bytes to daemon via TSRV_MSG_INPUT ─────── */
@@ -38,7 +37,7 @@ _session_write_cb(const char *buf, int len, void *data)
/* ── shadow Termpty ─────────────────────────────────────────────────── */
static Termpty *
-_shadow_pty_new(TermSession *ts, int cols, int rows, Config *config)
+_shadow_pty_new(int cols, int rows, Config *config)
{
Termpty *ty;
@@ -230,7 +229,12 @@ _cb_session_data(void *data, Ecore_Fd_Handler *handler EINA_UNUSED)
case TSRV_MSG_TITLE:
if (payload && size > 0)
- eina_stringshare_replace(&ts->shadow_pty->prop.title, payload);
+ {
+ /* Force NUL-termination in case a malformed message omitted it,
+ * preventing eina_stringshare_replace from reading past the buffer. */
+ ((char *)payload)[size - 1] = '\0';
+ eina_stringshare_replace(&ts->shadow_pty->prop.title, payload);
+ }
if (ts->cb_title.func)
ts->cb_title.func(ts->cb_title.data);
break;
@@ -265,14 +269,31 @@ _cb_session_data(void *data, Ecore_Fd_Handler *handler EINA_UNUSED)
static int
_ensure_daemon(const char *session_name, const char *sock_path)
{
- struct timespec ts_sleep;
- int i;
- pid_t child;
- struct stat st;
+ struct timespec ts_sleep;
+ int i;
+ pid_t child;
- /* If socket already exists, nothing to do */
- if (stat(sock_path, &st) == 0)
- return 0;
+ /* Probe with a real connect so we distinguish a live daemon from a stale
+ * socket file left by a crashed one. stat() succeeds for both, but
+ * connect() returns ECONNREFUSED for the stale case. */
+ {
+ int probe = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (probe >= 0)
+ {
+ struct sockaddr_un sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sun_family = AF_UNIX;
+ strncpy(sa.sun_path, sock_path, sizeof(sa.sun_path) - 1);
+ if (connect(probe, (struct sockaddr *)&sa, sizeof(sa)) == 0)
+ {
+ close(probe);
+ return 0; /* live daemon already accepting */
+ }
+ if (errno == ECONNREFUSED)
+ unlink(sock_path); /* stale socket from a previous crash */
+ close(probe);
+ }
+ }
child = fork();
if (child < 0)
@@ -313,15 +334,31 @@ _ensure_daemon(const char *session_name, const char *sock_path)
/* Parent: reap the short-lived first child immediately */
waitpid(child, NULL, 0);
- /* Poll for socket with 10 ms intervals, 3 s total */
+ /* Poll for the daemon to start accepting, using connect probes.
+ * This correctly distinguishes a live socket from a stale one. */
ts_sleep.tv_sec = 0;
ts_sleep.tv_nsec = 10 * 1000 * 1000; /* 10 ms */
for (i = 0; i < 300; i++)
{
+ int probe;
+ struct sockaddr_un sa;
+
nanosleep(&ts_sleep, NULL);
- if (stat(sock_path, &st) == 0)
- return 0;
+
+ probe = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (probe < 0) continue;
+
+ memset(&sa, 0, sizeof(sa));
+ sa.sun_family = AF_UNIX;
+ strncpy(sa.sun_path, sock_path, sizeof(sa.sun_path) - 1);
+
+ if (connect(probe, (struct sockaddr *)&sa, sizeof(sa)) == 0)
+ {
+ close(probe);
+ return 0;
+ }
+ close(probe);
}
ERR("termsession: daemon did not create socket within 3 s: %s", sock_path);
@@ -427,7 +464,7 @@ termsession_attach(const char *session_name,
/* Use requested cols/rows for the shadow pty; _sync_shadow will
* adjust to whatever the server reports in the header. */
- ty = _shadow_pty_new(NULL, cols, rows, config);
+ ty = _shadow_pty_new(cols, rows, config);
if (!ty)
{
ERR("termsession: failed to allocate shadow pty");
diff --git a/src/bin/termsrv.c b/src/bin/termsrv.c
index 69adb7d9..6e113d05 100644
--- a/src/bin/termsrv.c
+++ b/src/bin/termsrv.c
@@ -12,10 +12,6 @@
#include "termsrv.h"
-/* Sanity cap on incoming message payload size. No tymux message should
- * ever approach this; it guards against a malformed hdr.size of ~4 GB. */
-#define TERMSRV_MAX_MSG_PAYLOAD (1u << 20) /* 1 MiB */
-
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
@@ -74,8 +70,9 @@ termsrv_msg_send(int fd, TermSrvMsgType type,
};
int iovcnt = (size && payload) ? 2 : 1;
struct msghdr msg = { .msg_iov = iov, .msg_iovlen = (size_t)iovcnt };
+ ssize_t expected = (ssize_t)(sizeof(hdr) + ((payload && size) ? size : 0));
ssize_t n = sendmsg(fd, &msg, MSG_NOSIGNAL);
- return (n < 0) ? -1 : 0;
+ return (n == expected) ? 0 : -1;
}
/*
@@ -154,6 +151,10 @@ termsrv_msg_recv(int fd, void **payload_out, uint32_t *size_out)
return -1;
}
+ /* Reject unknown message types before allocating — valid range is
+ * ATTACH(1)..EXIT(9). Checking type first prevents a malloc that
+ * would otherwise leak on the error path. */
+ if (hdr.type == 0 || hdr.type > 9) return -1;
if (hdr.size > TERMSRV_MAX_MSG_PAYLOAD) return -1;
if (hdr.size > 0)
@@ -164,8 +165,6 @@ termsrv_msg_recv(int fd, void **payload_out, uint32_t *size_out)
{ free(buf); return -1; }
*payload_out = buf;
}
- /* Reject unknown message types; valid range is ATTACH(1)..EXIT(9). */
- if (hdr.type == 0 || hdr.type > 9) return -1;
*size_out = hdr.size;
return (int)hdr.type;
@@ -197,7 +196,7 @@ termsrv_send_attached(int sock_fd, int shm_fd,
cm->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(cm), &shm_fd, sizeof(int));
- return (sendmsg(sock_fd, &msg, 0) < 0) ? -1 : 0;
+ return (sendmsg(sock_fd, &msg, MSG_NOSIGNAL) < 0) ? -1 : 0;
}
int
diff --git a/src/bin/termsrv.h b/src/bin/termsrv.h
index 7ba18a55..c6b7de16 100644
--- a/src/bin/termsrv.h
+++ b/src/bin/termsrv.h
@@ -85,6 +85,12 @@ typedef struct {
int32_t exit_code;
} TermSrvMsgExit;
+/* ── payload size limit ─────────────────────────────────────────── */
+
+/* Sanity cap on incoming message payload size. No tymux message should
+ * ever approach this; it guards against a malformed hdr.size of ~4 GB. */
+#define TERMSRV_MAX_MSG_PAYLOAD (1u << 20) /* 1 MiB */
+
/* ── session name ───────────────────────────────────────────────── */
/* Maximum length of a session name (excluding null terminator).
diff --git a/src/bin/tymux.c b/src/bin/tymux.c
index 440d14dc..44933765 100644
--- a/src/bin/tymux.c
+++ b/src/bin/tymux.c
@@ -228,6 +228,9 @@ cmd_new(int argc, char **argv)
printf("Session '%s' created.\n", name);
+ /* Advertise the session name so nested tymux invocations can warn. */
+ setenv("TYMUX_SESSION", name, 1);
+
/* Replace this process with terminology, connecting to the new session. */
execlp("terminology", "terminology", "--session", name, (char *)NULL);
perror("tymux: exec terminology");
@@ -266,6 +269,9 @@ cmd_attach(int argc, char **argv)
return 1;
}
+ /* Advertise the session name so nested tymux invocations can warn. */
+ setenv("TYMUX_SESSION", name, 1);
+
execlp("terminology", "terminology", "--session", name, (char *)NULL);
perror("tymux: exec terminology");
return 1;
diff --git a/src/bin/tymux_session.c b/src/bin/tymux_session.c
index 82c8b57b..28935210 100644
--- a/src/bin/tymux_session.c
+++ b/src/bin/tymux_session.c
@@ -201,6 +201,7 @@ _cb_client_data(void *data, Ecore_Fd_Handler *fd_handler EINA_UNUSED)
type = termsrv_msg_recv(client->fd, &payload, &size);
if (type < 0)
{
+ free(payload);
_client_remove(sess, client);
return ECORE_CALLBACK_CANCEL;
}
@@ -221,8 +222,10 @@ _cb_client_data(void *data, Ecore_Fd_Handler *fd_handler EINA_UNUSED)
if (cols > 0 && rows > 0 &&
cols <= TERMSRV_MAX_COLS && rows <= TERMSRV_MAX_ROWS)
{
+ /* termpty_resize triggers _pty_change_cb which flushes
+ * to shm and sends NOTIFY to all clients — no explicit
+ * _flush_screen_to_shm call needed here. */
termpty_resize(sess->pty, (int)cols, (int)rows);
- _flush_screen_to_shm(sess);
}
}
break;
diff --git a/src/bin/tymuxd.c b/src/bin/tymuxd.c
index 97c25928..89b7cab5 100644
--- a/src/bin/tymuxd.c
+++ b/src/bin/tymuxd.c
@@ -32,10 +32,9 @@ static Eina_Bool
_cb_client_connect(void *data EINA_UNUSED,
Ecore_Fd_Handler *fd_handler EINA_UNUSED)
{
- int cfd;
- void *payload = NULL;
- uint32_t size = 0;
- int type;
+ int cfd;
+ TermSrvMsgHdr hdr;
+ ssize_t n;
cfd = accept(_listen_fd, NULL, NULL);
if (cfd < 0)
@@ -45,16 +44,30 @@ _cb_client_connect(void *data EINA_UNUSED,
return ECORE_CALLBACK_RENEW;
}
- type = termsrv_msg_recv(cfd, &payload, &size);
- if (type != TSRV_MSG_ATTACH)
+ /* Read the ATTACH header with MSG_WAITALL so we do not race against the
+ * kernel delivering the bytes. The newly-accepted fd may not have data
+ * ready yet when accept() returns, so MSG_DONTWAIT (used by the normal
+ * termsrv_msg_recv path) would return EAGAIN → type 0 → drop the client
+ * silently. */
+ n = recv(cfd, &hdr, sizeof(hdr), MSG_WAITALL);
+ if (n != (ssize_t)sizeof(hdr) || hdr.type != TSRV_MSG_ATTACH)
{
- ERR("expected ATTACH from new client (got %d), closing fd %d", type, cfd);
- free(payload);
+ ERR("expected ATTACH from new client (hdr.type=%u n=%zd), closing fd %d",
+ (n == (ssize_t)sizeof(hdr)) ? hdr.type : 0u, n, cfd);
close(cfd);
return ECORE_CALLBACK_RENEW;
}
- free(payload);
+ /* Drain any payload the client sent with the ATTACH (session name). */
+ if (hdr.size > 0 && hdr.size <= TERMSRV_MAX_MSG_PAYLOAD)
+ {
+ void *tmp = malloc(hdr.size);
+ if (tmp)
+ {
+ recv(cfd, tmp, hdr.size, MSG_WAITALL);
+ free(tmp);
+ }
+ }
termd_session_client_add(_session, cfd);
return ECORE_CALLBACK_RENEW;
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.