Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 46 additions & 29 deletions attach.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
31 changes: 31 additions & 0 deletions tests/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down