. "$BST_LIB" # BST-TIMEOUT: 300 # htpdate 1.3.7 (meta-networking recipes-support) -- sets the system clock from # the Date header of an HTTP response, for networks whose firewall blocks NTP. # It is the first TIME client on this image, and that is the whole reason it is # interesting here: every other network port so far moves bytes, and this one # reads a clock and computes against the guest's own. # # THIS SUITE NEVER LETS htpdate SET THE CLOCK, and that is a requirement of the # port rather than caution. htpdate's query-only mode (-q, and the default) runs # setclock(timeavg, 0), which prints the offset and returns; -s/-a/-x reach # clock_settime(2) and adjtimex(2). A suite that moved this guest's clock would # corrupt every LATER suite in the same boot -- the runner runs them # sequentially in one guest -- and the damage would surface as unrelated # timestamp failures in packages that have nothing to do with htpdate. So every # invocation below is query-only, and /clock-untouched scores that property # directly rather than trusting the flag. # # THE OFFSET IS SIGNED THE WAY htpdate REPORTS IT, not the way the arithmetic # first suggests. getHTTPdate() returns `-first_offset + 1 - when/1e9` where # first_offset is `now - servertime` in whole seconds, so the printed figure is # (servertime - now) plus a sub-second rounding term in [0,1). A server whose # clock reads EARLIER than the guest's therefore reports a NEGATIVE offset -- # the correction htpdate would apply, not the error it observed. /canned-offset # below is the case that pins the sign and the magnitude together, because a # port that parsed the date but inverted the subtraction would still pass every # assertion that only looks for the word "Offset". # # TIME ON THIS IMAGE IS A KNOWN-WEAK AREA (BRINTOS-172). That is why the two # assertions that could depend on the guest's clock being CORRECT are written so # they do not: /lighttpd-offset-small compares the guest against a server that # IS the guest (lighttpd on loopback, same clock, so the difference is ~0 # whatever the absolute time reads), and /canned-offset computes its expectation # from `date -u +%s` read on the same machine in the same second. Both are # self-consistent under an arbitrarily wrong wall clock; neither would survive a # port that fails to parse the Date or mis-computes the delta. HT=/usr/sbin/htpdate if [ ! -x "$HT" ]; then bst_skip htpdate/present "$HT is not installed on this image" exit 0 fi bst_ok htpdate/present D=${TMPDIR:-/tmp}/bst-htpdate.$$ rm -rf "$D"; mkdir -p "$D/www" || { bst_notok htpdate/workdir "cannot create $D"; exit 0; } # Read BEFORE any htpdate runs, so /clock-untouched at the end compares against # a mark this suite took itself rather than against an assumption about boot. SUITE_T0=$(date -u +%s) # htpdate reads http_proxy from the environment unless -n is given, and a proxy # set there would send every URL below to something other than the origin under # test. -n is passed as well; this is the belt to its braces. http_proxy=; https_proxy=; no_proxy=127.0.0.1 export http_proxy https_proxy no_proxy # 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). No signal is # delivered on this image (brintos/linux#21), so nothing here -- not timeout(1), # not kill -KILL -- can stop an htpdate blocked on a socket a server accepted and # then abandoned, and a foreground run of one 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 a server's startup on KERNEL state, read straight # out of /proc/net/tcp. A TCP listener leaves no filesystem artifact to poll, so # the obvious `sleep N` races a server that is merely still loading -- and on this # guest that is not a small window: 60-ckermit.test measured a 54 MB socat taking # longer than three seconds to stage, and reported `Connection refused` against a # listener that came up moments later. /proc/net/tcp costs no exec (which ss(1) # and netstat(1) both would, per probe, in a guest that does not return memory on # process exit -- brintos/linux#21) and a port in state 0A is exactly the fact the # client is about to depend on. 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 } ## ## 1. Identity and the build contract ## # -v is the one path that prints the version htpdate.c compiled in, so it is the # assertion that the binary on the image is the recipe's 1.3.7 and not something # the image inherited. check_out htpdate/version "htpdate version 1.3.7" "$HT" -v # The help text enumerates the modes, and -q's presence in it is what makes the # query-only contract this suite depends on a property of THIS build rather than # an assumption about the version. # # CAPTURED ONCE AND ASSERTED TWICE, rather than two check_out calls. -h does exit # 0 (showhelp() then exit(0); the exit(1) in htpdate.c is the separate # no-servers-given path), so check_out would express either assertion correctly # -- it would just exec htpdate a second time to do it, and on a guest that does # not return memory on process exit (brintos/linux#21) an exec bought for nothing # is not free. _h=$("$HT" -h 2>&1) has htpdate/help-precision "precision" "$_h" has htpdate/help-query "-q" "$_h" ## ## 2. THE TICKET'S NAMED CRITERION: a real Date from the lighttpd on this image ## # server.event-handler and server.upload-dirs are carried for reasons that belong # to the image rather than to lighttpd or to htpdate -- epoll_create is ENOSYS # here (brintos/linux#53) so the epoll default never reaches listen(2), and # /var/volatile is never populated (BRINTOS-626) so the default /var/tmp # upload-dir does not resolve. 88-lighttpd.test scores each on its own terms; # naming both here is what a self-contained document root costs on this image. LT=/usr/sbin/lighttpd PORT=8079 if [ ! -x "$LT" ]; then bst_skip htpdate/lighttpd-up "lighttpd is not installed on this image; the ticket's named origin is absent" else echo ok > "$D/www/index.html" mkdir -p "$D/upload" cat > "$D/lighttpd.conf" < "text/html" ) CONF "$LT" -D -f "$D/lighttpd.conf" > "$D/server.log" 2>&1 & if ! wait_listen "$PORT"; then bst_notok htpdate/lighttpd-up "no listener on :$PORT after 60s -- $(_bst_1line "$(cat "$D/server.log" "$D/error.log" 2>/dev/null)")" bst_notok htpdate/lighttpd-parses-date "no origin to query" bst_notok htpdate/lighttpd-offset "no origin to query" bst_notok htpdate/lighttpd-offset-small "no origin to query" else bst_ok htpdate/lighttpd-up # -d prints the raw timestamp it extracted, which is the only way to assert # WHAT was parsed rather than merely that something was. -p 1 keeps it to a # single HEAD (the poll loop runs `precision` times on one connection), -n # ignores any proxy, -q is query-only. T0=$(date -u +%s) bg 90 lt "$HT" -q -n -d -p 1 "127.0.0.1:$PORT" T1=$(date -u +%s) if [ "$bg_done" -ne 1 ]; then bst_notok htpdate/lighttpd-parses-date "htpdate never returned from 127.0.0.1:$PORT (bounded at 90s)" bst_notok htpdate/lighttpd-offset "htpdate never returned" bst_notok htpdate/lighttpd-offset-small "htpdate never returned" else # lighttpd's Date is generated from the guest's own clock, so the day it # names is the day `date -u` names -- whatever that day actually is. This # asserts the parse against the server's real header without assuming the # guest's clock is CORRECT. TODAY=$(date -u '+%d %b %Y') if bst_execfail "$bg_out"; then bst_unrun htpdate/lighttpd-parses-date "$bg_out" else has htpdate/lighttpd-parses-date "$TODAY" "$bg_out"; fi # The debug line is ` , (N ms) => `; the # query-only report is `Offset %.3f seconds`, or `No time correction # needed` when the delta is exactly zero -- which against a server on the # same clock is a perfectly ordinary outcome, so both count. case "$bg_out" in *"Offset "*|*"No time correction needed"*) bst_ok htpdate/lighttpd-offset ;; *) if bst_execfail "$bg_out"; then bst_unrun htpdate/lighttpd-offset "$bg_out" else bst_notok htpdate/lighttpd-offset "rc=$bg_rc no offset report -- $(_bst_1line "$bg_out")"; fi ;; esac # lighttpd IS this guest, so the two clocks are one clock and the offset # can only be the round trip plus htpdate's sub-second rounding term. The # bound is the query's own wall-clock duration plus 3s of slack, which is # what makes this independent of how wrong the absolute time is. OFF=$(echo "$bg_out" | sed -n 's/^Offset \(-\{0,1\}[0-9]*\)\..*/\1/p' | tail -1) case "$bg_out" in *"No time correction needed"*) OFF=0 ;; esac BOUND=$(( T1 - T0 + 3 )) if [ -z "$OFF" ]; then bst_notok htpdate/lighttpd-offset-small "no numeric offset in the report -- $(_bst_1line "$bg_out")" else ABS=$OFF; [ "$ABS" -lt 0 ] && ABS=$(( -ABS )) if [ "$ABS" -le "$BOUND" ]; then bst_ok htpdate/lighttpd-offset-small else bst_notok htpdate/lighttpd-offset-small "offset ${OFF}s against a server on this guest's own clock, bound ${BOUND}s"; fi fi fi fi # THE SERVER IS LEFT RUNNING ON PURPOSE. No signal is delivered on this image # (brintos/linux#21), so a kill cannot stop it -- and the attempt is not free: # SIGTERM to a resident wasm user process faults the runtime out of # __wasm32_signal_dispatch_restart ("null function or function signature # mismatch"), which kills the Worker lane and takes the rest of the guest with # it. 88-lighttpd.test scores that delivery deliberately and is the right place # for it; here it would only cost this suite its remaining sections. The guest # is torn down when the runner powers off. : fi ## ## 3. A KNOWN Date, so the offset is an arithmetic fact rather than a range ## # Against lighttpd the served Date is always ~now, which cannot separate "parsed # the header" from "reported zero". A controlled origin pins both: the Date is a # literal this file chooses, so the offset htpdate must report is computable here # to the second, and its SIGN is observable. # # ONE socat EXEC SERVES BOTH CASES, and that is a constraint of the guest rather # than a tidiness. socat is a 54 MB image, the guest does not return memory on # process exit (brintos/linux#21) and refuses to stage a large exec under # fragmentation (brintos/linux#44) -- 60-ckermit.test measured a SECOND socat # exec already failing `Cannot allocate memory` with 868 MB nominally free. So # there is one listener, and the response is selected by the request path: # htpdate builds `HEAD / HTTP/1.1` from the URL, so /nodate reaches the # Date-less arm and everything else the dated one. htpdate-origin.sh holds both. # # IT RUNS AFTER THE lighttpd SECTION DELIBERATELY. This section is the suite's # own strengthening of the criterion; section 2 is the criterion the ticket # NAMED. If the guest has only one large exec left in it, the named half is the # half that should get it -- and a socat that then cannot stage reports through # bst_execfail as an attributed red against #44 rather than as a mystery. # # -t is REQUIRED and is not a workaround. htpdate's sanity check drops any # response more than DEFAULT_TIME_LIMIT (1 year) from the guest's clock, and a # 2023 literal is past that, so without -t the response is filtered and htpdate # exits 1 with "No server suitable for synchronization found" -- correct # behaviour, and the reason the case supplies the flag rather than choosing a # nearer date: a date close enough to survive the default limit would also be # close enough that an inverted subtraction could still land inside the # tolerance. EPOCH=1672531200 # 2023-01-01T00:00:00Z BST_HTPDATE_DATE='Sun, 01 Jan 2023 00:00:00 GMT' # the same instant, as served export BST_HTPDATE_DATE CPORT=8081 ORIGIN=$BST_DATA/htpdate-origin.sh if ! command -v socat >/dev/null 2>&1; then bst_skip htpdate/canned-date "socat is not installed on this image; no controlled origin for a known Date" bst_skip htpdate/canned-offset "socat is not installed on this image; no controlled origin for a known Date" bst_skip htpdate/no-timestamp "socat is not installed on this image; no controlled origin for a Date-less response" elif [ ! -f "$ORIGIN" ]; then bst_notok htpdate/canned-date "$ORIGIN is missing from the brintos-selftests data payload" bst_notok htpdate/canned-offset "$ORIGIN is missing" bst_notok htpdate/no-timestamp "$ORIGIN is missing" else # BOTH streams go to files, and stdout is the one that matters. This listener # is deliberately never killed (see the note at the end of the section), so a # socat holding the SUITE's stdout open outlives the suite -- and the runner # reads a suite's output to EOF. Measured while writing this file: with stdout # inherited, the suite ran every case to completion and the reader still had # not returned. That is indistinguishable downstream from the suite that # reported nothing, which is the failure this image keeps paying for. socat's # EXEC child talks on the socket, not on these, so redirecting costs nothing. socat "TCP4-LISTEN:$CPORT,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 "$CPORT"; then bst_ok htpdate/canned-origin-up origin=1 elif bst_execfail "$socat_err"; then bst_unrun htpdate/canned-origin-up "$socat_err" origin=0 else bst_notok htpdate/canned-origin-up "socat never reached LISTEN on 127.0.0.1:$CPORT within 60s -- $(_bst_1line "$socat_err")" origin=0 fi if [ "$origin" -ne 1 ]; then bst_notok htpdate/canned-date "$BST_UNRUN_TAG (brintos/linux#44): the controlled origin could not be staged, so this assertion never ran" bst_notok htpdate/canned-offset "$BST_UNRUN_TAG (brintos/linux#44): the controlled origin could not be staged, so this assertion never ran" bst_notok htpdate/no-timestamp "$BST_UNRUN_TAG (brintos/linux#44): the controlled origin could not be staged, so this assertion never ran" else NOW=$(date -u +%s) bg 90 canned "$HT" -q -n -t -d -p 1 "127.0.0.1:$CPORT" if [ "$bg_done" -ne 1 ]; then bst_notok htpdate/canned-date "htpdate never returned from the controlled origin (bounded at 90s)" bst_notok htpdate/canned-offset "htpdate never returned" elif bst_execfail "$bg_out"; then bst_unrun htpdate/canned-date "$bg_out" bst_unrun htpdate/canned-offset "$bg_out" else # htpdate copies 24 bytes from Date+11, i.e. everything after `Sun, `, and # strptime()s it as `%d %b %Y %T`. This asserts the exact substring, which # is what separates a parse from a lucky zero. has htpdate/canned-date "01 Jan 2023 00:00:00" "$bg_out" # The reported figure is the CORRECTION htpdate would apply -- getHTTPdate # returns `-first_offset + 1 - when/1e9` where first_offset is # (now - servertime) -- so a 2023 Date against any later clock is a large # NEGATIVE number. A port that parsed the header but inverted the # subtraction would report the same magnitude with the wrong sign and pass # every assertion that only looks for the word "Offset"; this is the case # that catches it. Tolerance 5s: the rounding term is < 1 and the rest is # the exchange. WANT=$(( EPOCH - NOW )) OFF=$(echo "$bg_out" | sed -n 's/^Offset \(-\{0,1\}[0-9]*\)\..*/\1/p' | tail -1) if [ -z "$OFF" ]; then bst_notok htpdate/canned-offset "no 'Offset N seconds' line -- $(_bst_1line "$bg_out")" else DIFF=$(( OFF - WANT )); [ "$DIFF" -lt 0 ] && DIFF=$(( -DIFF )) if [ "$DIFF" -le 5 ]; then bst_ok htpdate/canned-offset else bst_notok htpdate/canned-offset "reported ${OFF}s, computed ${WANT}s from the served '$BST_HTPDATE_DATE' (differ by ${DIFF}s)"; fi fi fi # THE ERROR PATH, from the same listener on the /nodate arm. A 200 with no # Date header is the one case where htpdate has nothing to parse: it prints # " no timestamp", abandons the poll loop, and exits 1 because no # server was suitable. Worth scoring because it is the branch that separates # "parsed a header" from "found a header" -- a port that read an # uninitialised buffer here would report an offset instead of an error, and # every assertion above would still pass. bg 90 nodate "$HT" -q -n -t -d -p 1 "127.0.0.1:$CPORT/nodate" if [ "$bg_done" -ne 1 ]; then bst_notok htpdate/no-timestamp "htpdate never returned from the Date-less arm (bounded at 90s)" elif bst_execfail "$bg_out"; then bst_unrun htpdate/no-timestamp "$bg_out" else case "$bg_out" in *"no timestamp"*) if [ "$bg_rc" -eq 1 ]; then bst_ok htpdate/no-timestamp else bst_notok htpdate/no-timestamp "diagnosed 'no timestamp' but exited $bg_rc, want 1"; fi ;; *) bst_notok htpdate/no-timestamp "rc=$bg_rc want:'no timestamp' got:'$(_bst_1line "$bg_out")'" ;; esac fi fi # The listener is left running, for the reason section 2 gives about lighttpd: # no signal is delivered here (brintos/linux#21), and the attempt is not free. : fi ## ## 4. The clock this suite must not have moved ## # The property, not the flag. Every run above was query-only, so the guest's # clock must have advanced only by the time the suite itself took. A port whose # query mode reached clock_settime(2) would have stepped it to 2023 in section 3 # -- which is exactly the damage that would otherwise surface as unrelated # timestamp failures in whichever suite the runner happens to run next. NOWEND=$(date -u +%s) if [ "$NOWEND" -ge "$SUITE_T0" ] && [ $(( NOWEND - SUITE_T0 )) -le 600 ]; then bst_ok htpdate/clock-untouched else bst_notok htpdate/clock-untouched "clock moved from $SUITE_T0 to $NOWEND during a query-only suite" fi rm -rf "$D"