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 75959e14c7698c80f72c2cc2f90a345ae7614058
Author: [email protected] <[email protected]>
AuthorDate: Sun Mar 22 13:35:04 2026 -0600

    fix: address review issues in tymux backlog implementation
    
    Fix struct alignment (explicit pad + static assert), make num_lines
    atomic with acquire/release semantics, fix buffer fetch into wrong
    array slot, update proxy message type range, clamp buffer requests
    to avoid evicted IDs, use two-stage mmap for correct buffer sizing,
    guard write_seq increment, use uint32_t consistently for buffer IDs,
    and improve forward compatibility of recv payload checks.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---
 src/bin/termd_tymux.c |  50 +++++++++++------
 src/bin/termsrv.c     |  10 ++--
 src/bin/termsrv.h     |   7 ++-
 src/bin/termtymux.c   | 152 ++++++++++++++++++++++++++++++++++++--------------
 src/bin/tymux.c       |   2 +-
 5 files changed, 153 insertions(+), 68 deletions(-)

diff --git a/src/bin/termd_tymux.c b/src/bin/termd_tymux.c
index 857d6ebb..d83c4e88 100644
--- a/src/bin/termd_tymux.c
+++ b/src/bin/termd_tymux.c
@@ -72,8 +72,8 @@ struct _TermDTymux
    }              *backlog_bufs;         /* dynamic array of buffers */
    int             backlog_buf_count;    /* number of buffers in chain */
    int             backlog_buf_alloc;    /* allocated array slots */
-   int             oldest_buf_id;        /* global id of oldest buffer */
-   int             next_buf_id;          /* next global id to assign */
+   uint32_t        oldest_buf_id;        /* global id of oldest buffer */
+   uint32_t        next_buf_id;          /* next global id to assign */
    size_t          last_flushed_backpos; /* backpos at last flush */
 };
 
@@ -156,7 +156,7 @@ _backlog_buf_new(TermDTymux *sess, int cols)
    hdr->magic     = TERMSRV_BACKLOG_MAGIC;
    hdr->version   = TERMSRV_BACKLOG_VERSION;
    hdr->cols      = (uint32_t)cols;
-   hdr->num_lines = 0;
+   atomic_store_explicit(&hdr->num_lines, 0, memory_order_relaxed);
    hdr->max_lines = TERMSRV_BACKLOG_MAX_LINES;
    atomic_store_explicit(&hdr->write_seq, 0, memory_order_relaxed);
 
@@ -191,6 +191,7 @@ _flush_backlog_to_shm(TermDTymux *sess)
    size_t   backsize     = ty->backsize;
    size_t   new_lines;
    size_t   i;
+   int      lines_written = 0;
 
    if (cur_backpos == old_backpos)
      return;
@@ -226,18 +227,22 @@ _flush_backlog_to_shm(TermDTymux *sess)
 
         /* get or create a head buffer with matching cols */
         hdr = sess->backlog_bufs[sess->backlog_buf_count - 1].hdr;
-        if (hdr->num_lines >= hdr->max_lines || (int)hdr->cols != ty->w)
-          {
-             if (_backlog_buf_new(sess, ty->w) < 0)
-               break;
-             hdr = sess->backlog_bufs[sess->backlog_buf_count - 1].hdr;
-          }
+        {
+           uint32_t cur_num = atomic_load_explicit(&hdr->num_lines,
+                                                   memory_order_relaxed);
+           if (cur_num >= hdr->max_lines || (int)hdr->cols != ty->w)
+             {
+                if (_backlog_buf_new(sess, ty->w) < 0)
+                  break;
+                hdr = sess->backlog_bufs[sess->backlog_buf_count - 1].hdr;
+             }
+        }
 
         ts = BACKLOG_ROW_GET(ty, (int)i);
         if (!ts || !ts->cells)
           continue;
 
-        line_idx  = hdr->num_lines;
+        line_idx  = atomic_load_explicit(&hdr->num_lines, memory_order_relaxed);
         dst       = TERMSRV_BACKLOG_LINE(hdr, line_idx);
         copy_cols = (ts->w < hdr->cols) ? (uint32_t)ts->w : hdr->cols;
 
@@ -247,7 +252,9 @@ _flush_backlog_to_shm(TermDTymux *sess)
                  (size_t)(hdr->cols - copy_cols) * sizeof(Termcell));
 
         hdr->line_widths[line_idx] = copy_cols;
-        hdr->num_lines++;
+        atomic_store_explicit(&hdr->num_lines, line_idx + 1,
+                              memory_order_relaxed);
+        lines_written++;
      }
 
    /* evict oldest buffers while total lines exceed configured scrollback,
@@ -256,18 +263,24 @@ _flush_backlog_to_shm(TermDTymux *sess)
       int    total_lines = 0;
       int    b;
       for (b = 0; b < sess->backlog_buf_count; b++)
-        total_lines += (int)sess->backlog_bufs[b].hdr->num_lines;
+        total_lines += (int)atomic_load_explicit(
+                           &sess->backlog_bufs[b].hdr->num_lines,
+                           memory_order_relaxed);
 
       while (total_lines > sess->config->scrollback &&
              sess->backlog_buf_count > 1)
         {
-           total_lines -= (int)sess->backlog_bufs[0].hdr->num_lines;
+           total_lines -= (int)atomic_load_explicit(
+                              &sess->backlog_bufs[0].hdr->num_lines,
+                              memory_order_relaxed);
            _backlog_buf_evict_oldest(sess);
         }
    }
 
-   /* bump head buffer write_seq with release semantics */
-   if (sess->backlog_buf_count > 0)
+   /* bump head buffer write_seq with release semantics, but only when at
+    * least one line was actually appended — a no-op flush must not advance
+    * the counter and cause the client to re-read an unchanged buffer */
+   if (lines_written > 0 && sess->backlog_buf_count > 0)
      {
         TermBacklogBufHeader *head =
            sess->backlog_bufs[sess->backlog_buf_count - 1].hdr;
@@ -345,7 +358,7 @@ _pty_change_cb(void *data)
    pay.write_seq        = atomic_load_explicit(&sess->shm->write_seq,
                                                memory_order_acquire);
    pay.backlog_buf_count = (uint32_t)sess->backlog_buf_count;
-   pay.oldest_buf_id     = (uint32_t)sess->oldest_buf_id;
+   pay.oldest_buf_id     = sess->oldest_buf_id;
    if (sess->backlog_buf_count > 0)
      {
         TermBacklogBufHeader *head =
@@ -456,7 +469,8 @@ _cb_client_data(void *data, Ecore_Fd_Handler *fd_handler EINA_UNUSED)
                                             sess->backlog_bufs[idx].fd,
                                             req->buf_id,
                                             h->cols,
-                                            h->num_lines,
+                                            atomic_load_explicit(&h->num_lines,
+                                                                 memory_order_relaxed),
                                             h->max_lines);
                 }
            }
@@ -661,7 +675,7 @@ termd_tymux_client_add(TermDTymux *sess, int cfd)
                                 (uint32_t)sess->pty->h,
                                 (uint32_t)sess->backlog_buf_count,
                                 TERMSRV_BACKLOG_MAX_LINES,
-                                (uint32_t)sess->oldest_buf_id) < 0)
+                                sess->oldest_buf_id) < 0)
         {
            ERR("termsrv_send_attached failed for fd %d", cfd);
            close(cfd);
diff --git a/src/bin/termsrv.c b/src/bin/termsrv.c
index 60a24cc5..6f299690 100644
--- a/src/bin/termsrv.c
+++ b/src/bin/termsrv.c
@@ -210,7 +210,8 @@ termsrv_send_attached(int sock_fd, int shm_fd, int backlog_fd,
     if (nfds == 2)
       memcpy((char *)CMSG_DATA(cm) + sizeof(int), &backlog_fd, sizeof(int));
 
-    return (sendmsg(sock_fd, &msg, MSG_NOSIGNAL) < 0) ? -1 : 0;
+    ssize_t n = sendmsg(sock_fd, &msg, MSG_NOSIGNAL);
+    return (n == (ssize_t)(sizeof(hdr) + sizeof(pl))) ? 0 : -1;
 }
 
 int
@@ -242,7 +243,7 @@ termsrv_recv_attached(int sock_fd, int *shm_fd_out, int *backlog_fd_out,
     if (n < (ssize_t)(sizeof(hdr) + sizeof(pl))) return -1;
     if (msg.msg_flags & MSG_CTRUNC) return -1;
     if (hdr.type != TSRV_MSG_ATTACHED) return -1;
-    if (hdr.size != sizeof(TermSrvMsgAttached)) return -1;
+    if (hdr.size < sizeof(TermSrvMsgAttached)) return -1;
 
     struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
     if (!cm || cm->cmsg_type != SCM_RIGHTS ||
@@ -292,7 +293,8 @@ termsrv_send_backlog_buf(int sock_fd, int buf_fd,
     cm->cmsg_len   = CMSG_LEN(sizeof(int));
     memcpy(CMSG_DATA(cm), &buf_fd, sizeof(int));
 
-    return (sendmsg(sock_fd, &msg, MSG_NOSIGNAL) < 0) ? -1 : 0;
+    ssize_t n = sendmsg(sock_fd, &msg, MSG_NOSIGNAL);
+    return (n == (ssize_t)(sizeof(hdr) + sizeof(pl))) ? 0 : -1;
 }
 
 int
@@ -320,7 +322,7 @@ termsrv_recv_backlog_buf(int sock_fd, int *buf_fd_out,
     if (n < (ssize_t)(sizeof(hdr) + sizeof(pl))) return -1;
     if (msg.msg_flags & MSG_CTRUNC) return -1;
     if (hdr.type != TSRV_MSG_BACKLOG_BUF) return -1;
-    if (hdr.size != sizeof(TermSrvMsgBacklogBuf)) return -1;
+    if (hdr.size < sizeof(TermSrvMsgBacklogBuf)) return -1;
 
     struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
     if (!cm || cm->cmsg_type != SCM_RIGHTS ||
diff --git a/src/bin/termsrv.h b/src/bin/termsrv.h
index 97921815..035db6c7 100644
--- a/src/bin/termsrv.h
+++ b/src/bin/termsrv.h
@@ -44,13 +44,16 @@ typedef struct {
     uint32_t          magic;       /* TERMSRV_BACKLOG_MAGIC            */
     uint32_t          version;     /* TERMSRV_BACKLOG_VERSION          */
     uint32_t          cols;        /* terminal width when buf created  */
-    uint32_t          num_lines;   /* lines currently stored           */
+    _Atomic uint32_t  num_lines;   /* lines currently stored           */
     uint32_t          max_lines;   /* capacity (TERMSRV_BACKLOG_MAX_LINES) */
-    uint8_t           _pad[3];
+    uint8_t           _pad[4];
     _Atomic uint64_t  write_seq;   /* head buffer only: incremented on append */
     uint32_t          line_widths[TERMSRV_BACKLOG_MAX_LINES];
 } TermBacklogBufHeader;
 
+_Static_assert(offsetof(TermBacklogBufHeader, write_seq) % 8 == 0,
+               "write_seq must be 8-byte aligned for atomic access");
+
 #define TERMSRV_BACKLOG_HEADER_SIZE \
     ((sizeof(TermBacklogBufHeader) + 4095u) & ~4095u)  /* round up to page */
 
diff --git a/src/bin/termtymux.c b/src/bin/termtymux.c
index 8861d1c5..72203836 100644
--- a/src/bin/termtymux.c
+++ b/src/bin/termtymux.c
@@ -103,8 +103,14 @@ _backlog_bufs_evict_old(TermTymux *ts, uint32_t new_oldest_id)
 }
 
 /*
- * Request and mmap a backlog buffer from the daemon, then push it into
- * ts->backlog_bufs.  Returns EINA_TRUE on success.
+ * Request and mmap a backlog buffer from the daemon.
+ *
+ * If slot >= 0, the fetched buffer fills the existing NULL entry at
+ * ts->backlog_bufs[slot] (called from _backlog_get_cb for a known-position
+ * unmapped buffer).  If slot < 0, the buffer is appended at the tail
+ * (called from the NOTIFY handler for new head buffers).
+ *
+ * Returns EINA_TRUE on success.
  *
  * NOTE: this temporarily clears O_NONBLOCK on the socket (via
  * termtymux_request_backlog_buf) and must only be called from the main
@@ -112,7 +118,7 @@ _backlog_bufs_evict_old(TermTymux *ts, uint32_t new_oldest_id)
  * non-blocking recv).
  */
 static Eina_Bool
-_backlog_buf_fetch(TermTymux *ts, uint32_t buf_id)
+_backlog_buf_fetch(TermTymux *ts, uint32_t buf_id, int slot)
 {
    int      fd = -1;
    uint32_t cols = 0, num_lines = 0, max_lines = 0;
@@ -150,10 +156,22 @@ _backlog_buf_fetch(TermTymux *ts, uint32_t buf_id)
         return EINA_FALSE;
      }
 
-   if (!_backlog_buf_push(ts, hdr, map_size, buf_id))
+   if (slot >= 0)
      {
-        munmap(ptr, map_size);
-        return EINA_FALSE;
+        /* Fill the pre-existing NULL slot directly — no array growth needed */
+        TermBacklogBuf *entry = &ts->backlog_bufs[slot];
+        entry->hdr  = hdr;
+        entry->size = map_size;
+        /* entry->buf_id is already set */
+     }
+   else
+     {
+        /* Append new head buffer */
+        if (!_backlog_buf_push(ts, hdr, map_size, buf_id))
+          {
+             munmap(ptr, map_size);
+             return EINA_FALSE;
+          }
      }
 
    return EINA_TRUE;
@@ -180,12 +198,20 @@ _backlog_update_backsize(TermTymux *ts)
 
         if (b->hdr)
           {
-             /* head buffer: num_lines is live (re-read atomically) */
-             total += b->hdr->num_lines;
+             /* head buffer: num_lines is live — acquire ensures we see all
+              * cell writes that happened before the daemon incremented it */
+             total += atomic_load_explicit(&b->hdr->num_lines,
+                                          memory_order_acquire);
           }
         else
           {
-             /* unmapped historical buffer: assume it was filled */
+             /* Unmapped historical buffer: assume fully filled.
+              * BUG-7: a buffer created due to a terminal resize may be only
+              * partially filled, making this an overcount.  The NOTIFY
+              * protocol does not carry per-buffer num_lines, so we have no
+              * way to know the true count until the buffer is fetched.  The
+              * overcount is cosmetic — the scrollbar range is slightly too
+              * large — and corrects itself once the buffer is fetched. */
              total += TERMSRV_BACKLOG_MAX_LINES;
           }
      }
@@ -233,7 +259,8 @@ _backlog_get_cb(void *data, int y, ssize_t *wret)
          * The head (newest, last in array) may be partially filled;
          * older buffers are assumed full if not yet mmap'd. */
         if (b->hdr)
-          n_lines = b->hdr->num_lines;   /* live read from mmap */
+          n_lines = atomic_load_explicit(&b->hdr->num_lines,
+                                         memory_order_acquire); /* live read */
         else if (i == ts->backlog_buf_count - 1)
           n_lines = 0; /* head buf should always be mmap'd */
         else
@@ -254,21 +281,26 @@ _backlog_get_cb(void *data, int y, ssize_t *wret)
         /* Ensure buffer is mmap'd */
         if (!b->hdr)
           {
-             if (!_backlog_buf_fetch(ts, b->buf_id))
+             /* Pass slot=i so _backlog_buf_fetch fills this existing entry
+              * directly rather than appending a duplicate at the tail. */
+             if (!_backlog_buf_fetch(ts, b->buf_id, i))
                return NULL;
-             /* _backlog_buf_fetch appended to end; our 'b' pointer is now
-              * stale only if the realloc moved the array.  Re-look-up. */
-             b = &ts->backlog_bufs[i];
+             b = &ts->backlog_bufs[i]; /* re-look-up in case array was reallocated */
           }
         hdr = b->hdr;
-        if (!hdr || hdr->num_lines == 0)
+        if (!hdr)
           return NULL;
-
-        /* line_idx: 0 = oldest, num_lines-1 = newest */
-        /* local_offset=1 → newest = num_lines-1 */
-        if (local_offset == 0 || local_offset > hdr->num_lines)
-          return NULL;
-        line_idx = hdr->num_lines - local_offset;
+        {
+           uint32_t nl = atomic_load_explicit(&hdr->num_lines,
+                                              memory_order_acquire);
+           if (nl == 0)
+             return NULL;
+           /* line_idx: 0 = oldest, nl-1 = newest */
+           /* local_offset=1 → newest = nl-1 */
+           if (local_offset == 0 || local_offset > nl)
+             return NULL;
+           line_idx = nl - local_offset;
+        }
 
         *wret = (ssize_t)hdr->line_widths[line_idx];
         return TERMSRV_BACKLOG_LINE(hdr, line_idx);
@@ -505,6 +537,11 @@ _cb_tymux_data(void *data, Ecore_Fd_Handler *handler EINA_UNUSED)
                      ts->oldest_buf_id = new_oldest;
                   }
 
+                /* Clamp next_new_id so we never request buffer ids that the
+                 * daemon has already evicted (can happen if we lag behind) */
+                if (next_new_id < new_oldest)
+                  next_new_id = new_oldest;
+
                 /* Fetch new head buffer(s) produced since the last NOTIFY.
                  * new_head = oldest + count - 1; we want every id from
                  * next_new_id through new_head. */
@@ -528,7 +565,7 @@ _cb_tymux_data(void *data, Ecore_Fd_Handler *handler EINA_UNUSED)
                                  }
                             }
                           if (!already)
-                            _backlog_buf_fetch(ts, id);
+                            _backlog_buf_fetch(ts, id, -1);
                        }
                   }
 
@@ -830,40 +867,69 @@ termtymux_attach(const char *tymux_name,
    ts->last_backlog_buf_count = backlog_buf_count;
 
    /* mmap the head backlog buffer received in ATTACHED.
-    * Map at maximum size so we don't need to do a two-stage map just to
-    * read the cols field.  Excess virtual pages have no physical cost. */
+    * Use a two-stage map: first map just the header page to read the actual
+    * cols width, then re-map at the correct total size.  Mapping at
+    * TERMSRV_BACKLOG_TOTAL_SIZE(TERMSRV_MAX_COLS) when the file is smaller
+    * (because the session was started with fewer columns) would cause SIGBUS
+    * on any access past the file's actual end. */
    if (backlog_fd >= 0 && backlog_buf_count > 0)
      {
-        size_t map_size = TERMSRV_BACKLOG_TOTAL_SIZE(TERMSRV_MAX_COLS);
-        void  *ptr;
+        TermBacklogBufHeader *hdr_tmp;
+        void                 *hdr_page;
+        uint32_t              head_cols = 0;
+        uint32_t              head_id   = oldest_buf_id + backlog_buf_count - 1;
 
-        ptr = mmap(NULL, map_size, PROT_READ, MAP_SHARED, backlog_fd, 0);
-        close(backlog_fd);
-        backlog_fd = -1;
-
-        if (ptr != MAP_FAILED)
+        hdr_page = mmap(NULL, TERMSRV_BACKLOG_HEADER_SIZE,
+                        PROT_READ, MAP_SHARED, backlog_fd, 0);
+        if (hdr_page != MAP_FAILED)
           {
-             TermBacklogBufHeader *hdr = ptr;
-             uint32_t              head_id;
+             hdr_tmp = hdr_page;
+             if (hdr_tmp->magic   == TERMSRV_BACKLOG_MAGIC &&
+                 hdr_tmp->version == TERMSRV_BACKLOG_VERSION &&
+                 hdr_tmp->cols    >  0 &&
+                 hdr_tmp->cols    <= TERMSRV_MAX_COLS)
+               head_cols = hdr_tmp->cols;
+             munmap(hdr_page, TERMSRV_BACKLOG_HEADER_SIZE);
+          }
+        else
+          WRN("termtymux: mmap head backlog buf header: %s", strerror(errno));
 
-             if (hdr->magic   == TERMSRV_BACKLOG_MAGIC &&
-                 hdr->version == TERMSRV_BACKLOG_VERSION)
+        if (head_cols > 0)
+          {
+             size_t map_size = TERMSRV_BACKLOG_TOTAL_SIZE(head_cols);
+             void  *ptr;
+
+             ptr = mmap(NULL, map_size, PROT_READ, MAP_SHARED, backlog_fd, 0);
+             close(backlog_fd);
+             backlog_fd = -1;
+
+             if (ptr != MAP_FAILED)
                {
-                  head_id = oldest_buf_id + backlog_buf_count - 1;
-                  if (!_backlog_buf_push(ts, hdr, map_size, head_id))
+                  TermBacklogBufHeader *hdr = ptr;
+
+                  if (hdr->magic   == TERMSRV_BACKLOG_MAGIC &&
+                      hdr->version == TERMSRV_BACKLOG_VERSION)
                     {
-                       WRN("termtymux: failed to push head backlog buf");
+                       if (!_backlog_buf_push(ts, hdr, map_size, head_id))
+                         {
+                            WRN("termtymux: failed to push head backlog buf");
+                            munmap(ptr, map_size);
+                         }
+                    }
+                  else
+                    {
+                       WRN("termtymux: head backlog buf magic/version mismatch");
                        munmap(ptr, map_size);
                     }
                }
              else
-               {
-                  WRN("termtymux: head backlog buf magic/version mismatch");
-                  munmap(ptr, map_size);
-               }
+               WRN("termtymux: mmap head backlog buf: %s", strerror(errno));
           }
         else
-          WRN("termtymux: mmap head backlog buf: %s", strerror(errno));
+          {
+             close(backlog_fd);
+             backlog_fd = -1;
+          }
      }
    else if (backlog_fd >= 0)
      {
diff --git a/src/bin/tymux.c b/src/bin/tymux.c
index 0040b137..966dbe7a 100644
--- a/src/bin/tymux.c
+++ b/src/bin/tymux.c
@@ -276,7 +276,7 @@ _proto_recv(int fd, void **payload_out, uint32_t *size_out)
           return -1;
      }
 
-   if (hdr.type == 0 || hdr.type > 9) return -1;
+   if (hdr.type == 0 || hdr.type > 11) return -1;
    if (hdr.size > (1u << 20)) return -1;
 
    if (hdr.size > 0)

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to