From bba451c959e6ef62f8cb19e2b16da8e454d60d88 Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Mon, 12 Oct 2026 11:26:28 +0000 Subject: [PATCH 1/7] Give the server its own session keyring on Linux The server otherwise keeps the session keyring of the login session that started it, which pam_keyinit(8) may revoke when that session ends, leaving the server and its panes unable to use the kernel keyring. Join a new session keyring with the user keyring linked in once the configuration file has been loaded, and revoke it when the server exits. This is controlled by a new session-keyring server option, on by default. --- Makefile.am | 5 +++++ cfg.c | 4 ++++ compat.h | 6 ++++++ compat/keyring-linux.c | 42 ++++++++++++++++++++++++++++++++++++++++++ configure.ac | 7 +++++++ options-table.c | 8 ++++++++ server.c | 3 +++ tmux.1 | 8 ++++++++ 8 files changed, 83 insertions(+) create mode 100644 compat/keyring-linux.c diff --git a/Makefile.am b/Makefile.am index 349d6e224..a45ecd515 100644 --- a/Makefile.am +++ b/Makefile.am @@ -239,6 +239,11 @@ if NEED_FORKPTY nodist_tmux_SOURCES += compat/forkpty-@PLATFORM@.c endif +# Add compat file for Linux session keyrings. +if HAVE_KEYCTL +nodist_tmux_SOURCES += compat/keyring-linux.c +endif + # Add compat file for systemd. if HAVE_SYSTEMD nodist_tmux_SOURCES += compat/systemd.c diff --git a/cfg.c b/cfg.c index 31294f92a..17c01ad64 100644 --- a/cfg.c +++ b/cfg.c @@ -62,6 +62,10 @@ cfg_done(__unused struct cmdq_item *item, __unused void *data) } prompt_load_history(); +#ifdef HAVE_KEYCTL + if (options_get_number(global_options, "session-keyring")) + keyring_join(); +#endif return (CMD_RETURN_NORMAL); } diff --git a/compat.h b/compat.h index 66a22640b..0631b7252 100644 --- a/compat.h +++ b/compat.h @@ -441,6 +441,12 @@ void *reallocarray(void *, size_t, size_t); void *recallocarray(void *, size_t, size_t, size_t); #endif +#ifdef HAVE_KEYCTL +/* keyring-linux.c */ +void keyring_join(void); +void keyring_revoke(void); +#endif + #ifdef HAVE_SYSTEMD /* systemd.c */ int systemd_activated(void); diff --git a/compat/keyring-linux.c b/compat/keyring-linux.c new file mode 100644 index 000000000..e777bbbbb --- /dev/null +++ b/compat/keyring-linux.c @@ -0,0 +1,42 @@ +#include +#include + +#include +#include +#include +#include + +#include "tmux.h" + +/* Serial of the session keyring we joined, or 0 if none. */ +static long keyring_serial; + +/* + * Create a new session keyring since the current one belongs to the login + * session which we're meant to outlive (and pam_keyinit(8) is likely to revoke + * the login session's keyring when it ends). + */ +void +keyring_join(void) +{ + long serial; + + serial = syscall(SYS_keyctl, KEYCTL_JOIN_SESSION_KEYRING, NULL); + if (serial == -1) { + log_debug("%s: join failed: %s", __func__, strerror(errno)); + return; + } + if (syscall(SYS_keyctl, KEYCTL_LINK, KEY_SPEC_USER_KEYRING, + KEY_SPEC_SESSION_KEYRING) == -1) + log_debug("%s: link failed: %s", __func__, strerror(errno)); + keyring_serial = serial; +} + +void +keyring_revoke(void) +{ + if (keyring_serial == 0) + return; + if (syscall(SYS_keyctl, KEYCTL_REVOKE, keyring_serial) == -1) + log_debug("%s: revoke failed: %s", __func__, strerror(errno)); +} diff --git a/configure.ac b/configure.ac index 5da5f30af..139d93b88 100644 --- a/configure.ac +++ b/configure.ac @@ -795,6 +795,13 @@ else AC_LIBOBJ(daemon) fi +# Look for linux/keyctl.h, compat/keyring-linux.c used if found. +AC_CHECK_HEADER(linux/keyctl.h, found_keyctl=yes, found_keyctl=no) +if test "x$found_keyctl" = xyes; then + AC_DEFINE(HAVE_KEYCTL) +fi +AM_CONDITIONAL(HAVE_KEYCTL, [test "x$found_keyctl" = xyes]) + # Look for stravis, compat/{vis,unvis}.c used if missing. AC_CHECK_FUNC(stravis, found_stravis=yes, found_stravis=no) if test "x$found_stravis" = xyes; then diff --git a/options-table.c b/options-table.c index a65694973..813fc7b26 100644 --- a/options-table.c +++ b/options-table.c @@ -532,6 +532,14 @@ const struct options_table_entry options_table[] = { .text = "Maximum number of commands to keep in history." }, + { .name = "session-keyring", + .type = OPTIONS_TABLE_FLAG, + .scope = OPTIONS_TABLE_SERVER, + .default_num = 1, + .text = "Whether the server should join a new session keyring " + "(Linux only)." + }, + { .name = "set-clipboard", .type = OPTIONS_TABLE_CHOICE, .scope = OPTIONS_TABLE_SERVER, diff --git a/server.c b/server.c index be4427954..bad77f5b8 100644 --- a/server.c +++ b/server.c @@ -259,6 +259,9 @@ server_start(struct tmuxproc *client, uint64_t flags, struct event_base *base, job_kill_all(); prompt_save_history(); +#ifdef HAVE_KEYCTL + keyring_revoke(); +#endif exit(0); } diff --git a/tmux.1 b/tmux.1 index cf82a209f..b843d7e65 100644 --- a/tmux.1 +++ b/tmux.1 @@ -5096,6 +5096,14 @@ each client. .It Ic prompt\-history\-limit Ar number Set the number of history items to save in the history file for each type of command prompt. +.It Xo Ic session\-keyring +.Op Cm on | off +.Xc +On Linux, if enabled (the default), the server joins a new session keyring with +the user keyring linked into it, rather than keeping the session keyring of the +login session it was started from, which may be revoked when that session ends. +The new keyring is revoked when the server exits. +This option is checked once the configuration file has been loaded. .It Xo Ic set\-clipboard .Op Cm on | external | off .Xc From 3d0839b7a389ddea2d306142e4c705556b8849a9 Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Mon, 12 Oct 2026 11:26:28 +0000 Subject: [PATCH 2/7] Add a regress test for the server's session keyring on Linux Install keyutils in CI so the test runs rather than being skipped. --- .github/workflows/regress.yml | 1 + regress/session-keyring.sh | 89 +++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 regress/session-keyring.sh diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index ded64b7f9..5eb1f73c2 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -51,6 +51,7 @@ jobs: automake \ bison \ build-essential \ + keyutils \ libevent-dev \ libncurses-dev \ libutf8proc-dev \ diff --git a/regress/session-keyring.sh b/regress/session-keyring.sh new file mode 100644 index 000000000..8c854bdf6 --- /dev/null +++ b/regress/session-keyring.sh @@ -0,0 +1,89 @@ +#!/bin/sh + +# On Linux the server should join a session keyring of its own unless the +# session-keyring option is off, so it keeps working after the login session's +# keyring is revoked, and revoke it when it exits. + +PATH=/bin:/usr/bin +TERM=screen + +[ "$(uname)" = Linux ] || exit 0 +keyctl session - true >/dev/null 2>&1 || exit 0 + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +OUT=$(mktemp -d) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f$OUT/conf" +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +fail() +{ + echo "$*" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null + [ -s $OUT/holder ] && kill $(cat $OUT/holder) 2>/dev/null + rm -rf "$OUT" +} +trap cleanup 0 1 15 + +# Set KEYRING to the session keyring of a new pane on server $1. +pane_keyring() +{ + $1 new-window -d "keyctl id @s >$OUT/pane 2>&1; $1 wait-for -S pane" || + fail "could not create pane" + $1 wait-for pane + KEYRING=$(cat $OUT/pane) + case "$KEYRING" in + ''|*[!0-9]*) + fail "pane keyring not usable: $KEYRING" + ;; + esac +} + +# With the option off, the server should keep the keyring it was started with. +echo 'set -g session-keyring off' >$OUT/conf +OFF=$(keyctl session - sh -c " + $TMUX2 new -d 'sleep 1000' && keyctl id @s +" 2>/dev/null) || fail "could not start server" +pane_keyring "$TMUX2" +[ "$KEYRING" = "$OFF" ] || fail "pane has a new keyring with option off" +$TMUX2 kill-server + +# Start the server from a login session with its own keyring, then revoke that +# keyring as pam_keyinit(8) does when the login session ends. +LOGIN=$(keyctl session - sh -c " + $TMUX new -d 'sleep 1000' && keyctl id @s && keyctl revoke @s +" 2>/dev/null) || fail "could not start server" + +# A new pane should still have a usable keyring, with the user keyring linked. +pane_keyring "$TMUX" +[ "$KEYRING" != "$LOGIN" ] || fail "pane has the login keyring" +keyctl rlist $KEYRING | grep -qw "$(keyctl id @u)" || + fail "user keyring not linked" + +# The server should revoke its keyring when it exits, even while a process it +# started (which ignores the SIGHUP from its pane closing) still holds it. +$TMUX new-window -d " + trap '' HUP + echo \$\$ >$OUT/holder + $TMUX wait-for -S held + exec sleep 100 +" || fail "could not create pane" +$TMUX wait-for held +SERVER=$($TMUX display -p '#{pid}') +$TMUX kill-server +n=0 +while kill -0 $SERVER 2>/dev/null && [ $n -lt 50 ]; do + sleep 0.1 + n=$((n + 1)) +done +keyctl rdescribe $KEYRING >$OUT/exit 2>&1 +grep -q "revoked" $OUT/exit || fail "keyring not revoked: $(cat $OUT/exit)" + +exit 0 From 90cc41de320e8930950bf53dc6a7de228920a97a Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Mon, 12 Oct 2026 11:26:28 +0000 Subject: [PATCH 3/7] Replace the session-keyring option with a configure option Join the new session keyring when the server starts rather than once the configuration file has been loaded, and control it at build time with --enable-keyring, on by default if linux/keyctl.h is found. Rename keyring_join and keyring_revoke to new_session_keyring and revoke_session_keyring. --- cfg.c | 4 ---- compat.h | 4 ++-- compat/keyring-linux.c | 4 ++-- configure.ac | 16 ++++++++++---- options-table.c | 8 ------- regress/session-keyring.sh | 44 +++++++++++--------------------------- server.c | 6 +++++- tmux.1 | 8 ------- 8 files changed, 34 insertions(+), 60 deletions(-) diff --git a/cfg.c b/cfg.c index 17c01ad64..31294f92a 100644 --- a/cfg.c +++ b/cfg.c @@ -62,10 +62,6 @@ cfg_done(__unused struct cmdq_item *item, __unused void *data) } prompt_load_history(); -#ifdef HAVE_KEYCTL - if (options_get_number(global_options, "session-keyring")) - keyring_join(); -#endif return (CMD_RETURN_NORMAL); } diff --git a/compat.h b/compat.h index 0631b7252..4c74b7168 100644 --- a/compat.h +++ b/compat.h @@ -443,8 +443,8 @@ void *recallocarray(void *, size_t, size_t, size_t); #ifdef HAVE_KEYCTL /* keyring-linux.c */ -void keyring_join(void); -void keyring_revoke(void); +void new_session_keyring(void); +void revoke_session_keyring(void); #endif #ifdef HAVE_SYSTEMD diff --git a/compat/keyring-linux.c b/compat/keyring-linux.c index e777bbbbb..fb8e7126a 100644 --- a/compat/keyring-linux.c +++ b/compat/keyring-linux.c @@ -17,7 +17,7 @@ static long keyring_serial; * the login session's keyring when it ends). */ void -keyring_join(void) +new_session_keyring(void) { long serial; @@ -33,7 +33,7 @@ keyring_join(void) } void -keyring_revoke(void) +revoke_session_keyring(void) { if (keyring_serial == 0) return; diff --git a/configure.ac b/configure.ac index 139d93b88..8ce5ef9da 100644 --- a/configure.ac +++ b/configure.ac @@ -795,10 +795,18 @@ else AC_LIBOBJ(daemon) fi -# Look for linux/keyctl.h, compat/keyring-linux.c used if found. -AC_CHECK_HEADER(linux/keyctl.h, found_keyctl=yes, found_keyctl=no) -if test "x$found_keyctl" = xyes; then - AC_DEFINE(HAVE_KEYCTL) +# Look for linux/keyctl.h, compat/keyring-linux.c used if found and enabled. +AC_ARG_ENABLE( + keyring, + AS_HELP_STRING(--disable-keyring, disable giving the server its own Linux session keyring) +) +if test "x$enable_keyring" != xno; then + AC_CHECK_HEADER(linux/keyctl.h, found_keyctl=yes, found_keyctl=no) + if test "x$found_keyctl" = xyes; then + AC_DEFINE(HAVE_KEYCTL) + elif test "x$enable_keyring" = xyes; then + AC_MSG_ERROR("linux/keyctl.h not found") + fi fi AM_CONDITIONAL(HAVE_KEYCTL, [test "x$found_keyctl" = xyes]) diff --git a/options-table.c b/options-table.c index 813fc7b26..a65694973 100644 --- a/options-table.c +++ b/options-table.c @@ -532,14 +532,6 @@ const struct options_table_entry options_table[] = { .text = "Maximum number of commands to keep in history." }, - { .name = "session-keyring", - .type = OPTIONS_TABLE_FLAG, - .scope = OPTIONS_TABLE_SERVER, - .default_num = 1, - .text = "Whether the server should join a new session keyring " - "(Linux only)." - }, - { .name = "set-clipboard", .type = OPTIONS_TABLE_CHOICE, .scope = OPTIONS_TABLE_SERVER, diff --git a/regress/session-keyring.sh b/regress/session-keyring.sh index 8c854bdf6..07cc29acf 100644 --- a/regress/session-keyring.sh +++ b/regress/session-keyring.sh @@ -1,8 +1,8 @@ #!/bin/sh -# On Linux the server should join a session keyring of its own unless the -# session-keyring option is off, so it keeps working after the login session's -# keyring is revoked, and revoke it when it exits. +# On Linux the server should join a session keyring of its own, so it keeps +# working after the login session's keyring is revoked, and revoke it when it +# exits. PATH=/bin:/usr/bin TERM=screen @@ -12,10 +12,8 @@ keyctl session - true >/dev/null 2>&1 || exit 0 [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) OUT=$(mktemp -d) -TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" -TMUX2="$TEST_TMUX -LtestB$$ -f$OUT/conf" +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" $TMUX kill-server 2>/dev/null -$TMUX2 kill-server 2>/dev/null fail() { @@ -26,35 +24,11 @@ fail() cleanup() { $TMUX kill-server 2>/dev/null - $TMUX2 kill-server 2>/dev/null [ -s $OUT/holder ] && kill $(cat $OUT/holder) 2>/dev/null rm -rf "$OUT" } trap cleanup 0 1 15 -# Set KEYRING to the session keyring of a new pane on server $1. -pane_keyring() -{ - $1 new-window -d "keyctl id @s >$OUT/pane 2>&1; $1 wait-for -S pane" || - fail "could not create pane" - $1 wait-for pane - KEYRING=$(cat $OUT/pane) - case "$KEYRING" in - ''|*[!0-9]*) - fail "pane keyring not usable: $KEYRING" - ;; - esac -} - -# With the option off, the server should keep the keyring it was started with. -echo 'set -g session-keyring off' >$OUT/conf -OFF=$(keyctl session - sh -c " - $TMUX2 new -d 'sleep 1000' && keyctl id @s -" 2>/dev/null) || fail "could not start server" -pane_keyring "$TMUX2" -[ "$KEYRING" = "$OFF" ] || fail "pane has a new keyring with option off" -$TMUX2 kill-server - # Start the server from a login session with its own keyring, then revoke that # keyring as pam_keyinit(8) does when the login session ends. LOGIN=$(keyctl session - sh -c " @@ -62,7 +36,15 @@ LOGIN=$(keyctl session - sh -c " " 2>/dev/null) || fail "could not start server" # A new pane should still have a usable keyring, with the user keyring linked. -pane_keyring "$TMUX" +$TMUX new-window -d "keyctl id @s >$OUT/pane 2>&1; $TMUX wait-for -S pane" || + fail "could not create pane" +$TMUX wait-for pane +KEYRING=$(cat $OUT/pane) +case "$KEYRING" in +''|*[!0-9]*) + fail "pane keyring not usable: $KEYRING" + ;; +esac [ "$KEYRING" != "$LOGIN" ] || fail "pane has the login keyring" keyctl rlist $KEYRING | grep -qw "$(keyctl id @u)" || fail "user keyring not linked" diff --git a/server.c b/server.c index bad77f5b8..a70a1502c 100644 --- a/server.c +++ b/server.c @@ -220,6 +220,10 @@ server_start(struct tmuxproc *client, uint64_t flags, struct event_base *base, TAILQ_INIT(&message_log); gettimeofday(&start_time, NULL); +#ifdef HAVE_KEYCTL + new_session_keyring(); +#endif + #ifdef HAVE_SYSTEMD server_fd = systemd_create_socket(flags, &cause); #else @@ -260,7 +264,7 @@ server_start(struct tmuxproc *client, uint64_t flags, struct event_base *base, job_kill_all(); prompt_save_history(); #ifdef HAVE_KEYCTL - keyring_revoke(); + revoke_session_keyring(); #endif exit(0); diff --git a/tmux.1 b/tmux.1 index b843d7e65..cf82a209f 100644 --- a/tmux.1 +++ b/tmux.1 @@ -5096,14 +5096,6 @@ each client. .It Ic prompt\-history\-limit Ar number Set the number of history items to save in the history file for each type of command prompt. -.It Xo Ic session\-keyring -.Op Cm on | off -.Xc -On Linux, if enabled (the default), the server joins a new session keyring with -the user keyring linked into it, rather than keeping the session keyring of the -login session it was started from, which may be revoked when that session ends. -The new keyring is revoked when the server exits. -This option is checked once the configuration file has been loaded. .It Xo Ic set\-clipboard .Op Cm on | external | off .Xc From 782f82c51988067591a26050da8012e5ccc86d4c Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Mon, 12 Oct 2026 11:26:28 +0000 Subject: [PATCH 4/7] Remove the regress test for the server's session keyring Also stop installing keyutils in CI, which was only needed by the test. --- .github/workflows/regress.yml | 1 - regress/session-keyring.sh | 71 ----------------------------------- 2 files changed, 72 deletions(-) delete mode 100644 regress/session-keyring.sh diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 5eb1f73c2..ded64b7f9 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -51,7 +51,6 @@ jobs: automake \ bison \ build-essential \ - keyutils \ libevent-dev \ libncurses-dev \ libutf8proc-dev \ diff --git a/regress/session-keyring.sh b/regress/session-keyring.sh deleted file mode 100644 index 07cc29acf..000000000 --- a/regress/session-keyring.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/sh - -# On Linux the server should join a session keyring of its own, so it keeps -# working after the login session's keyring is revoked, and revoke it when it -# exits. - -PATH=/bin:/usr/bin -TERM=screen - -[ "$(uname)" = Linux ] || exit 0 -keyctl session - true >/dev/null 2>&1 || exit 0 - -[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -OUT=$(mktemp -d) -TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" -$TMUX kill-server 2>/dev/null - -fail() -{ - echo "$*" - exit 1 -} - -cleanup() -{ - $TMUX kill-server 2>/dev/null - [ -s $OUT/holder ] && kill $(cat $OUT/holder) 2>/dev/null - rm -rf "$OUT" -} -trap cleanup 0 1 15 - -# Start the server from a login session with its own keyring, then revoke that -# keyring as pam_keyinit(8) does when the login session ends. -LOGIN=$(keyctl session - sh -c " - $TMUX new -d 'sleep 1000' && keyctl id @s && keyctl revoke @s -" 2>/dev/null) || fail "could not start server" - -# A new pane should still have a usable keyring, with the user keyring linked. -$TMUX new-window -d "keyctl id @s >$OUT/pane 2>&1; $TMUX wait-for -S pane" || - fail "could not create pane" -$TMUX wait-for pane -KEYRING=$(cat $OUT/pane) -case "$KEYRING" in -''|*[!0-9]*) - fail "pane keyring not usable: $KEYRING" - ;; -esac -[ "$KEYRING" != "$LOGIN" ] || fail "pane has the login keyring" -keyctl rlist $KEYRING | grep -qw "$(keyctl id @u)" || - fail "user keyring not linked" - -# The server should revoke its keyring when it exits, even while a process it -# started (which ignores the SIGHUP from its pane closing) still holds it. -$TMUX new-window -d " - trap '' HUP - echo \$\$ >$OUT/holder - $TMUX wait-for -S held - exec sleep 100 -" || fail "could not create pane" -$TMUX wait-for held -SERVER=$($TMUX display -p '#{pid}') -$TMUX kill-server -n=0 -while kill -0 $SERVER 2>/dev/null && [ $n -lt 50 ]; do - sleep 0.1 - n=$((n + 1)) -done -keyctl rdescribe $KEYRING >$OUT/exit 2>&1 -grep -q "revoked" $OUT/exit || fail "keyring not revoked: $(cat $OUT/exit)" - -exit 0 From a88e7de22db590a91c5f68195152c5d833ab7056 Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Mon, 12 Oct 2026 11:29:29 +0000 Subject: [PATCH 5/7] add copyright header --- compat/keyring-linux.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/compat/keyring-linux.c b/compat/keyring-linux.c index fb8e7126a..967c799b3 100644 --- a/compat/keyring-linux.c +++ b/compat/keyring-linux.c @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2026 Ilya Priven + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + #include #include From cef1b2f5dc21afccbc20ea4c5ad44278e0a9ca90 Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Mon, 12 Oct 2026 22:37:05 +0000 Subject: [PATCH 6/7] don't revoke + fork session --- compat.h | 3 +-- compat/keyring-linux.c | 55 ++++++++++++++++++++++++++++-------------- configure.ac | 30 +++++++++++------------ server.c | 5 +--- 4 files changed, 54 insertions(+), 39 deletions(-) diff --git a/compat.h b/compat.h index 4c74b7168..2a81845fb 100644 --- a/compat.h +++ b/compat.h @@ -443,8 +443,7 @@ void *recallocarray(void *, size_t, size_t, size_t); #ifdef HAVE_KEYCTL /* keyring-linux.c */ -void new_session_keyring(void); -void revoke_session_keyring(void); +void fork_session_keyring(void); #endif #ifdef HAVE_SYSTEMD diff --git a/compat/keyring-linux.c b/compat/keyring-linux.c index 967c799b3..11d02c987 100644 --- a/compat/keyring-linux.c +++ b/compat/keyring-linux.c @@ -19,40 +19,59 @@ #include #include +#include #include #include #include "tmux.h" -/* Serial of the session keyring we joined, or 0 if none. */ -static long keyring_serial; - /* * Create a new session keyring since the current one belongs to the login * session which we're meant to outlive (and pam_keyinit(8) is likely to revoke - * the login session's keyring when it ends). + * the login session's keyring when it ends). Link the keys from the current + * session keyring into it so they remain available after that. */ void -new_session_keyring(void) +fork_session_keyring(void) { - long serial; + int32_t *keys = NULL; + long size, n = 0, i; - serial = syscall(SYS_keyctl, KEYCTL_JOIN_SESSION_KEYRING, NULL); - if (serial == -1) { + size = syscall(SYS_keyctl, KEYCTL_READ, KEY_SPEC_SESSION_KEYRING, NULL, + 0); + if (size > 0) { + keys = xmalloc(size); + n = syscall(SYS_keyctl, KEYCTL_READ, KEY_SPEC_SESSION_KEYRING, + keys, size); + if (n > size) + n = size; + n = n > 0 ? n / sizeof *keys : 0; + } + + /* + * Link the current session keyring into the thread keyring so we still + * possess its keys (and so are allowed to link them) after leaving it. + */ + if (syscall(SYS_keyctl, KEYCTL_LINK, KEY_SPEC_SESSION_KEYRING, + KEY_SPEC_THREAD_KEYRING) == -1) + log_debug("%s: link failed: %s", __func__, strerror(errno)); + + if (syscall(SYS_keyctl, KEYCTL_JOIN_SESSION_KEYRING, NULL) == -1) { log_debug("%s: join failed: %s", __func__, strerror(errno)); - return; + goto out; + } + for (i = 0; i < n; i++) { + if (syscall(SYS_keyctl, KEYCTL_LINK, keys[i], + KEY_SPEC_SESSION_KEYRING) == -1) { + log_debug("%s: link %d failed: %s", __func__, keys[i], + strerror(errno)); + } } if (syscall(SYS_keyctl, KEYCTL_LINK, KEY_SPEC_USER_KEYRING, KEY_SPEC_SESSION_KEYRING) == -1) log_debug("%s: link failed: %s", __func__, strerror(errno)); - keyring_serial = serial; -} -void -revoke_session_keyring(void) -{ - if (keyring_serial == 0) - return; - if (syscall(SYS_keyctl, KEYCTL_REVOKE, keyring_serial) == -1) - log_debug("%s: revoke failed: %s", __func__, strerror(errno)); +out: + syscall(SYS_keyctl, KEYCTL_CLEAR, KEY_SPEC_THREAD_KEYRING); + free(keys); } diff --git a/configure.ac b/configure.ac index b72b84bec..0a7d81d1d 100644 --- a/configure.ac +++ b/configure.ac @@ -541,6 +541,21 @@ if test "x$enable_cgroups" = xyes; then fi fi +# Look for linux/keyctl.h, compat/keyring-linux.c used if found and enabled. +AC_ARG_ENABLE( + keyring, + AS_HELP_STRING(--disable-keyring, disable giving the server its own Linux session keyring) +) +if test "x$enable_keyring" != xno; then + AC_CHECK_HEADER(linux/keyctl.h, found_keyctl=yes, found_keyctl=no) + if test "x$found_keyctl" = xyes; then + AC_DEFINE(HAVE_KEYCTL) + elif test "x$enable_keyring" = xyes; then + AC_MSG_ERROR("linux/keyctl.h not found") + fi +fi +AM_CONDITIONAL(HAVE_KEYCTL, [test "x$found_keyctl" = xyes]) + # Enable image support. AC_ARG_ENABLE( images, @@ -804,21 +819,6 @@ else AC_LIBOBJ(daemon) fi -# Look for linux/keyctl.h, compat/keyring-linux.c used if found and enabled. -AC_ARG_ENABLE( - keyring, - AS_HELP_STRING(--disable-keyring, disable giving the server its own Linux session keyring) -) -if test "x$enable_keyring" != xno; then - AC_CHECK_HEADER(linux/keyctl.h, found_keyctl=yes, found_keyctl=no) - if test "x$found_keyctl" = xyes; then - AC_DEFINE(HAVE_KEYCTL) - elif test "x$enable_keyring" = xyes; then - AC_MSG_ERROR("linux/keyctl.h not found") - fi -fi -AM_CONDITIONAL(HAVE_KEYCTL, [test "x$found_keyctl" = xyes]) - # Look for stravis, compat/{vis,unvis}.c used if missing. AC_CHECK_FUNC(stravis, found_stravis=yes, found_stravis=no) if test "x$found_stravis" = xyes; then diff --git a/server.c b/server.c index a70a1502c..540bfada0 100644 --- a/server.c +++ b/server.c @@ -221,7 +221,7 @@ server_start(struct tmuxproc *client, uint64_t flags, struct event_base *base, gettimeofday(&start_time, NULL); #ifdef HAVE_KEYCTL - new_session_keyring(); + fork_session_keyring(); #endif #ifdef HAVE_SYSTEMD @@ -263,9 +263,6 @@ server_start(struct tmuxproc *client, uint64_t flags, struct event_base *base, job_kill_all(); prompt_save_history(); -#ifdef HAVE_KEYCTL - revoke_session_keyring(); -#endif exit(0); } From b85e4988b59e07ef28961660836b8d4be5ce9864 Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Wed, 14 Oct 2026 00:19:57 +0000 Subject: [PATCH 7/7] retry reading session keyring if it grows --- compat/keyring-linux.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/compat/keyring-linux.c b/compat/keyring-linux.c index 11d02c987..ab7e5d1db 100644 --- a/compat/keyring-linux.c +++ b/compat/keyring-linux.c @@ -35,18 +35,18 @@ void fork_session_keyring(void) { int32_t *keys = NULL; - long size, n = 0, i; + long size = 0, n, i; - size = syscall(SYS_keyctl, KEYCTL_READ, KEY_SPEC_SESSION_KEYRING, NULL, - 0); - if (size > 0) { - keys = xmalloc(size); + /* KEYCTL_READ returns the full size; retry if keys were added. */ + for (;;) { n = syscall(SYS_keyctl, KEYCTL_READ, KEY_SPEC_SESSION_KEYRING, keys, size); - if (n > size) - n = size; - n = n > 0 ? n / sizeof *keys : 0; + if (n <= size) + break; + size = n; + keys = xrealloc(keys, size); } + n = n > 0 ? n / sizeof *keys : 0; /* * Link the current session keyring into the thread keyring so we still