a73x

5b5bf1d0

test: the e2e pins ask the OS through named oracle helpers

a73x   2026-09-03 15:10

Commit message
test: the e2e pins ask the OS through named oracle helpers

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SakwJEwD9dXBoRP5kWbemW

test/e2e.sh
Old New
@@ -105,6 +105,12 @@ command -v python3 > /dev/null 2>&1 || {
105 E2E_DIR=$(dirname "$0") 105 E2E_DIR=$(dirname "$0")
106 . "$E2E_DIR/e2e_lib.sh" 106 . "$E2E_DIR/e2e_lib.sh"
107 107
108 # Before any group, and in the E2E_ONLY path too: every pin below asks the
109 # OS through the lib's oracle helpers, and a helper that has stopped
110 # answering makes those pins agree with anything. One scenario here says
111 # the questions still reach the OS, so a later failure is the product's.
112 oracle_selftest
113
108 E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session 06_web 07_wallcli 08_mouse 09_hosts 10_agent 11_select 12_panes 13_birth 14_upgrade 15_askpass 16_push' 114 E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session 06_web 07_wallcli 08_mouse 09_hosts 10_agent 11_select 12_panes 13_birth 14_upgrade 15_askpass 16_push'
109 # The groups that cannot stand alone, and what they need. E2E_ONLY refuses 115 # The groups that cannot stand alone, and what they need. E2E_ONLY refuses
110 # these by name rather than running them: a group whose fixtures are 116 # these by name rather than running them: a group whose fixtures are
@@ -170,8 +176,8 @@ done
170 # one of those and adds a convergence point would be pinning a fact every 176 # one of those and adds a convergence point would be pinning a fact every
171 # leg above already establishes. 177 # leg above already establishes.
172 178
173 [ "$OK_COUNT" = "112" ] || { 179 [ "$OK_COUNT" = "113" ] || {
174 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 112 —" 180 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 113 —"
175 echo " a scenario was added (update the pin) or silently lost" 181 echo " a scenario was added (update the pin) or silently lost"
176 exit 1 182 exit 1
177 } 183 }
test/e2e_01_boot.sh
Old New
@@ -54,15 +54,15 @@ ok "--version on both binaries"
54 KEYOUT=$("$MUX" d keygen) 54 KEYOUT=$("$MUX" d keygen)
55 [ "$KEYOUT" = "$XDG_CONFIG_HOME/mux/key" ] || { 55 [ "$KEYOUT" = "$XDG_CONFIG_HOME/mux/key" ] || {
56 echo "e2e FAIL: keygen printed '$KEYOUT'"; exit 1; } 56 echo "e2e FAIL: keygen printed '$KEYOUT'"; exit 1; }
57 PERMS=$(stat -c %a "$KEYOUT") 57 PERMS=$(file_mode "$KEYOUT")
58 [ "$PERMS" = "600" ] || { echo "e2e FAIL: keygen perms $PERMS, want 600"; exit 1; } 58 [ "$PERMS" = "600" ] || { echo "e2e FAIL: keygen perms $PERMS, want 600"; exit 1; }
59 DPERMS=$(stat -c %a "$XDG_CONFIG_HOME/mux") 59 DPERMS=$(file_mode "$XDG_CONFIG_HOME/mux")
60 [ "$DPERMS" = "700" ] || { echo "e2e FAIL: key dir perms $DPERMS, want 700"; exit 1; } 60 [ "$DPERMS" = "700" ] || { echo "e2e FAIL: key dir perms $DPERMS, want 700"; exit 1; }
61 SUM1=$(sha256sum "$KEYOUT") 61 SUM1=$(sha256_of "$KEYOUT")
62 if "$MUX" d keygen > /dev/null 2>&1; then 62 if "$MUX" d keygen > /dev/null 2>&1; then
63 echo "e2e FAIL: second keygen did not refuse"; exit 1 63 echo "e2e FAIL: second keygen did not refuse"; exit 1
64 fi 64 fi
65 SUM2=$(sha256sum "$KEYOUT") 65 SUM2=$(sha256_of "$KEYOUT")
66 [ "$SUM1" = "$SUM2" ] || { echo "e2e FAIL: refused keygen still changed the key"; exit 1; } 66 [ "$SUM1" = "$SUM2" ] || { echo "e2e FAIL: refused keygen still changed the key"; exit 1; }
67 ok "keygen creates once, 0600 in a 0700 dir, refuses twice" 67 ok "keygen creates once, 0600 in a 0700 dir, refuses twice"
68 68
@@ -532,13 +532,14 @@ start_daemon "$SOCK4" "$OUT.d4.d" "--quic daemon never bound its session socket"
532 --quic "127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 3000 532 --quic "127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 3000
533 D4PID=$DPID 533 D4PID=$DPID
534 534
535 # The UDP port is actually held. /proc/net/udp rather than ss or lsof: it is 535 # The UDP port is actually held. Asked through the lib's udp_local_bound
536 # always there on the platform this daemon runs on, and needs no privileges. 536 # rather than ss or lsof: the kernel's own table is always there and needs
537 # 127.0.0.1 is 0100007F in the little-endian hex the file uses. 537 # no privileges, and the spelling of the question is the oracle's business.
538 # 127.0.0.1 is 0100007F in the little-endian hex that table uses.
538 QHEX=$(printf '0100007F:%04X' "$QPORT") 539 QHEX=$(printf '0100007F:%04X' "$QPORT")
539 grep -qi " $QHEX " /proc/net/udp || { 540 udp_local_bound "$QHEX" || {
540 echo "e2e FAIL: no UDP socket bound at 127.0.0.1:$QPORT ($QHEX)" 541 echo "e2e FAIL: no UDP socket bound at 127.0.0.1:$QPORT ($QHEX)"
541 grep -i "0100007F" /proc/net/udp || true 542 udp_table | grep -i "0100007F" || true
542 exit 1 543 exit 1
543 } 544 }
544 545
@@ -783,8 +784,9 @@ D4PID=$DPID
783 # here rather than one, since either alone still passes on an accident. 784 # here rather than one, since either alone still passes on an accident.
784 # 785 #
785 # One expression, used by the wait and the verdict: two spellings of the 786 # One expression, used by the wait and the verdict: two spellings of the
786 # same match are two matches waiting to drift apart. 787 # same match are two matches waiting to drift apart. It is the lib's
787 udp_local_bound() { awk -v h="$1" '$2==h{f=1} END{exit !f}' /proc/net/udp; } 788 # udp_local_bound now, so the local-address-only rule this block bought is
789 # also what the M8 bind check above asks, and neither can drift again.
788 i=0 790 i=0
789 # 50 x 0.1s, the same 5s every daemon wait in this file allows — and scaled 791 # 50 x 0.1s, the same 5s every daemon wait in this file allows — and scaled
790 # by TIME_SCALE like every other budget, because under test/coverage.sh the 792 # by TIME_SCALE like every other budget, because under test/coverage.sh the
@@ -796,8 +798,8 @@ done
796 udp_local_bound "$QHEX" || { 798 udp_local_bound "$QHEX" || {
797 echo "e2e FAIL: restarted daemon did not rebind udp 127.0.0.1:$QPORT ($QHEX) in $(( 5 * TIME_SCALE ))s" 799 echo "e2e FAIL: restarted daemon did not rebind udp 127.0.0.1:$QPORT ($QHEX) in $(( 5 * TIME_SCALE ))s"
798 echo " daemon pid $D4PID: $(ps -o stat=,comm= -p "$D4PID" 2>/dev/null || echo gone)" 800 echo " daemon pid $D4PID: $(ps -o stat=,comm= -p "$D4PID" 2>/dev/null || echo gone)"
799 echo " /proc/net/udp lines mentioning the port:" 801 echo " kernel UDP table lines mentioning the port:"
800 grep -i " $QHEX " /proc/net/udp || echo " (none)" 802 udp_table | grep -i "$QHEX" || echo " (none)"
801 cat "$OUT.q"; exit 1; 803 cat "$OUT.q"; exit 1;
802 } 804 }
803 kill -0 "$D4PID" || { echo "e2e FAIL: restarted --quic daemon died"; cat "$OUT.q"; exit 1; } 805 kill -0 "$D4PID" || { echo "e2e FAIL: restarted --quic daemon died"; cat "$OUT.q"; exit 1; }
@@ -901,7 +903,7 @@ kill -0 "$SPID" || { echo "e2e FAIL: started daemon not alive"; exit 1; }
901 # way in 03_side), so this holds today by construction — which is exactly 903 # way in 03_side), so this holds today by construction — which is exactly
902 # why it is asserted here: a later split of the two callers would leave 904 # why it is asserted here: a later split of the two callers would leave
903 # `d start` free to hand execve the /proc link and name its daemon `exe`. 905 # `d start` free to hand execve the /proc link and name its daemon `exe`.
904 SCOMM=$(cat "/proc/$SPID/comm") 906 SCOMM=$(pid_comm "$SPID")
905 [ "$SCOMM" = "mux" ] || { 907 [ "$SCOMM" = "mux" ] || {
906 echo "e2e FAIL: the daemon mux d start -d brought up has comm '$SCOMM', want 'mux'" 908 echo "e2e FAIL: the daemon mux d start -d brought up has comm '$SCOMM', want 'mux'"
907 exit 1; } 909 exit 1; }
@@ -923,7 +925,7 @@ assert_converged "$OUT.s8" "$SOCK8" "started daemon"
923 # than the key is: it carries whatever the daemon says about its own start. 925 # than the key is: it carries whatever the daemon says about its own start.
924 MUXLOG="$XDG_STATE_HOME/mux/muxd.log" 926 MUXLOG="$XDG_STATE_HOME/mux/muxd.log"
925 [ -f "$MUXLOG" ] || { echo "e2e FAIL: no daemon log at $MUXLOG"; exit 1; } 927 [ -f "$MUXLOG" ] || { echo "e2e FAIL: no daemon log at $MUXLOG"; exit 1; }
926 LPERMS=$(stat -c %a "$MUXLOG") 928 LPERMS=$(file_mode "$MUXLOG")
927 [ "$LPERMS" = "600" ] || { echo "e2e FAIL: daemon log perms $LPERMS, want 600"; exit 1; } 929 [ "$LPERMS" = "600" ] || { echo "e2e FAIL: daemon log perms $LPERMS, want 600"; exit 1; }
928 930
929 # Rerun: silent no-op beyond the already-running line, exit 0, same daemon. 931 # Rerun: silent no-op beyond the already-running line, exit 0, same daemon.
@@ -941,12 +943,12 @@ grep -q "already running on $SOCK8 (stop it first with \`mux d stop --sock $SOCK
941 # leave that line where it is and grow the file. 943 # leave that line where it is and grow the file.
942 SEED="a live daemon was writing here" 944 SEED="a live daemon was writing here"
943 echo "$SEED" >> "$MUXLOG" 945 echo "$SEED" >> "$MUXLOG"
944 SEEDED=$(stat -c %s "$MUXLOG") 946 SEEDED=$(file_size "$MUXLOG")
945 "$MUX" d start -d --sock "$SOCK8T" 2> "$OUT.start" 947 "$MUX" d start -d --sock "$SOCK8T" 2> "$OUT.start"
946 TPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.start") 948 TPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.start")
947 defer_kill "$TPID" 949 defer_kill "$TPID"
948 [ -n "$TPID" ] || { echo "e2e FAIL: append spawn reported no pid"; exit 1; } 950 [ -n "$TPID" ] || { echo "e2e FAIL: append spawn reported no pid"; exit 1; }
949 GREW=$(stat -c %s "$MUXLOG") 951 GREW=$(file_size "$MUXLOG")
950 [ "$GREW" -ge "$SEEDED" ] || { 952 [ "$GREW" -ge "$SEEDED" ] || {
951 echo "e2e FAIL: log shrank on spawn ($SEEDED -> $GREW) — a second daemon zeroed the first's" 953 echo "e2e FAIL: log shrank on spawn ($SEEDED -> $GREW) — a second daemon zeroed the first's"
952 exit 1; } 954 exit 1; }
@@ -1039,9 +1041,10 @@ set -e
1039 echo "e2e FAIL: mux d endpoint --sock ... --start exited $ESRC (want 0)" 1041 echo "e2e FAIL: mux d endpoint --sock ... --start exited $ESRC (want 0)"
1040 cat "$OUT.esa.err"; exit 1; } 1042 cat "$OUT.esa.err"; exit 1; }
1041 # (a) The named socket is bound, and bound by a `mux` — asked of the OS by 1043 # (a) The named socket is bound, and bound by a `mux` — asked of the OS by
1042 # INODE, not by a name. A `mux` running anywhere would satisfy a name check 1044 # INODE, not by a name, which is what pid_holds_unix_sock does. A `mux`
1043 # while binding something else entirely; the listening inode for this path 1045 # running anywhere would satisfy a name check while binding something else
1044 # out of /proc/net/unix, found among that pid's open fds, cannot. 1046 # entirely; the kernel's listening inode for this path, found among that
1047 # pid's own open fds, cannot.
1045 ESPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.esa.err" | head -1) 1048 ESPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.esa.err" | head -1)
1046 defer_kill "$ESPID" 1049 defer_kill "$ESPID"
1047 [ -n "$ESPID" ] || { 1050 [ -n "$ESPID" ] || {
@@ -1049,14 +1052,12 @@ defer_kill "$ESPID"
1049 [ -S "$ESOWN" ] || { 1052 [ -S "$ESOWN" ] || {
1050 echo "e2e FAIL: endpoint --start bound nothing at the socket it was given" 1053 echo "e2e FAIL: endpoint --start bound nothing at the socket it was given"
1051 ls -la "$ESRUN"; exit 1; } 1054 ls -la "$ESRUN"; exit 1; }
1052 ESINO=$(awk -v p="$ESOWN" '$NF == p {print $7}' /proc/net/unix | head -1) 1055 pid_holds_unix_sock "$ESPID" "$ESOWN" || {
1053 [ -n "$ESINO" ] || {
1054 echo "e2e FAIL: the kernel lists no listening socket at $ESOWN"; exit 1; }
1055 readlink /proc/"$ESPID"/fd/* 2>/dev/null | grep -qx "socket:\[$ESINO\]" || {
1056 echo "e2e FAIL: pid $ESPID does not hold the socket at $ESOWN — the daemon" 1056 echo "e2e FAIL: pid $ESPID does not hold the socket at $ESOWN — the daemon"
1057 echo " that answered is not the daemon --start reported starting" 1057 echo " that answered is not the daemon --start reported starting,"
1058 echo " or the kernel lists no listening socket at that path at all"
1058 exit 1; } 1059 exit 1; }
1059 ESCOMM=$(cat "/proc/$ESPID/comm") 1060 ESCOMM=$(pid_comm "$ESPID")
1060 [ "$ESCOMM" = "mux" ] || { 1061 [ "$ESCOMM" = "mux" ] || {
1061 echo "e2e FAIL: the socket at $ESOWN is served by comm '$ESCOMM', want 'mux'"; exit 1; } 1062 echo "e2e FAIL: the socket at $ESOWN is served by comm '$ESCOMM', want 'mux'"; exit 1; }
1062 # (b) ...and the announce came back on stdout, first line, in the grammar 1063 # (b) ...and the announce came back on stdout, first line, in the grammar
@@ -1149,12 +1150,13 @@ HFMARK="$OUT.hf.sent"; defer_rm "$HFMARK"
1149 # connect() succeeds into the listen backlog, so the peer's marker below 1150 # connect() succeeds into the listen backlog, so the peer's marker below
1150 # says the byte was written and NOT that the daemon has it. What the leg 1151 # says the byte was written and NOT that the daemon has it. What the leg
1151 # needs is an ACCEPTED half frame — an unaccepted one parks nothing — and 1152 # needs is an ACCEPTED half frame — an unaccepted one parks nothing — and
1152 # the accept is a socket appearing in /proc. Ask the OS about the OS. 1153 # the accept is one more fd appearing in the daemon's table. Ask the OS
1154 # about the OS.
1153 HFDPID=$(real_pid "$DHFPID") 1155 HFDPID=$(real_pid "$DHFPID")
1154 HFFD0=$(find "/proc/$HFDPID/fd" -mindepth 1 2>/dev/null | wc -l) 1156 HFFD0=$(pid_fd_count "$HFDPID")
1155 [ "$HFFD0" -gt 0 ] || { 1157 [ "$HFFD0" -gt 0 ] || {
1156 echo "e2e FAIL: half-frame: cannot read /proc/$HFDPID/fd, so the accept" 1158 echo "e2e FAIL: half-frame: the OS will not list pid $HFDPID's open fds,"
1157 echo " below could not be witnessed"; exit 1; } 1159 echo " so the accept below could not be witnessed"; exit 1; }
1158 python3 - "$SOCKHF" "$HFMARK" <<'EOF' & 1160 python3 - "$SOCKHF" "$HFMARK" <<'EOF' &
1159 import socket, sys, time 1161 import socket, sys, time
1160 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 1162 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
@@ -1175,7 +1177,7 @@ wait_for "$HFMARK" sent 5 || {
1175 # cannot speak for. Nothing else connects in this window, so one more fd is 1177 # cannot speak for. Nothing else connects in this window, so one more fd is
1176 # this peer's. 1178 # this peer's.
1177 _hfi=0 1179 _hfi=0
1178 while [ "$(find "/proc/$HFDPID/fd" -mindepth 1 2>/dev/null | wc -l)" -le "$HFFD0" ]; do 1180 while [ "$(pid_fd_count "$HFDPID")" -le "$HFFD0" ]; do
1179 _hfi=$((_hfi + 1)) 1181 _hfi=$((_hfi + 1))
1180 [ "$_hfi" -lt $(( 100 * TIME_SCALE )) ] || { 1182 [ "$_hfi" -lt $(( 100 * TIME_SCALE )) ] || {
1181 echo "e2e FAIL: half-frame: the daemon never accepted the peer, so the" 1183 echo "e2e FAIL: half-frame: the daemon never accepted the peer, so the"
test/e2e_03_side.sh
Old New
@@ -193,23 +193,24 @@ defer_kill "$PAPID"
193 # agent code in it and passed. The exec names the running image, so the link 193 # agent code in it and passed. The exec names the running image, so the link
194 # below can only ever be that image — and this reads the link rather 194 # below can only ever be that image — and this reads the link rather
195 # than the argv, because argv is what a wrong spawn would still get right. 195 # than the argv, because argv is what a wrong spawn would still get right.
196 # readlink -f on both sides: /proc/PID/exe is fully resolved, so a logical 196 # Fully resolved on both sides: pid_exe answers with the resolved image, so
197 # path reached through a symlinked directory would fail a correct spawn. 197 # a logical path reached through a symlinked directory would fail a correct
198 PAEXE=$(readlink -f "/proc/$PAPID/exe") 198 # spawn unless this side resolves too.
199 PAEXE=$(pid_exe "$PAPID")
199 PAWANT=$(readlink -f "$MUX_ELF") 200 PAWANT=$(readlink -f "$MUX_ELF")
200 [ "$PAEXE" = "$PAWANT" ] || { 201 [ "$PAEXE" = "$PAWANT" ] || {
201 echo "e2e FAIL: the auto-started daemon is running $PAEXE, not the build's $PAWANT" 202 echo "e2e FAIL: the auto-started daemon is running $PAEXE, not the build's $PAWANT"
202 echo " (an auto-start that resolves a NAME grades whatever is installed)" 203 echo " (an auto-start that resolves a NAME grades whatever is installed)"
203 exit 1; } 204 exit 1; }
204 # And the name the kernel gives it, which the link cannot see: `comm` comes 205 # And the OS's own name for the process, which the image path cannot see:
205 # from the basename of the FILENAME handed to execve, so exec'ing the 206 # `comm` comes from the basename of the FILENAME handed to execve, so
206 # /proc/self/exe link itself leaves every daemon on the box called `exe` — 207 # exec'ing the self-exe link itself leaves every daemon on the box called
207 # invisible to `pgrep mux`, `killall mux`, `ps -o comm` and systemd's 208 # `exe` — invisible to `pgrep mux`, `killall mux`, `ps -o comm` and
208 # MainPID name, with only the args still saying `mux d start`. 209 # systemd's MainPID name, with only the args still saying `mux d start`.
209 PACOMM=$(cat "/proc/$PAPID/comm") 210 PACOMM=$(pid_comm "$PAPID")
210 [ "$PACOMM" = "mux" ] || { 211 [ "$PACOMM" = "mux" ] || {
211 echo "e2e FAIL: the auto-started daemon's comm is '$PACOMM', want 'mux'" 212 echo "e2e FAIL: the auto-started daemon's comm is '$PACOMM', want 'mux'"
212 echo " (exec the RESOLVED path; the /proc link names the process after itself)" 213 echo " (exec the RESOLVED path; the self-exe link names the process after itself)"
213 exit 1; } 214 exit 1; }
214 # And the argv, asked of the kernel for the same reason: the attach ASKS 215 # And the argv, asked of the kernel for the same reason: the attach ASKS
215 # for a daemon in words — `d start -d --sock PATH`, nothing else — and the 216 # for a daemon in words — `d start -d --sock PATH`, nothing else — and the
@@ -219,7 +220,7 @@ PACOMM=$(cat "/proc/$PAPID/comm")
219 # what this is watching for is an ADDITION. `-d` is absent by the same 220 # what this is watching for is an ADDITION. `-d` is absent by the same
220 # rule the fork obeys — the child is the foreground daemon, and a `-d` 221 # rule the fork obeys — the child is the foreground daemon, and a `-d`
221 # still in its line would fork again. 222 # still in its line would fork again.
222 PACMD=$(tr '\0' ' ' < "/proc/$PAPID/cmdline") 223 PACMD=$(pid_args "$PAPID")
223 [ "$PACMD" = "mux d start --sock $SOCK15 " ] || { 224 [ "$PACMD" = "mux d start --sock $SOCK15 " ] || {
224 echo "e2e FAIL: the auto-started daemon's argv is [$PACMD]," 225 echo "e2e FAIL: the auto-started daemon's argv is [$PACMD],"
225 echo " want [mux d start --sock $SOCK15 ]" 226 echo " want [mux d start --sock $SOCK15 ]"
test/e2e_04_handoff.sh
Old New
@@ -88,7 +88,11 @@ mkdir -p "$SSHIM_DIR" "$HRUN" "$HRUN2"
88 ssh_shim_head "$SSHIM_DIR/ssh" 88 ssh_shim_head "$SSHIM_DIR/ssh"
89 cat >> "$SSHIM_DIR/ssh" <<'SHIM' 89 cat >> "$SSHIM_DIR/ssh" <<'SHIM'
90 echo $$ >> "${SSHIM_PIDLOG:?}" 90 echo $$ >> "${SSHIM_PIDLOG:?}"
91 printf '%s %s\n' "$(cat /proc/$$/comm)" "$(cat /proc/$PPID/comm)" >> "${SSHIM_COMMLOG:?}" 91 # ps rather than the lib's pid_comm: this shim is written to disk and run
92 # as its own `sh` by the client under test, so nothing the suite sourced
93 # is in scope here. `ps -o comm=` is the same answer the oracle gives and
94 # is spelled the same way on every OS the suite could run on.
95 printf '%s %s\n' "$(ps -o comm= -p $$)" "$(ps -o comm= -p $PPID)" >> "${SSHIM_COMMLOG:?}"
92 shift 96 shift
93 printf '%s\n' "$*" >> "${SSHIM_ARGLOG:?}" 97 printf '%s\n' "$*" >> "${SSHIM_ARGLOG:?}"
94 exec /bin/sh -c "$*" 98 exec /bin/sh -c "$*"
@@ -199,23 +203,23 @@ kill -0 "$HAPID" 2>/dev/null || {
199 # The cache: present, private (it holds the key), and naming a port that is 203 # The cache: present, private (it holds the key), and naming a port that is
200 # really being listened on. The last one is the assertion that matters — 204 # really being listened on. The last one is the assertion that matters —
201 # a remembered number proves nothing, a remembered number that maps to the 205 # a remembered number proves nothing, a remembered number that maps to the
202 # daemon's actual UDP socket proves the announce was true. /proc/net/udp 206 # daemon's actual UDP socket proves the announce was true. Asked through
203 # for the same reasons the M8 block uses it, and 00000000 because the lazy 207 # udp_local_bound for the same reasons the M8 block uses it, and 00000000
204 # bind takes 0.0.0.0. 208 # because the lazy bind takes 0.0.0.0.
205 [ -f "$HCACHE" ] || { 209 [ -f "$HCACHE" ] || {
206 echo "e2e FAIL: cold handoff left no cache at $HCACHE" 210 echo "e2e FAIL: cold handoff left no cache at $HCACHE"
207 ls -la "$XDG_CACHE_HOME/mux/hosts" 2>/dev/null; exit 1; } 211 ls -la "$XDG_CACHE_HOME/mux/hosts" 2>/dev/null; exit 1; }
208 HCPERM=$(stat -c %a "$HCACHE") 212 HCPERM=$(file_mode "$HCACHE")
209 [ "$HCPERM" = "600" ] || { 213 [ "$HCPERM" = "600" ] || {
210 echo "e2e FAIL: handoff cache perms $HCPERM, want 600 (the file holds a key)"; exit 1; } 214 echo "e2e FAIL: handoff cache perms $HCPERM, want 600 (the file holds a key)"; exit 1; }
211 HCDPERM=$(stat -c %a "$XDG_CACHE_HOME/mux/hosts") 215 HCDPERM=$(file_mode "$XDG_CACHE_HOME/mux/hosts")
212 [ "$HCDPERM" = "700" ] || { 216 [ "$HCDPERM" = "700" ] || {
213 echo "e2e FAIL: handoff cache dir perms $HCDPERM, want 700"; exit 1; } 217 echo "e2e FAIL: handoff cache dir perms $HCDPERM, want 700"; exit 1; }
214 HPORT=$(sed -n 's/^endpoint \([0-9][0-9]*\) [0-9a-f]*$/\1/p' "$HCACHE") 218 HPORT=$(sed -n 's/^endpoint \([0-9][0-9]*\) [0-9a-f]*$/\1/p' "$HCACHE")
215 [ -n "$HPORT" ] || { 219 [ -n "$HPORT" ] || {
216 echo "e2e FAIL: the cache is not an announce line:"; cat -v "$HCACHE"; exit 1; } 220 echo "e2e FAIL: the cache is not an announce line:"; cat -v "$HCACHE"; exit 1; }
217 HHEXUDP=$(printf '00000000:%04X' "$HPORT") 221 HHEXUDP=$(printf '00000000:%04X' "$HPORT")
218 grep -qi " $HHEXUDP " /proc/net/udp || { 222 udp_local_bound "$HHEXUDP" || {
219 echo "e2e FAIL: the cached port $HPORT names no UDP listener ($HHEXUDP);" 223 echo "e2e FAIL: the cached port $HPORT names no UDP listener ($HHEXUDP);"
220 echo " the announce the client believed was not the daemon's" 224 echo " the announce the client believed was not the daemon's"
221 exit 1; } 225 exit 1; }
@@ -479,7 +483,7 @@ HWPORT=$(sed -n 's/^endpoint \([0-9][0-9]*\) [0-9a-f]*$/\1/p' "$HCACHE_D")
479 [ "$HWPORT" = "$HQPORT" ] || { 483 [ "$HWPORT" = "$HQPORT" ] || {
480 echo "e2e FAIL: the cache at $HCACHE_D names port '$HWPORT', want the silent $HQPORT" 484 echo "e2e FAIL: the cache at $HCACHE_D names port '$HWPORT', want the silent $HQPORT"
481 cat -v "$HCACHE_D"; exit 1; } 485 cat -v "$HCACHE_D"; exit 1; }
482 HWMODE=$(stat -c %a "$HCACHE_D") 486 HWMODE=$(file_mode "$HCACHE_D")
483 [ "$HWMODE" = "600" ] || { 487 [ "$HWMODE" = "600" ] || {
484 echo "e2e FAIL: the cache at $HCACHE_D is mode $HWMODE; readCache refuses" 488 echo "e2e FAIL: the cache at $HCACHE_D is mode $HWMODE; readCache refuses"
485 echo " anything looser, so this leg would attach COLD" 489 echo " anything looser, so this leg would attach COLD"
test/e2e_lib.sh
Old New
@@ -300,7 +300,7 @@ hardkill() {
300 # wrapper writes no coverage.db at any --output-interval, so a clean 300 # wrapper writes no coverage.db at any --output-interval, so a clean
301 # exit is the only exit that keeps the data. Then wait for it, because 301 # exit is the only exit that keeps the data. Then wait for it, because
302 # killing both at once is the same as never killing the child at all. 302 # killing both at once is the same as never killing the child at all.
303 for _c in $(ps -o pid= --ppid "$1" 2>/dev/null); do 303 for _c in $(pid_children "$1"); do
304 kill -9 "$_c" 2>/dev/null || true 304 kill -9 "$_c" 2>/dev/null || true
305 done 305 done
306 _i=0 306 _i=0
@@ -321,7 +321,7 @@ hardkill() {
321 # cmdline is what a resumed-argv check would otherwise read. 321 # cmdline is what a resumed-argv check would otherwise read.
322 real_pid() { 322 real_pid() {
323 if [ "$(ps -o comm= -p "$1" 2>/dev/null)" = kcov ]; then 323 if [ "$(ps -o comm= -p "$1" 2>/dev/null)" = kcov ]; then
324 ps -o pid= --ppid "$1" 2>/dev/null | head -1 | tr -d ' ' 324 pid_children "$1" | head -1
325 else 325 else
326 echo "$1" 326 echo "$1"
327 fi 327 fi
@@ -330,7 +330,7 @@ real_pid() {
330 softkill() { 330 softkill() {
331 if [ "$(ps -o comm= -p "$1" 2>/dev/null)" = kcov ]; then 331 if [ "$(ps -o comm= -p "$1" 2>/dev/null)" = kcov ]; then
332 _rc=1 332 _rc=1
333 for _c in $(ps -o pid= --ppid "$1" 2>/dev/null); do 333 for _c in $(pid_children "$1"); do
334 kill "$_c" 2>/dev/null && _rc=0 334 kill "$_c" 2>/dev/null && _rc=0
335 done 335 done
336 return $_rc 336 return $_rc
@@ -338,6 +338,14 @@ softkill() {
338 kill "$1" 2>/dev/null 338 kill "$1" 2>/dev/null
339 } 339 }
340 340
341 # ---- the OS oracle ------------------------------------------------------
342 # The helpers themselves are in os_oracle.sh, sourced here where they used
343 # to be written out. They moved because soak.sh reads two of them as well,
344 # and its copies were a second spelling of a question this repo means to
345 # ask in one place. That file is trap-free on purpose: the registry and the
346 # EXIT trap below are this file's, and it must not bring a second of either.
347 . "$(dirname "$0")/os_oracle.sh"
348
341 # Poll until nothing answers on a socket path (2s). Keyed off the daemon's 349 # Poll until nothing answers on a socket path (2s). Keyed off the daemon's
342 # own liveness rather than a fixed sleep, same reasoning as wait_for. 350 # own liveness rather than a fixed sleep, same reasoning as wait_for.
343 wait_gone() { 351 wait_gone() {
@@ -1315,3 +1323,137 @@ cleanup() {
1315 fi 1323 fi
1316 } 1324 }
1317 trap cleanup EXIT INT TERM 1325 trap cleanup EXIT INT TERM
1326
1327 # ---- the oracle's own pin -----------------------------------------------
1328 # The runner calls this once, before the first group. Every helper above is
1329 # a question a pin asks INSTEAD of asking the daemon, so a helper that
1330 # quietly stopped answering would not fail a pin loudly — it would make the
1331 # pin agree with anything, and the suite would go green having tested
1332 # nothing. This asks each helper something the box can independently
1333 # confirm.
1334 #
1335 # Off-origin on every dimension the helpers could accidentally hold
1336 # constant, because a fixture that pins N=1 or offset=0 is blind to N and
1337 # to the offset (CLAUDE.md): the subject is a child that is not pid 1, it
1338 # holds MORE than the three fds any fixture would, it has an argv worth
1339 # losing, and it has a child of its own. The socket arm is asked in both
1340 # directions — a listener that HOLDS the path and a shell that does not —
1341 # because a `pid_holds_unix_sock` that answered yes to everything would
1342 # pass the one pin that reads it just as happily as a correct one.
1343 #
1344 # The `sh -c` body ends in `:` on purpose. With nothing after it, a shell
1345 # execs the last command in place rather than forking it — bash measured
1346 # doing exactly that — and the subject would be `sleep`, wearing sleep's
1347 # comm, exe and argv and having no child at all. That is a different
1348 # process from the one this means to ask about.
1349 oracle_selftest() {
1350 _osock="$OUT.oracle.sock"
1351 rm -f "$_osock"
1352 printf 'hello' > "$OUT.oracle.probe"
1353 # python3 rather than sh: nothing in POSIX sh can bind a unix socket,
1354 # and the runner has already refused a box without python3.
1355 python3 -c 'import socket,sys,time
1356 s = socket.socket(socket.AF_UNIX); s.bind(sys.argv[1]); s.listen(1); time.sleep(300)' \
1357 "$_osock" &
1358 _olpid=$!
1359 sh -c 'exec 5>/dev/null 6>/dev/null; sleep 300; :' &
1360 _opid=$!
1361 # Registered, not merely killed at the bottom: an assertion below that
1362 # fails exits through the trap, and an unregistered `sleep 300` would
1363 # outlive the run by five minutes — ten of them under `make soak`.
1364 defer_kill "$_olpid" "$_opid"
1365 _oi=0
1366 while [ ! -S "$_osock" ] && [ "$_oi" -lt $(( 50 * TIME_SCALE )) ]; do
1367 sleep 0.1; _oi=$((_oi + 1))
1368 done
1369 [ -S "$_osock" ] || {
1370 echo "e2e FAIL: oracle: the listener bound no socket at $_osock"; exit 1; }
1371
1372 pid_alive "$_opid" || {
1373 echo "e2e FAIL: oracle: pid_alive says a live shell is dead"; exit 1; }
1374 [ "$(pid_comm "$_opid")" = sh ] || {
1375 echo "e2e FAIL: oracle: pid_comm of an sh is '$(pid_comm "$_opid")'"; exit 1; }
1376 pid_args "$_opid" | grep -q 'sleep 300' || {
1377 echo "e2e FAIL: oracle: pid_args lost the argv: [$(pid_args "$_opid")]"; exit 1; }
1378 [ "$(pid_fd_count "$_opid")" -ge 5 ] || {
1379 echo "e2e FAIL: oracle: pid_fd_count is $(pid_fd_count "$_opid") for a shell"
1380 echo " holding fd 5 and fd 6 on top of the usual three"; exit 1; }
1381 [ "$(pid_exe "$_opid")" = "$(readlink -f "$(command -v sh)")" ] || {
1382 echo "e2e FAIL: oracle: pid_exe is '$(pid_exe "$_opid")', want the resolved"
1383 echo " $(readlink -f "$(command -v sh)")"; exit 1; }
1384 # head -1 because this is a pid to kill as well as an answer to check,
1385 # and the registry and kill(1) below both take one word.
1386 _okid=$(pid_children "$_opid" | head -1)
1387 [ -n "$_okid" ] || {
1388 echo "e2e FAIL: oracle: pid_children names no child of a shell running sleep"
1389 exit 1; }
1390 defer_kill "$_okid"
1391 # Through a variable and a case rather than straight into test(1): a
1392 # helper that answered with nothing would otherwise abort the run with
1393 # a bare `[: -gt: unary operator expected` and no e2e FAIL line saying
1394 # which helper went quiet.
1395 _orss=$(pid_rss_kb "$_opid")
1396 case "$_orss" in ''|*[!0-9]*) _orss=0 ;; esac
1397 [ "$_orss" -gt 0 ] || {
1398 echo "e2e FAIL: oracle: pid_rss_kb is '$(pid_rss_kb "$_opid")'"; exit 1; }
1399 pid_holds_unix_sock "$_olpid" "$_osock" || {
1400 echo "e2e FAIL: oracle: pid_holds_unix_sock cannot see $_osock among the"
1401 echo " fds of the pid $_olpid that bound it"; exit 1; }
1402 # The `socket` KIND off a real fd: the listener bound one, so its count
1403 # is at least one. Only that KIND is asserted here. `carrier` and
1404 # `ptymaster` would each need a process holding an anonymous memory file
1405 # or a pty master, which is a daemon — and e2e_14_upgrade already asks
1406 # both of a real one, where a wrong answer fails that pin.
1407 _ofdk=$(pid_holds_fd_kind "$_olpid" socket)
1408 case "$_ofdk" in ''|*[!0-9]*) _ofdk=0 ;; esac
1409 [ "$_ofdk" -ge 1 ] || {
1410 echo "e2e FAIL: oracle: pid_holds_fd_kind says $_olpid holds"
1411 echo " '$(pid_holds_fd_kind "$_olpid" socket)' fds of kind socket, and it"
1412 echo " bound $_osock"; exit 1; }
1413 # `! cmd || {}` rather than `cmd && {}`, for the reason spelled out
1414 # below: an AND-list whose left side fails is a failing command under
1415 # `set -e`, so the passing case would end the run.
1416 ! pid_holds_fd_kind "$_opid" nosuchkind 2>/dev/null || {
1417 echo "e2e FAIL: oracle: pid_holds_fd_kind accepted a kind it does not know,"
1418 echo " so a typo in a group file would count zero and pass"; exit 1; }
1419 ! pid_holds_unix_sock "$_opid" "$_osock" || {
1420 echo "e2e FAIL: oracle: pid_holds_unix_sock says the shell $_opid holds"
1421 echo " $_osock, which only $_olpid ever opened"; exit 1; }
1422 # `! cmd || {}` rather than `cmd && {}`, for wait_sock's reason: an
1423 # AND-list whose left side fails is a failing command under `set -e`,
1424 # so the passing case would end the run.
1425 ! udp_local_bound 'ZZZZZZZZ:ZZZZ' || {
1426 echo "e2e FAIL: oracle: udp_local_bound found a socket at an address no"
1427 echo " kernel can spell"; exit 1; }
1428 [ -n "$(udp_table)" ] || {
1429 echo "e2e FAIL: oracle: udp_table is empty, so a bind diagnostic would"
1430 echo " print nothing"; exit 1; }
1431 # Against what the probe file IS, never against a second spelling of the
1432 # same command: comparing file_mode with `stat -c %a` would agree with
1433 # itself on any OS and would have to be ported alongside the helper. A
1434 # chmod the kernel performed, five bytes written, and the published
1435 # SHA-256 of "hello" are answers no arm of the oracle can define away.
1436 chmod 640 "$OUT.oracle.probe"
1437 [ "$(file_mode "$OUT.oracle.probe")" = "640" ] || {
1438 echo "e2e FAIL: oracle: file_mode says '$(file_mode "$OUT.oracle.probe")' for a"
1439 echo " file the kernel just chmod'd to 640"; exit 1; }
1440 [ "$(file_size "$OUT.oracle.probe")" = "5" ] || {
1441 echo "e2e FAIL: oracle: file_size says '$(file_size "$OUT.oracle.probe")' for five bytes"
1442 exit 1; }
1443 [ "$(sha256_of "$OUT.oracle.probe")" = \
1444 "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" ] || {
1445 echo "e2e FAIL: oracle: sha256_of hashes 'hello' to"
1446 echo " '$(sha256_of "$OUT.oracle.probe")'"; exit 1; }
1447
1448 kill "$_olpid" "$_opid" "$_okid" 2>/dev/null || true
1449 wait "$_olpid" 2>/dev/null || true
1450 wait "$_opid" 2>/dev/null || true
1451 _oi=0
1452 while pid_alive "$_opid" && [ "$_oi" -lt $(( 50 * TIME_SCALE )) ]; do
1453 sleep 0.05; _oi=$((_oi + 1))
1454 done
1455 ! pid_alive "$_opid" || {
1456 echo "e2e FAIL: oracle: pid_alive says a killed shell still lives"; exit 1; }
1457 rm -f "$_osock" "$OUT.oracle.probe"
1458 ok "oracle: the OS answers the helpers by name"
1459 }
test/os_oracle.sh
Old New
@@ -0,0 +1,65 @@
1 # shellcheck shell=sh
2 # os_oracle.sh — the OS oracle, sourced and never run.
3 #
4 # Sourced by test/e2e_lib.sh, where these helpers used to live, and by
5 # test/soak.sh, which reads two of them between runs. It is deliberately
6 # trap-free and state-free: both readers bring a cleanup registry and an
7 # EXIT trap of their own, and a second trap here would replace one of
8 # theirs. soak.sh used to carry copies of the two it needs, and a copy is a
9 # second spelling to port the day a second OS arrives.
10 #
11 # "Ask the OS about the OS, not the daemon" (CLAUDE.md). Every pin that
12 # reads a pid, an fd table, a bound UDP port or a file's mode asks through
13 # these names, so the SPELLING of the question lives in one place per OS
14 # and the question itself lives in the group file. The Linux arm is /proc,
15 # GNU stat and sha256sum; a second OS adds a `case "$(uname)"` arm here and
16 # nothing in a group file changes. Each prints its answer on stdout and
17 # returns 1 when the OS will not say, so a caller may treat "no answer"
18 # and "the wrong answer" as the same failure.
19 #
20 # oracle_selftest in e2e_lib.sh is these helpers' own pin. A helper that
21 # stopped answering — a missing binary, a /proc the sandbox will not show —
22 # would otherwise turn every pin that reads it into a check that passes
23 # without running, which is the one failure a green tree cannot show.
24 pid_alive() { kill -0 "$1" 2>/dev/null || [ -d "/proc/$1" ]; }
25 pid_exe() { readlink -f "/proc/$1/exe" 2>/dev/null; }
26 pid_comm() { cat "/proc/$1/comm" 2>/dev/null; }
27 pid_args() { tr '\0' ' ' < "/proc/$1/cmdline" 2>/dev/null; }
28 pid_children() { ps -o pid= --ppid "$1" 2>/dev/null | tr -d ' '; }
29 pid_fd_count() { find "/proc/$1/fd" -mindepth 1 2>/dev/null | wc -l | tr -d ' '; }
30 pid_fd_targets() { readlink "/proc/$1"/fd/* 2>/dev/null; }
31 # pid_holds_fd_kind PID KIND — how many of PID's open fds are of KIND, which
32 # is one of `socket`, `carrier` (the upgrade manifest's anonymous memory
33 # file) or `ptymaster`. Prints the count. These are the three KINDS an
34 # upgrade pin asks about, and naming them is the point: a second OS's arm
35 # counts the same three things under whatever that OS calls them, and the
36 # group file keeps asking the same question. `|| true` because grep -c
37 # prints 0 AND exits 1 when nothing matches, and callers run under `set -e`.
38 pid_holds_fd_kind() {
39 case "$2" in
40 socket) _fdk='socket:' ;;
41 carrier) _fdk='memfd:' ;;
42 ptymaster) _fdk='/dev/ptmx' ;;
43 *) echo "pid_holds_fd_kind: no such fd kind '$2'" >&2; return 1 ;;
44 esac
45 pid_fd_targets "$1" | grep -c -F "$_fdk" || true
46 }
47 # By INODE, never by name: a process running the right program while
48 # binding something else entirely would satisfy a name check. The kernel's
49 # listening inode for PATH, found among that pid's open fds, cannot.
50 pid_holds_unix_sock() {
51 _ino=$(awk -v p="$2" '$NF == p {print $7}' /proc/net/unix | head -1)
52 [ -n "$_ino" ] && pid_fd_targets "$1" | grep -qx "socket:\[$_ino\]"
53 }
54 pid_rss_kb() { awk '/VmRSS/{print $2}' "/proc/$1/status" 2>/dev/null || echo 0; }
55 # Matched on the LOCAL address ($2) rather than anywhere on the line: the
56 # remote address of a client dialling this port is the same hex, and a
57 # whole-line match read a reconnecting client as a bound listener.
58 udp_local_bound() { awk -v h="$1" '$2==h{f=1} END{exit !f}' /proc/net/udp; }
59 udp_table() { cat /proc/net/udp 2>/dev/null; }
60 file_mode() { stat -c %a "$1"; }
61 file_size() { stat -c %s "$1"; }
62 sha256_of() { sha256sum "$1" | cut -d' ' -f1; }
63 # GNU timeout is a binary here; a box without one names it gtimeout, and a
64 # group file keeps spelling `timeout` either way.
65 command -v timeout >/dev/null 2>&1 || timeout() { gtimeout "$@"; }