. "$BST_LIB" # BST-TIMEOUT: 900 # ntimed 0.0+git db0abbb4 (meta-networking recipes-support) -- a from-scratch # NTP client, built by its own ./configure-generated Makefile with no # dependencies and no library. It ships one program, /usr/sbin/ntimed-client, # whose argv[1] selects between three mains: the default steering client, a # bounded --poll-server that only measures, and --sim-client. # # --------------------------------------------------------------------------- # TWO THINGS ARE MISSING AND THEY FAIL AT DIFFERENT PLACES # # There is no NTP server this guest can reach, and the clock-discipline calls # the steering client ends in are syscalls this kernel may not answer. Those are # separate faults with separate owners, and a suite that let either one stand for # "the network is down" would misattribute the other. So neither is guessed at: # data/ntimed-probe (src/ntimed_probe.c) measures each one directly, and every # arm below is skipped only on the probe reading that confirms its own absence. # Section 7 then asserts the reading and what ntimed-client did AGREE, so a skip # can never be this suite quietly giving up. # # THE PROBE IS ALSO THE SERVER. There is no NTP daemon on this image and a canned # payload cannot serve one: ntimed echoes its own transmit timestamp into the # request and drops any reply whose origin timestamp does not match it byte for # byte (ntp_peer.c NTP_Peer_Poll), so the responder has to read each request to # answer it. `ntimed-probe server' binds 127.0.0.1:123 and does exactly that. # That buys the assertion nothing else can reach: the request ntimed PUTS ON THE # WIRE is scored (section 6), so a NTP_Packet_Pack that came out wrong on wasm32 # is caught here rather than surfacing as a client that never gets a reply. # # THE SERVER SERVES THIS GUEST'S OWN CLOCK, deliberately, the way 88-rdate.test's # -s arm does. The offset ntimed computes is then a few microseconds, so on the # day the clock-discipline syscalls start working the steering client has nothing # to correct and no later suite in the boot pays for this one. # # EVERY CONSTANT BELOW IS AN x86-64 ORACLE, taken from the identical sources # built natively and run against the identical probe before this suite was # written -- the usage text, the nine-name parameter table in registration # order, the -p diagnostics, and the [0 4 4 1 4 -20 ...] header ntimed # renders for the probe's reply. Nothing here is a guess about what the program # ought to print. NT=/usr/sbin/ntimed-client if [ ! -x "$NT" ]; then bst_skip ntimed/present "$NT is not installed on this image" exit 0 fi bst_ok ntimed/present D=${TMPDIR:-/tmp}/bst-ntimed.$$ rm -rf "$D"; mkdir -p "$D" || { bst_notok ntimed/workdir "cannot create $D"; exit 0; } SUITE_T0=$(date -u +%s) # 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 } # fails1 -- ntimed reports every argument error through # Fail(), which prints `Failure: ' to stderr and exit(1)s. Both halves are # asserted together because a build that kept the text and lost the status would # pass a substring check alone. fails1() { _id=$1; _needle=$2; shift 2 _out=$("$@" 2>&1); _rc=$? if bst_execfail "$_out"; then bst_unrun "$_id" "$_out"; return; fi case "$_out" in *"$_needle"*) if [ "$_rc" -eq 1 ]; then bst_ok "$_id" else bst_notok "$_id" "diagnosed it but exited $_rc, want 1"; fi ;; *) bst_notok "$_id" "rc=$_rc want:'$_needle' got:'$(_bst_1line "$_out")'" ;; esac } # bg -- run something that MAY NEVER RETURN and poll for # it, without wait(1). ntimed-client's default main never exits by design, and # no signal is delivered on this image (brintos/linux#21), so it cannot be timed # out from outside either; a foreground run would cost the whole suite, since the # runner buffers a suite's report until the suite exits. The rc travels out # through a file because $? is not available to a poll. 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) else bg_rc=""; fi bg_out=$(cat "$D/${_tag}.out" 2>/dev/null) } ## ## 1. Identity, usage, and the one argument error that needs no network ## # getopt's own `invalid option' line goes to stderr first; ntimed's Usage follows # it from Fail(). argv[0] is interpolated into that string, which is why the # needle carries the installed path. fails1 ntimed/usage "Usage $NT [-p param] [-t tracefile] servers..." "$NT" -Z # No peer arguments at all. main_client counts what NTP_PeerSet_Add returned and # refuses zero -- reached without a socket, a lookup or a packet. fails1 ntimed/no-peers "Failure: No NTP peers found" "$NT" ## ## 2. THE PARAMETER TABLE -- the ticket's named criterion ## # `-p ?' lists every registered parameter and stops. The ORDER is the assertion, # not just the membership: the table is assembled by three separate registrations # in main_client's fixed sequence -- PLL_Init() first (pll_std.c's five), then # Param_Register(client_param_table) (poll_rate, foo), then NF_Init() # (ntp_filter.c's two). A build that reordered them would still list nine names. _pl=$("$NT" -p '?' 2>&1) if bst_execfail "$_pl"; then for c in param-list param-list-order; do bst_unrun "ntimed/$c" "$_pl"; done else has ntimed/param-list "List of available parameters:" "$_pl" _names=$(printf '%s\n' "$_pl" | sed -n 's/^[[:space:]][[:space:]]*\([a-z_][a-z_0-9]*\)$/\1/p' | tr '\n' ' ') check_eq ntimed/param-list-order \ "pll_std_p_init pll_std_i_init pll_std_capture_time pll_std_stiffen_rate pll_std_p_limit poll_rate foo ntp_filter_average ntp_filter_threshold " \ "$_names" fi fails1 ntimed/param-list-rc "Stopping after parameter query." "$NT" -p '?' # A bare `-p ' prints that parameter's bounds and stops. The three numbers # are param_tbl.h's literals rendered by "%.3e", so they are exact, and each is # read as the line FOLLOWING its label rather than as a substring -- the labels # are one word each and would otherwise match a value that had drifted. _pq=$("$NT" -p poll_rate 2>&1) if bst_execfail "$_pq"; then for c in param-query-min param-query-max param-query-def; do bst_unrun "ntimed/$c" "$_pq"; done else _after() { printf '%s\n' "$_pq" | sed -n "/^$1:/{n;s/^[[:space:]]*//;p;}" | head -1 } check_eq ntimed/param-query-min "1.600e+01" "$(_after Minimum)" check_eq ntimed/param-query-max "4.096e+03" "$(_after Maximum)" check_eq ntimed/param-query-def "6.400e+01" "$(_after Default)" fi # The doc string is word-wrapped by param_wrapline() at 64 columns with its own # tab handling -- a hand-rolled formatter worth one case, since it is the only # code in the program that walks a string character by character. has ntimed/param-query-doc "Proportional term when PLL starts." "$("$NT" -p pll_std_p_init 2>&1)" ## ## 3. -p VALIDATION -- four rejections, four distinct diagnostics ## # Param_Tweak() reaches these before any socket is opened, so all four are # network-free and none of them can be blamed on a missing server. fails1 ntimed/param-unknown "-p unknown parameter 'nosuch' (try -p '?')" "$NT" -p nosuch fails1 ntimed/param-bad-value "-p 'poll_rate' bad value 'abc'" "$NT" -p poll_rate=abc fails1 ntimed/param-below-min "-p 'poll_rate' below min value (16)" "$NT" -p poll_rate=1 fails1 ntimed/param-above-max "-p 'poll_rate' above max value (4096)" "$NT" -p poll_rate=99999 ## ## 4. SERVER-NAME AND ADDRESS PARSING ## # ntimed never writes the port 123 anywhere: every peer is resolved with # getaddrinfo(host, "ntp"), so /etc/services decides whether a peer argument can # be parsed at all. --poll-server is used for both cases because it is BOUNDED -- # it schedules its own end task and returns -- while the default client loops # forever; both errors are raised while the peer set is being built, before # either mode reaches its run loop. fails1 ntimed/peer-unresolvable "hostname 'no.such.host.invalid', port 'ntp': " \ "$NT" --poll-server -d 1 no.such.host.invalid fails1 ntimed/peer-duplicate "Failure: hostname 127.0.0.1 is duplicated" \ "$NT" --poll-server -d 1 127.0.0.1 127.0.0.1 ## ## 5. THE PLATFORM PROBE -- what is actually missing, measured ## # data/ntimed-probe answers the two questions this suite must not guess at. It # arrives 0644 on a read-only root, so it is copied out and made executable in # /tmp, per the packager README. PROBE=$D/ntimed-probe have_net=0; have_adj=0; probe_ok=0 PROBE_IDS="net-udp-socket net-getaddrinfo-ntp net-bind-123 net-udp-loopback platform-ntp-adjtime platform-clock-settime" if [ ! -f "$BST_DATA/ntimed-probe" ]; then bst_notok ntimed/probe-stage "data/ntimed-probe is missing from the brintos-selftests payload" for c in $PROBE_IDS; do bst_notok "ntimed/$c" "data/ntimed-probe is missing from the brintos-selftests payload" done elif ! cp "$BST_DATA/ntimed-probe" "$PROBE" 2>/dev/null || ! chmod +x "$PROBE" 2>/dev/null; then bst_notok ntimed/probe-stage "cannot stage $BST_DATA/ntimed-probe into $D" for c in $PROBE_IDS; do bst_notok "ntimed/$c" "cannot stage $BST_DATA/ntimed-probe into $D" done else bst_ok ntimed/probe-stage _cp=$("$PROBE" caps 2>&1) if bst_execfail "$_cp"; then for c in $PROBE_IDS; do bst_unrun "ntimed/$c" "$_cp"; done else probe_ok=1 printf '%s\n' "$_cp" | sed -n 's/^NTMV-/# ntimed-probe: /p' _cap() { printf '%s\n' "$_cp" | sed -n "s/^NTMC-$1 \\([a-z]*\\) .*/\\1/p" | head -1; } _why() { printf '%s\n' "$_cp" | sed -n "s/^NTMC-$1 [a-z]* //p" | head -1; } # These four must hold. They are the port's own preconditions, not platform # gaps: a UDP socket, the ntp service name, the privileged bind the local # server needs, and a datagram that comes back. for p in udp4-socket:net-udp-socket getaddrinfo-ntp:net-getaddrinfo-ntp \ bind-ntp-port:net-bind-123 udp-loopback:net-udp-loopback; do _n=${p%%:*}; _id=${p#*:} if [ "$(_cap "$_n")" = "yes" ]; then bst_ok "ntimed/$_id" else bst_notok "ntimed/$_id" "probe: $_n $(_why "$_n")"; fi done if [ "$(_cap bind-ntp-port)" = "yes" ] && [ "$(_cap udp-loopback)" = "yes" ]; then have_net=1 fi # These two are READINGS, not assertions about ntimed. ntp_adjtime(2) is # where time_unix.c kt_setfreq() ends, and its result goes straight into # assert(i >= 0) -- an ENOSYS there is an abort inside ntimed-client, not a # diagnostic it can report. clock_settime(2) is the other half, the step # arm's AZ(). 88-rdate.test records clock_settime(227) and settimeofday(164) # as absent from the wasm32 syscall table # (brintos/linux-in-the-browser#182); whether adjtimex is too is what this # reading settles, and section 7 holds ntimed-client to the same answer. if [ "$(_cap ntp-adjtime-freq)" = "yes" ]; then have_adj=1; bst_ok ntimed/platform-ntp-adjtime else bst_notok ntimed/platform-ntp-adjtime \ "ntp_adjtime(MOD_FREQUENCY) refused: $(_why ntp-adjtime-freq) -- the slew arm of time_unix.c cannot run on this kernel (sibling of brintos/linux-in-the-browser#182, which records clock_settime(227) and settimeofday(164) absent from the wasm32 syscall table)" fi if [ "$(_cap clock-settime)" = "yes" ]; then bst_ok ntimed/platform-clock-settime else bst_notok ntimed/platform-clock-settime \ "clock_settime(2) refused: $(_why clock-settime) -- brintos/linux-in-the-browser#182, clock_settime(227) absent from the wasm32 syscall table, so kt_step()'s AZ() cannot succeed" fi fi fi ## ## 6. THE WIRE -- what ntimed actually sends, and what it does with a reply ## # Gated on the probe's OWN reading of the two capabilities this arm needs, and on # nothing else: if the probe says the port can be bound and loopback carries a # datagram, this arm runs. A skip here is therefore always traceable to a # measured absence, never to the suite declining to try. WIRE_IDS="wire-request-form wire-request-count wire-reply-parsed wire-refid wire-run-completed" if [ "$probe_ok" -ne 1 ]; then for c in $WIRE_IDS; do bst_notok "ntimed/$c" "the platform probe did not run, so this arm never had a server" done elif [ "$have_net" -ne 1 ]; then for c in $WIRE_IDS; do bst_skip "ntimed/$c" "probe: bind-ntp-port=$(_cap bind-ntp-port) udp-loopback=$(_cap udp-loopback) -- no NTP server can be stood up on this guest, so there is nothing for ntimed to poll" done else # The server outlives the client on purpose: it is never killed (no signal is # delivered here, brintos/linux#21), so it must end by itself. Both its streams # go to a file -- a child holding the SUITE's stdout open would keep the runner # waiting for an EOF that never comes, which downstream is indistinguishable # from a suite that reported nothing. "$PROBE" server 30 > "$D/srv.out" 2>&1 & _n=0 while [ "$_n" -lt 20 ]; do grep -q '^NTMS-listen OK' "$D/srv.out" 2>/dev/null && break _n=$((_n+1)); sleep 1 done # --poll-server is the mode this arm wants: it wires Time_Unix_Passive(), so it # touches no clock at all, and it schedules its own end task at -d seconds and # returns. Peer polls fall at roughly 0, 1 and 3 seconds in (ntp_peerset.c's # init_duration/init_packets ramp), so -d 5 buys three exchanges. bg 60 poll "$NT" --poll-server -d 5 127.0.0.1 _srv=$(cat "$D/srv.out" 2>/dev/null) # THE REQUEST. NTP_Tool_Client_Req() builds it: leap UNKNOWN (3), version 4, # mode 3 (client), stratum 0, poll 4, precision -6, in 48 bytes. Asserting it # from the SERVER side is what makes this a statement about the port's packing # rather than about the network -- nothing else on this image emits that # header, and NTP_Packet_Pack is the only code that could get it wrong. has ntimed/wire-request-form \ "len=48 li=3 vn=4 mode=3 stratum=0 poll=4 prec=-6" "$_srv" _reqs=$(printf '%s\n' "$_srv" | grep -c '^NTMS-req ') if [ "${_reqs:-0}" -ge 2 ]; then bst_ok ntimed/wire-request-count else bst_notok ntimed/wire-request-count "the server saw ${_reqs:-0} request(s) in 5s of --poll-server, want >= 2"; fi if [ "$bg_done" -ne 1 ]; then for c in wire-reply-parsed wire-refid wire-run-completed; do bst_notok "ntimed/$c" "--poll-server -d 5 had not returned after 60s" done elif bst_execfail "$bg_out"; then for c in wire-reply-parsed wire-refid wire-run-completed; do bst_unrun "ntimed/$c" "$bg_out" done else # THE REPLY, read back through ntimed's own renderer. NTP_Tool_Format lays # the fields out in fixed columns, so this literal pins leap 0, version 4, # mode 4 (server), stratum 1, poll 4 and precision -20 -- every header field # the probe set, decoded by the port's own NTP_Packet_Unpack. has ntimed/wire-reply-parsed "Poll 127.0.0.1 127.0.0.1 [0 4 4 1 4 -20 " "$bg_out" # The refid the probe stamps, "BSTP", arrives as the four bytes 0x42535450. # It is the one header field that travels by memcpy on both sides rather # than through the endian helpers, so it separates a byte-order fault from a # copy that lost bytes. has ntimed/wire-refid "0x42535450" "$bg_out" if [ "$bg_rc" -eq 0 ]; then has ntimed/wire-run-completed "# Run completed" "$bg_out" else bst_notok ntimed/wire-run-completed "--poll-server exited $bg_rc -- $(_bst_1line "$bg_out")" fi fi fi ## ## 7. THE STEERING CLIENT, AND THE ASSERTION THAT THE TWO READINGS AGREE ## # main_client is the mode that disciplines the clock, and it is the mode that # never exits: TODO_Run loops forever and no signal can stop it here # (brintos/linux#21). So it is started in the background, left to run, and read # through its own -t trace file. It is deliberately given the SAME loopback # server, which serves this guest's own clock, so a kernel that unexpectedly # accepts the discipline calls steers by microseconds and harms nothing. CL_IDS="client-peer-found client-param-report client-param-tweak discipline-agrees" if [ "$probe_ok" -ne 1 ] || [ "$have_net" -ne 1 ]; then for c in $CL_IDS; do bst_skip "ntimed/$c" "no NTP server could be stood up (section 5's reading), so the steering client has nothing to poll" done else "$PROBE" server 45 > "$D/srv2.out" 2>&1 & _n=0 while [ "$_n" -lt 20 ]; do grep -q '^NTMS-listen OK' "$D/srv2.out" 2>/dev/null && break _n=$((_n+1)); sleep 1 done ( "$NT" -p poll_rate=128 -t "$D/cli.trace" 127.0.0.1 > "$D/cli.out" 2>&1 ) & _n=0 while [ "$_n" -lt 40 ]; do grep -q '^# param ntp_filter_threshold' "$D/cli.trace" 2>/dev/null && break _n=$((_n+1)); sleep 1 done sleep 25 _tr=$(cat "$D/cli.trace" 2>/dev/null) _di=$(cat "$D/cli.out" 2>/dev/null) has ntimed/client-peer-found "# Found 1 peers" "$_tr" # Param_Report writes every registered parameter to the TRACE channel, which is # why -t is needed to see it at all; the -p override is visible in the same # line as the default it replaced, so the tweak and the report are one fact # each rather than two claims about the same line. has ntimed/client-param-report "# param ntp_filter_threshold 3 # min 0.01, max 10, default 3" "$_tr" has ntimed/client-param-tweak "# param poll_rate 128 # min 16, max 4096, default 64" "$_tr" # THE AGREEMENT. KERNPLL is the trace line kt_setfreq() writes immediately # after ntp_adjtime(2) returns -- so it appears if and only if the call was # reached AND did not abort the process at assert(i >= 0). Reaching it needs # the PLL to have climbed to track mode, which takes tens of seconds of good # packets, so its ABSENCE alone is not evidence of anything. What IS evidence, # and what this case scores, is a CONTRADICTION between the two readings: a # KERNPLL line on a guest whose probe called the syscall refused, or a probe # that called it available beside a client that never wrote one. _kp=$(printf '%s\n' "$_tr" | sed -n 's/^KERNPLL .*/&/p' | head -1) printf '# ntimed: KERNPLL="%s" probe-ntp-adjtime=%s\n' "$_kp" "$have_adj" if [ -n "$_kp" ]; then _i=${_kp##* } if [ "$have_adj" -ne 1 ]; then bst_notok ntimed/discipline-agrees \ "the probe read ntp_adjtime as refused, yet ntimed-client wrote '$_kp' -- the two disagree, and one of them is wrong about this kernel" elif [ "$_i" -ge 0 ] 2>/dev/null; then bst_ok ntimed/discipline-agrees else bst_notok ntimed/discipline-agrees "ntp_adjtime returned $_i inside ntimed-client -- assert(i >= 0) in kt_setfreq() would have aborted it" fi elif [ "$have_adj" -eq 1 ]; then bst_notok ntimed/discipline-agrees \ "the probe read ntp_adjtime as available, but ntimed-client wrote no KERNPLL line in 25s of polling -- either the PLL never reached track mode, or it died before it did: $(_bst_1line "$_di")" else # The consistent case: the probe measured the syscall refused, and the client # -- which cannot report that, only assert on it -- never got a KERNPLL line # out. Attributed rather than passed over: the red is on # ntimed/platform-ntp-adjtime, where the cause is named. bst_ok ntimed/discipline-agrees fi fi ## ## 8. The clock this suite must not have moved ## # Section 7 may legitimately have let the kernel steer it -- by microseconds, # against a server serving this guest's own time. Anything past the wall time the # suite itself took means something stepped it, and every later suite in the boot # would pay for that. NOWEND=$(date -u +%s) if [ "$NOWEND" -ge "$SUITE_T0" ] && [ $(( NOWEND - SUITE_T0 )) -le 900 ]; then bst_ok ntimed/clock-untouched else bst_notok ntimed/clock-untouched "clock moved from $SUITE_T0 to $NOWEND during this suite" fi rm -rf "$D"