. "$BST_LIB" # BST-TIMEOUT: 600 # rdate 1.5 (meta-networking recipes-connectivity) -- the RFC 868 time client. # It asks a server for the time over TCP (or UDP with -u) and either PRINTS it # (-p, and the default) or SETS this machine's clock to it (-s). # # WHAT THIS SUITE SCORES IS THE VALUE rdate READS, NOT THE CLOCK IT SETS, and # that division is the ticket's. RFC 868 is four bytes on the wire -- seconds # since 1900 -- so "did the port work" is an arithmetic question with one right # answer, and every case below is written so that answer does not depend on what # this guest's own clock happens to say. Section 2 serves a FIXED instant and # asserts the exact string rdate must print for it; a port that fetched the # bytes but botched the 1900->1970 subtraction, or the byte order, would print # something else and fail there. # # THE CLOCK-SETTING ARM IS A RED WITNESS, NOT AN ASSERTION (section 4). # settimeofday(2) refuses here with ENOSYS: it is absent from the wasm32 syscall # table (164), as is clock_settime (227), so NOTHING on this image can set the # wall clock (brintos/linux-in-the-browser#182). /set-clock reports whatever # actually happened and attributes a refusal to that issue rather than asserting # success. NOT #120, which is the relative-timer gap and states that the wall # clock reads correctly -- reading is fine here, only setting is missing. # It runs against a server that reports THIS GUEST's OWN TIME, so that on the day # the call starts working the clock lands within a second of where it already # was. A -s case pointed at section 2's 2023 literal would, the moment the gap # closes, step this guest to 2023 and take every later suite in the boot with it. # # ONE socat, FOUR ARMS. socat is a 54 MB image and the guest refuses a second # large exec under fragmentation (brintos/linux#44), so there is a single # listener and the response is selected through a file the suite writes -- # rdate-origin.sh reads $BST_RDATE_STATE. A TCP time server has no request in # which to carry a mode, which is why it cannot be selected the way # 88-htpdate.test selects its arms by URL path. RD=/usr/bin/rdate if [ ! -x "$RD" ]; then bst_skip rdate/present "$RD is not installed on this image" exit 0 fi bst_ok rdate/present D=${TMPDIR:-/tmp}/bst-rdate.$$ rm -rf "$D"; mkdir -p "$D" || { bst_notok rdate/workdir "cannot create $D"; exit 0; } # Read BEFORE anything runs, so /clock-untouched at the end compares against a # mark this suite took itself. SUITE_T0=$(date -u +%s) # ctime(3) renders in LOCAL time, and rdate prints ctime_r's output verbatim, so # the expected string in section 2 is only a constant if the zone is. UTC0 is a # POSIX TZ spec rather than a zone NAME: glibc parses it directly and never looks # for a tzdata file, so this does not depend on what (if anything) this image # ships under /usr/share/zoneinfo. TZ=UTC0 export TZ PORT=8037 # 37 is the RFC 868 assigned port; 8037 keeps it unprivileged # and clear of every port the other suites bind (checked). DEAD=8038 # deliberately nothing here -- section 5's refused connect. # has -- substring assertion over a value already read. has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "want:'$2' got:'$(_bst_1line "$3")'" ;; esac } # bg -- run a client that MAY NEVER RETURN in the # background and poll up to for it, without wait(1). # # rdate DOES carry its own timeout (-t, default 10s), and it is implemented with # alarm(2) plus a SIGALRM handler -- which is exactly why it cannot be relied on # here. No signal is delivered on this image (brintos/linux#21), so the alarm # never interrupts the connect() or the read(), and rdate blocks forever on a # server that accepts and then abandons it. A foreground run would cost the WHOLE # suite rather than one case: the runner buffers a suite's report and prints it # only when the suite exits. The rc travels out through a file because $? is not # available to a poll. Sets bg_done=1/0, and bg_rc/bg_out when bg_done=1. bg() { _t=$1; _tag=$2; shift 2 rm -f "$D/$_tag.rc" "$D/$_tag.out" ( "$@" > "$D/$_tag.out" 2>&1; echo $? > "$D/$_tag.rc" ) & _i=0; bg_done=0 while [ "$_i" -lt "$_t" ]; do if [ -f "$D/$_tag.rc" ]; then bg_done=1; break; fi _i=$((_i+1)); sleep 1 done if [ "$bg_done" -eq 1 ]; then bg_rc=$(cat "$D/$_tag.rc" 2>/dev/null); bg_out=$(cat "$D/$_tag.out" 2>/dev/null) else bg_rc=""; bg_out=$(cat "$D/$_tag.out" 2>/dev/null) fi } # wait_listen -- bound the server's startup on KERNEL state, read out of # /proc/net/tcp. A TCP listener leaves no filesystem artifact to poll, so a plain # `sleep N' races a socat that is merely still staging -- 60-ckermit.test measured # that taking longer than three seconds on this guest. /proc costs no exec, which # ss(1) and netstat(1) would, in a guest that does not return memory on process # exit (brintos/linux#21). wait_listen() { _hex=$(printf '%04X' "$1"); _n=0 while [ "$_n" -lt 60 ]; do grep -qi ":$_hex " /proc/net/tcp 2>/dev/null && return 0 grep -qi ":$_hex " /proc/net/tcp6 2>/dev/null && return 0 _n=$((_n+1)); sleep 1 done return 1 } # mode -- choose which arm rdate-origin.sh serves on the NEXT connection. mode() { printf '%s\n' "$1" > "$D/mode"; } ## ## 1. Identity and the build contract ## # rdate has no --version; usage() is the only text it prints about itself, and # -h reaches it with exit 0 (the exit(1) arm is the separate no-hosts path). The # flags asserted here are the ones every later section depends on existing, so a # build that quietly lost one is caught before the cases that would blame the # network for it. _u=$("$RD" -h 2>&1) has rdate/usage-print "[-p]" "$_u" has rdate/usage-set "[-s]" "$_u" has rdate/usage-udp "[-u]" "$_u" # No host on the command line is a usage error, and it is the one rdate reports # with a non-zero status without touching the network. bg 30 noargs "$RD" if [ "$bg_done" -ne 1 ]; then bst_notok rdate/no-host "rdate never returned with no arguments (bounded at 30s)" elif bst_execfail "$bg_out"; then bst_unrun rdate/no-host "$bg_out" elif [ "$bg_rc" -eq 1 ]; then has rdate/no-host "Usage:" "$bg_out" else bst_notok rdate/no-host "exited $bg_rc with no host, want 1 -- $(_bst_1line "$bg_out")" fi ## ## 2. THE TICKET'S NAMED CRITERION: a fixed instant, read back exactly ## # The server serves 0xE75B4B80 == 3881520000 seconds since 1900, which is # 1672531200 seconds since 1970 -- 2023-01-01T00:00:00Z. Under TZ=UTC0, ctime(3) # renders that as the literal below, to the character. # # THIS IS THE CASE THAT SEPARATES A PORT FROM A CONNECTION. Every plausible way # of getting rdate slightly wrong lands somewhere else: drop the ntohl and the # value is nowhere near 2023; drop the 2208988800 subtraction and it reads 2093; # read three bytes and it is off by a factor of 256. Only a correct port prints # this string. WANT='Sun Jan 1 00:00:00 2023' ORIGIN=$BST_DATA/rdate-origin.sh BST_RDATE_STATE=$D/mode export BST_RDATE_STATE mode canned origin=0 if ! command -v socat >/dev/null 2>&1; then for c in origin-up canned-print canned-host canned-rc eof-diagnosed set-fetch set-clock; do bst_skip "rdate/$c" "socat is not installed on this image; there is no RFC 868 origin to query" done elif [ ! -f "$ORIGIN" ]; then for c in origin-up canned-print canned-host canned-rc eof-diagnosed set-fetch set-clock; do bst_notok "rdate/$c" "$ORIGIN is missing from the brintos-selftests data payload" done else # BOTH streams go to files. This listener is deliberately never killed (see the # note at the end of the section), so a socat holding the SUITE's stdout open # would outlive the suite -- and the runner reads a suite's output to EOF, so # the report would never be delivered at all. That is indistinguishable # downstream from a suite that reported nothing. socat "TCP4-LISTEN:$PORT,bind=127.0.0.1,reuseaddr,fork" EXEC:"/bin/sh $ORIGIN" \ >"$D/socat.out" 2>"$D/socat.err" & socat_err=$(cat "$D/socat.err" 2>/dev/null) if wait_listen "$PORT"; then bst_ok rdate/origin-up; origin=1 elif bst_execfail "$socat_err"; then bst_unrun rdate/origin-up "$socat_err" else bst_notok rdate/origin-up "socat never reached LISTEN on 127.0.0.1:$PORT within 60s -- $(_bst_1line "$socat_err")" fi if [ "$origin" -ne 1 ]; then for c in canned-print canned-host canned-rc eof-diagnosed set-fetch set-clock; do bst_notok "rdate/$c" "$BST_UNRUN_TAG (brintos/linux#44): the RFC 868 origin could not be staged, so this assertion never ran" done fi fi if [ "$origin" -eq 1 ]; then # -n names the service; getaddrinfo takes a numeric port string, which is how # this stays off the privileged port 37 that DEFAULT_SERVICE ("time") resolves # to. -p is explicit even though it is also the default, because section 4 # needs -s -p and the two spellings should read the same. bg 90 canned "$RD" -p -n "$PORT" 127.0.0.1 if [ "$bg_done" -ne 1 ]; then bst_notok rdate/canned-print "rdate never returned from 127.0.0.1:$PORT (bounded at 90s)" bst_notok rdate/canned-host "rdate never returned" bst_notok rdate/canned-rc "rdate never returned" elif bst_execfail "$bg_out"; then bst_unrun rdate/canned-print "$bg_out" bst_unrun rdate/canned-host "$bg_out" bst_unrun rdate/canned-rc "$bg_out" else # The printed line is `rdate: []\t'. Both halves are asserted: # the host tag proves the report is about the server that was queried, and # the time is the arithmetic fact above. case "$bg_out" in *"$WANT"*) bst_ok rdate/canned-print ;; *) bst_notok rdate/canned-print "want:'$WANT' got:'$(_bst_1line "$bg_out")' (served 0xE75B4B80 = 1672531200 + 2208988800; TZ=$TZ)" ;; esac has rdate/canned-host "[127.0.0.1]" "$bg_out" if [ "$bg_rc" -eq 0 ]; then bst_ok rdate/canned-rc else bst_notok rdate/canned-rc "exited $bg_rc after a successful read, want 0 -- $(_bst_1line "$bg_out")"; fi fi ## ## 3. The error path the protocol actually has ## # A server that accepts and closes without writing is the one malformed reply # RFC 868 admits of. rdate's read loop then has bytes outstanding with nr == 0 # and must say so rather than reporting whatever was in its uninitialised # four-byte buffer -- which, since that buffer feeds ntohl() straight into a # time_t, is a case worth having: a port that skipped the check would print a # plausible-looking date here and pass every assertion in section 2. mode eof bg 90 eof "$RD" -p -n "$PORT" 127.0.0.1 if [ "$bg_done" -ne 1 ]; then bst_notok rdate/eof-diagnosed "rdate never returned from the empty-reply arm (bounded at 90s)" elif bst_execfail "$bg_out"; then bst_unrun rdate/eof-diagnosed "$bg_out" else case "$bg_out" in *"EOF from time server"*) if [ "$bg_rc" -eq 1 ]; then bst_ok rdate/eof-diagnosed else bst_notok rdate/eof-diagnosed "diagnosed the EOF but exited $bg_rc, want 1"; fi ;; *) bst_notok rdate/eof-diagnosed "rc=$bg_rc want:'EOF from time server' got:'$(_bst_1line "$bg_out")'" ;; esac fi ## ## 4. THE CLOCK-SETTING ARM -- a measurement, not an assertion ## # Served THIS GUEST's own time (see the header), so a settimeofday(2) that # unexpectedly succeeds moves the clock by about a second and harms nothing. # # /set-fetch and /set-clock are deliberately two cases over ONE run. The port's # own job -- fetch the time and report it -- is scored by /set-fetch and is # expected to pass. Whether the kernel then accepts the clock change is # /set-clock, and a refusal there is an ATTRIBUTED red against # brintos/linux-in-the-browser#182 rather than a defect in this port. Splitting # them is what keeps the platform gap from reading as a broken package. mode now NOW0=$(date -u +%s) bg 90 setclk "$RD" -s -p -n "$PORT" 127.0.0.1 if [ "$bg_done" -ne 1 ]; then bst_notok rdate/set-fetch "rdate never returned from the -s arm (bounded at 90s)" bst_notok rdate/set-clock "rdate never returned" elif bst_execfail "$bg_out"; then bst_unrun rdate/set-fetch "$bg_out" bst_unrun rdate/set-clock "$bg_out" else # -p keeps the fetched time on stdout even in set mode, so the fetch is # observable independently of what the clock did. The year is whatever this # guest believes; asserting the host tag alone keeps this independent of it. has rdate/set-fetch "[127.0.0.1]" "$bg_out" case "$bg_out" in *"could not set system time"*) bst_notok rdate/set-clock "settimeofday(2) refused (brintos/linux-in-the-browser#182 -- settimeofday(164) and clock_settime(227) absent from the wasm32 syscall table): $(_bst_1line "$bg_out")" ;; *) if [ "$bg_rc" -eq 0 ]; then NOW1=$(date -u +%s) _d=$(( NOW1 - NOW0 )); [ "$_d" -lt 0 ] && _d=$(( -_d )) if [ "$_d" -le 120 ]; then bst_ok rdate/set-clock else bst_notok rdate/set-clock "clock accepted the set but moved ${_d}s against a server serving this guest's own time"; fi else bst_notok rdate/set-clock "exited $bg_rc without the 'could not set system time' diagnostic -- $(_bst_1line "$bg_out")" fi ;; esac fi fi ## ## 5. THE TICKET'S SECOND CRITERION: an unreachable server exits non-zero ## # Nothing has ever listened on $DEAD in this boot, so connect(2) is refused # locally and no timeout is involved -- which matters, because rdate's own -t is # alarm-based and no signal is delivered here (brintos/linux#21). This is the one # failure path that does not depend on that. bg 60 refused "$RD" -p -t 5 -n "$DEAD" 127.0.0.1 if [ "$bg_done" -ne 1 ]; then bst_notok rdate/refused "rdate never returned from a closed port (bounded at 60s) -- its own -t is alarm-based and no signal is delivered here (brintos/linux#21)" elif bst_execfail "$bg_out"; then bst_unrun rdate/refused "$bg_out" elif [ "$bg_rc" -eq 0 ]; then bst_notok rdate/refused "exited 0 against a closed port, want non-zero -- $(_bst_1line "$bg_out")" else bst_ok rdate/refused fi ## ## 6. UDP -- declared, and deliberately not exercised here ## # RFC 868 also runs over UDP and rdate reaches it with -u, but a UDP origin needs # a SECOND socat, and the guest refuses a second large exec under fragmentation # (brintos/linux#44 -- 60-ckermit.test measured it failing with 868 MB nominally # free). Spending the image's one remaining large exec on the arm the ticket did # not name, and losing section 2 to it, is the wrong trade. Recorded as a skip so # the gap in coverage is visible rather than merely absent. bst_skip rdate/udp "the -u arm needs a second socat exec; the guest refuses one under fragmentation (brintos/linux#44)" ## ## 7. The clock this suite must not have moved far ## # Section 4 may legitimately have set it -- to within a second of where it was, # which is the whole reason that arm is served `now'. Anything beyond the time # the suite itself took means the clock was stepped, and the next suite in this # boot would pay for it. NOWEND=$(date -u +%s) if [ "$NOWEND" -ge "$SUITE_T0" ] && [ $(( NOWEND - SUITE_T0 )) -le 900 ]; then bst_ok rdate/clock-untouched else bst_notok rdate/clock-untouched "clock moved from $SUITE_T0 to $NOWEND during this suite" fi rm -rf "$D"