diff --git a/attach.c b/attach.c index 5e8453e..c2986a8 100644 --- a/attach.c +++ b/attach.c @@ -256,6 +256,34 @@ int replay_session_log(int saved_errno) return 1; } +/* Returns 1 if sockname appears in the current process's atch session +** ancestry chain (SESSION_ENVVAR, colon-separated, outermost first). +** Shared by attach_main and kill_main to refuse both direct self- +** attach/self-kill and indirect loops like A -> B -> A. */ +static int session_in_ancestry(const char *sockname) +{ + const char *chain = getenv(SESSION_ENVVAR); + size_t slen; + const char *p; + + if (!chain || !*chain) + return 0; + + slen = strlen(sockname); + p = chain; + while (*p) { + const char *colon = strchr(p, ':'); + size_t tlen = colon ? (size_t)(colon - p) : strlen(p); + + if (tlen == slen && strncmp(p, sockname, tlen) == 0) + return 1; + if (!colon) + break; + p = colon + 1; + } + return 0; +} + int attach_main(int noerror) { struct packet pkt; @@ -264,35 +292,13 @@ int attach_main(int noerror) int s; /* 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. */ - { - const char *tosearch = getenv(SESSION_ENVVAR); - - if (tosearch && *tosearch) { - size_t slen = strlen(sockname); - const char *p = tosearch; - - while (*p) { - const char *colon = strchr(p, ':'); - size_t tlen = - colon ? (size_t)(colon - p) : strlen(p); - - if (tlen == slen - && strncmp(p, sockname, tlen) == 0) { - if (!noerror) - printf - ("%s: cannot attach to session '%s' from within itself\n", - progname, - session_shortname()); - return 1; - } - if (!colon) - break; - p = colon + 1; - } - } + * direct self-attach and indirect loops like A -> B -> A). */ + if (session_in_ancestry(sockname)) { + if (!noerror) + printf + ("%s: cannot attach to session '%s' from within itself\n", + progname, session_shortname()); + return 1; } /* Attempt to open the socket. Don't display an error if noerror is @@ -537,6 +543,17 @@ int kill_main(int force) const char *name = session_shortname(); int i; + /* Refuse to kill/stop any session in our ancestry chain — same check + * as attach_main, see session_in_ancestry(). Previously unchecked: + * killing your own controlling session from within it just made the + * signal race the caller's own exit, with no explanatory message. */ + if (session_in_ancestry(sockname)) { + printf + ("%s: cannot kill/stop session '%s' from within itself — run this from another session or terminal instead\n", + progname, name); + return 1; + } + signal(SIGPIPE, SIG_IGN); if (force) { diff --git a/tests/test.sh b/tests/test.sh index 5848d71..7e274da 100644 --- a/tests/test.sh +++ b/tests/test.sh @@ -276,6 +276,37 @@ run "$ATCH" kill -f s-noexist-force assert_exit "kill -f: nonexistent → exit 1" 1 "$rc" assert_contains "kill -f: nonexistent → message" "does not exist" "$out" +# kill/stop refuses to target the session in the caller's own ancestry chain +# (ATCH_SESSION), same guard as attach's self-attach check. +# NOTE: `VAR=val run ...` would leak VAR into the rest of this script, since +# `run` is a shell function -- a temporary assignment before a function call +# persists after it returns (POSIX behavior, unlike prefixing an external +# command). Export/unset explicitly instead. +"$ATCH" start s-selfkill sleep 999 +export ATCH_SESSION="$HOME/.cache/atch/s-selfkill" +run "$ATCH" kill s-selfkill +unset ATCH_SESSION +assert_exit "kill: self → exit 1" 1 "$rc" +assert_contains "kill: self → message" "from within itself" "$out" +tidy s-selfkill + +"$ATCH" start s-selfkillf sleep 999 +export ATCH_SESSION="$HOME/.cache/atch/s-selfkillf" +run "$ATCH" kill -f s-selfkillf +unset ATCH_SESSION +assert_exit "kill -f: self → exit 1" 1 "$rc" +assert_contains "kill -f: self → message" "from within itself" "$out" +tidy s-selfkillf + +# a mismatched ATCH_SESSION (different session, not an ancestor) must not +# trip the guard -- only an exact ancestry-chain match refuses the kill +"$ATCH" start s-notself sleep 999 +export ATCH_SESSION="$HOME/.cache/atch/some-other-session" +run "$ATCH" kill s-notself +unset ATCH_SESSION +assert_exit "kill: non-ancestor session unaffected → exit 0" 0 "$rc" +assert_contains "kill: non-ancestor session unaffected → stopped" "stopped" "$out" + # ── 6. clear command ───────────────────────────────────────────────────────── run "$ATCH" clear