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
19 changes: 15 additions & 4 deletions master.c
Original file line number Diff line number Diff line change
Expand Up @@ -246,17 +246,28 @@ static int create_socket(char *name)
if (strlen(name) > sizeof(sockun.sun_path) - 1)
return socket_with_chdir(name, create_socket);

omask = umask(077);
/*
** Use umask(0177) during bind so the kernel creates the socket file
** with mode 0600 directly (0777 & ~0177 = 0600). This ensures
** S_IXUSR is never set on the socket file at any point during
** creation, eliminating the TOCTOU window between bind(2) and the
** subsequent chmod(2) that would otherwise let `atch list` briefly
** see a newly-started session as [attached].
*/
omask = umask(0177);
s = socket(PF_UNIX, SOCK_STREAM, 0);
umask(omask); /* umask always succeeds, errno is untouched. */
if (s < 0)
if (s < 0) {
umask(omask);
return -1;
}
sockun.sun_family = AF_UNIX;
memcpy(sockun.sun_path, name, strlen(name) + 1);
if (bind(s, (struct sockaddr *)&sockun, sizeof(sockun)) < 0) {
umask(omask);
close(s);
return -1;
}
umask(omask); /* umask always succeeds, errno is untouched. */
if (listen(s, 128) < 0) {
close(s);
return -1;
Expand All @@ -265,7 +276,7 @@ static int create_socket(char *name)
close(s);
return -1;
}
/* chmod it to prevent any surprises */
/* chmod it to enforce 0600 regardless of platform quirks */
if (chmod(name, 0600) < 0) {
close(s);
return -1;
Expand Down
78 changes: 78 additions & 0 deletions tests/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,84 @@ run "$ATCH"
assert_exit "no args: exits 0 (usage)" 0 "$rc"
assert_contains "no args: shows Usage:" "Usage:" "$out"

# ── 22. start-inside-session: no [attached] when started from inside a session ──
#
# Regression test for: a session created with `atch start` from within an
# attached session must never appear as [attached] in `atch list`.
#
# Root cause: create_socket restored the original umask BEFORE calling bind(2).
# With a typical shell umask of 022, bind created the socket file with mode
# 0755 (S_IXUSR set). chmod(0600) was called immediately after, but the
# tiny window between bind and chmod was enough for a concurrent `atch list`
# (or an immediate stat after start) to see the stale execute bit and report
# the session as [attached].
#
# Fix: use umask(0177) before bind so the socket is created directly as 0600
# (no execute bit ever present during creation).
#
# Test strategy:
# A. Start outer-session so there is an [attached] session in the directory.
# B. Simulate being inside outer-session by setting ATCH_SESSION.
# C. Run `atch start inner-session` — no client must ever attach.
# D. Check socket mode immediately: S_IXUSR must NOT be set.
# E. Check `atch list`: inner-session must NOT show [attached].

"$ATCH" start sis-outer sleep 999
wait_socket sis-outer
SIS_OUTER_SOCK="$HOME/.cache/atch/sis-outer"

# Attach to outer via python so it shows [attached] — this mirrors the real
# scenario where the user is inside the outer session.
if command -v python3 >/dev/null 2>&1; then
python3 - "$SIS_OUTER_SOCK" << 'PYEOF' &
import socket, struct, sys, time
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(sys.argv[1])
s.sendall(struct.pack('BB8s', 1, 0, b'\x00' * 8)) # MSG_ATTACH
time.sleep(15)
s.close()
PYEOF
SIS_ATTACH_PID=$!
sleep 0.1

# Start inner-session as if we are inside outer-session (ATCH_SESSION set)
ATCH_SESSION="$SIS_OUTER_SOCK" "$ATCH" start sis-inner sleep 999
wait_socket sis-inner
SIS_INNER_SOCK="$HOME/.cache/atch/sis-inner"

# Check socket mode immediately after start: no S_IXUSR allowed.
# The owner execute bit (S_IXUSR) is the bit 0 of the hundreds digit
# in the 3-digit octal representation (i.e., digit is 1, 3, 5, or 7).
# We extract the hundreds digit and test whether it is odd.
SOCK_MODE=$(stat -c "%a" "$SIS_INNER_SOCK" 2>/dev/null || \
stat -f "%Lp" "$SIS_INNER_SOCK" 2>/dev/null || echo "unknown")
# Hundreds digit: remove last two chars → first char of 3-digit mode
OWNER_DIGIT="${SOCK_MODE%??}"
case "$OWNER_DIGIT" in
1|3|5|7)
fail "start-inside: socket mode must not have S_IXUSR" \
"owner digit 0,2,4 or 6 (no execute)" "$OWNER_DIGIT (mode $SOCK_MODE)" ;;
*)
ok "start-inside: socket created without S_IXUSR (mode $SOCK_MODE)" ;;
esac

# Check list: inner-session must NOT appear as [attached]
run "$ATCH" list
assert_not_contains \
"start-inside: inner session not shown as [attached] in list" \
"[attached]" \
"$(echo "$out" | grep sis-inner)"

kill $SIS_ATTACH_PID 2>/dev/null
wait $SIS_ATTACH_PID 2>/dev/null

tidy sis-outer
tidy sis-inner
else
ok "start-inside: skip (python3 not available)"
ok "start-inside: skip (python3 not available)"
fi

# ── summary ──────────────────────────────────────────────────────────────────

printf "\n1..%d\n" "$T"
Expand Down