. "$BST_LIB" # BST-TIMEOUT: 600 # chrony 4.5 (meta-networking recipes-support/chrony) -- the NTP implementation # this image ships in place of a reference ntpd. Two programs, packaged apart: # /usr/sbin/chronyd, the daemon that both serves and consumes NTP, and # /usr/bin/chronyc, the control client that talks to it over the cmdmon # protocol rather than over NTP. # # --------------------------------------------------------------------------- # THIS SUITE RUNS THE DAEMON, AND THE THREE OPTIONS THAT MAKE THAT SAFE HERE # # A daemon suite on this platform normally cannot end: no signal is delivered to # a guest process (brintos/linux#21), so nothing started in the background can be # killed, and a foreground run would cost the whole suite -- the runner buffers a # suite's report until the suite exits. chronyd needs none of that machinery, # because it can be told to stop by itself: # # -t SECONDS main.c:687, SCH_AddTimeoutByDelay(timeout, quit_timeout, NULL). # It is a SCHEDULER timeout, not alarm(2) and not a signal, so it # fires on this platform exactly as it does anywhere. Every chronyd # this suite starts carries one and is expected to reap itself. # -x Don't control the clock. clock_settime(227) and settimeofday(164) # are absent from the wasm32 syscall table # (brintos/linux-in-the-browser#182, recorded by 88-rdate.test), so # the discipline path is not reachable here; -x means the daemon # never tries and no later suite in the boot pays for it. # -Q Log the offset and exit, for the client half of section 5. It # measures and returns; it is the one mode that puts a real NTP # exchange on the wire without asking to steer anything. # # Section 6 then asserts the clock did not move, so the claim that nothing here # touches it is measured rather than promised. # # --------------------------------------------------------------------------- # WHAT THE PORT ACTUALLY FIXED, AND WHERE THAT IS SCORED # # chronyc did not link. configure's editline probe answers Yes on `-ledit' # because readline() and add_history() are in libedit's own objects; the termcap # entry points libedit reaches on the first call (el_init -> terminal_init -> # terminal_set) are not, and an ncurses built --with-termlib keeps those six in # libtinfo. Everything on this arm is a static archive and wasmld refuses an # image with unresolved imports, so the shortfall was fatal at chronyc's link: # `phantom import: env.tgetent' and five more. The port adds a second configure # probe that asks the question the first one does not. # # THAT FAULT CANNOT REACH THIS SUITE, and it is worth being exact about why: it # is a LINK failure, so a regression of the patch itself fails the image build # and no test ever runs. What this suite can catch is the OTHER repair -- the one # that makes the build go green by dropping editline from PACKAGECONFIG instead # of fixing the probe. That leaves a chronyc that links, runs, and has quietly # lost its line editing, and the only place it shows is the feature list chronyc # prints. Hence chrony/chronyc-editline: it is a red exactly when someone has # bought the build back at the cost of the feature. # # --------------------------------------------------------------------------- # EVERY CONSTANT BELOW IS AN ORACLE, not a prediction. The version strings are # read out of the wasm32 binaries this port produced; the texts, exit statuses, # the -p round-trip and the two-daemon NTP exchange were taken from the identical # 4.5 sources built natively and run before this suite was written. 0x7F7F0101 is # ntp.h:113, NTP_REFID_LOCAL (127.127.1.1). CHRONYD=/usr/sbin/chronyd CHRONYC=/usr/bin/chronyc if [ ! -x "$CHRONYD" ] && [ ! -x "$CHRONYC" ]; then bst_skip chrony/present "neither $CHRONYD nor $CHRONYC is installed on this image" exit 0 fi bst_ok chrony/present D=${TMPDIR:-/tmp}/bst-chrony.$$ rm -rf "$D" if ! mkdir -p "$D"; then bst_notok chrony/workdir "cannot create $D"; exit 0; fi 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 } # The command socket's DIRECTORY is validated by chronyd itself before it will # bind: cmdmon refuses one that is not owned by uid 0 or that is group/world # writable, and says so in its log. Both are properties of the guest, not of the # port, so section 4 reads that log rather than assuming either way. RUN=$D/run mkdir -p "$RUN" chmod 750 "$RUN" 2>/dev/null # chrony_start [extra args...] -- start a chronyd whose output goes # to a FILE. A background child holding the suite's own stdout open would keep # the runner waiting for an EOF that never comes, which downstream is # indistinguishable from a suite that reported nothing. chrony_start() { _ctag=$1; _cconf=$2; shift 2 rm -f "$D/$_ctag.out" ( "$CHRONYD" -f "$_cconf" -d "$@" > "$D/$_ctag.out" 2>&1 ) & } # chrony_wait_sock -- 0 once the command socket is bound. chrony_wait_sock() { _wi=0 while [ "$_wi" -lt "$2" ]; do if [ -S "$1" ]; then return 0; fi _wi=$((_wi+1)); sleep 1 done return 1 } # chrony_env_refused -- true when chronyd declined the command socket for a # reason that is about this guest's filesystem rather than about chrony. Those # cases are skips with the daemon's own sentence quoted; anything else is a red. chrony_env_refused() { case "$1" in *"Wrong owner of"*|*"Wrong permissions on"*|*"Disabled command socket"*) return 0 ;; *) return 1 ;; esac } ## ## 1. IDENTITY -- the exact feature sets these two binaries were built with ## # The feature list is assembled from the configure results one macro at a time, # so it is the most compact statement available of what the port negotiated, and # a single flipped sign is a real change in what the image ships. It is therefore # asserted WHOLE -- but on its own, split from the version line that carries it. # # That split is not tidiness. A result detail is collapsed to 100 characters # (_bst_1line), and chronyd's full version line is 122: asserting it entire # produces a red whose `want:' and `got:' are both truncated to the same # identical-looking prefix, with the flipped sign beyond the cut. The feature # list alone is 91 characters, so the evidence survives into the failure message # -- which is the only place anyone will ever read it. Measured, not reasoned # about: the first run of this suite against the native 4.5 oracle produced # exactly that unreadable red. # # `${x##*(}' takes the LONGEST leading match, so it skips the "(chrony)" in the # program name and lands on the feature list; nothing inside that list is a # parenthesis. chrony_features() { _cf=${1##*\(}; printf '%s' "${_cf%)}"; } if [ ! -x "$CHRONYD" ]; then for c in chronyd-version chronyd-features; do bst_skip "chrony/$c" "$CHRONYD is not installed on this image" done else _dv=$("$CHRONYD" -v 2>&1) if bst_execfail "$_dv"; then for c in chronyd-version chronyd-features; do bst_unrun "chrony/$c" "$_dv"; done else has chrony/chronyd-version "chronyd (chrony) version 4.5" "$_dv" check_eq chrony/chronyd-features \ "+CMDMON +NTP +REFCLOCK +RTC -PRIVDROP -SCFILTER -SIGND +ASYNCDNS -NTS -SECHASH +IPV6 -DEBUG" \ "$(chrony_features "$_dv")" fi fi if [ ! -x "$CHRONYC" ]; then for c in chronyc-version chronyc-features chronyc-editline; do bst_skip "chrony/$c" "$CHRONYC is not installed on this image" done else _cv=$("$CHRONYC" -v 2>&1) if bst_execfail "$_cv"; then for c in chronyc-version chronyc-features chronyc-editline; do bst_unrun "chrony/$c" "$_cv" done else has chrony/chronyc-version "chronyc (chrony) version 4.5" "$_cv" check_eq chrony/chronyc-features \ "+READLINE -SECHASH +IPV6 -DEBUG" "$(chrony_features "$_cv")" # The port's own signal, scored on its own case so a regression names itself # rather than arriving as a diff of a long string. -READLINE here means the # editline/termcap shortfall was answered by dropping the feature. case "$_cv" in *"+READLINE"*) bst_ok chrony/chronyc-editline ;; *) bst_notok chrony/chronyc-editline \ "chronyc reports '$(_bst_1line "$_cv")' -- editline is no longer linked in, so the termcap fix in the configure probe has been replaced by disabling the feature" ;; esac fi fi ## ## 2. USAGE -- and the three options the rest of this suite depends on ## # The first line interpolates argv[0], so invoking by absolute path is what makes # it assertable. The three descriptions are not a sample: -p is section 3, -Q is # section 5, and -t is what lets a daemon suite terminate on this platform at # all. A build that lost any of them would take those sections with it. if [ ! -x "$CHRONYD" ]; then for c in chronyd-usage chronyd-opt-p chronyd-opt-Q chronyd-opt-t chronyd-badopt; do bst_skip "chrony/$c" "$CHRONYD is not installed on this image" done else _du=$("$CHRONYD" -h 2>&1) if bst_execfail "$_du"; then for c in chronyd-usage chronyd-opt-p chronyd-opt-Q chronyd-opt-t; do bst_unrun "chrony/$c" "$_du" done else has chrony/chronyd-usage "Usage: $CHRONYD [OPTION]... [DIRECTIVE]..." "$_du" has chrony/chronyd-opt-p "Print configuration and exit" "$_du" has chrony/chronyd-opt-Q "Log offset and exit" "$_du" has chrony/chronyd-opt-t "Exit after elapsed time" "$_du" fi # getopt's own diagnostic, then chronyd's Usage from the same path, exit 1. _db=$("$CHRONYD" -Z 2>&1); _dbrc=$? if bst_execfail "$_db"; then bst_unrun chrony/chronyd-badopt "$_db" else case "$_db" in *"invalid option -- 'Z'"*"Usage: $CHRONYD"*) if [ "$_dbrc" -eq 1 ]; then bst_ok chrony/chronyd-badopt else bst_notok chrony/chronyd-badopt "diagnosed it but exited $_dbrc, want 1"; fi ;; *) bst_notok chrony/chronyd-badopt "rc=$_dbrc got:'$(_bst_1line "$_db")'" ;; esac fi fi if [ ! -x "$CHRONYC" ]; then bst_skip chrony/chronyc-usage "$CHRONYC is not installed on this image" else _cu=$("$CHRONYC" --help 2>&1) if bst_execfail "$_cu"; then bst_unrun chrony/chronyc-usage "$_cu" else has chrony/chronyc-usage "Usage: $CHRONYC [OPTION]... [COMMAND]..." "$_cu"; fi fi ## ## 3. THE CONFIGURATION PARSER -- network-free, clock-free ## # `-p' parses the file, prints the directives it understood, and exits. That # makes it a round-trip: the output of a file chronyd accepted is that same file, # so one comparison covers the reader and the renderer at once and needs no # expected-text of its own. Everything in section 3 runs without a socket. CONF_IDS="conf-roundtrip conf-bad-directive conf-missing" if [ ! -x "$CHRONYD" ]; then for c in $CONF_IDS; do bst_skip "chrony/$c" "$CHRONYD is not installed on this image"; done else printf 'local stratum 10 orphan\nport 0\ncmdport 0\nmaxupdateskew 100.0\n' > "$D/p.conf" _pw=$(cat "$D/p.conf") _pr=$("$CHRONYD" -f "$D/p.conf" -p 2>&1) if bst_execfail "$_pr"; then bst_unrun chrony/conf-roundtrip "$_pr" else check_eq chrony/conf-roundtrip "$_pw" "$_pr"; fi # A directive no parser branch claims. The message carries the line number and # the file, which is the part that makes it useful and the part most likely to # rot; exit 1 is asserted with it, since text alone would pass on a build that # diagnosed the fault and carried on. printf 'nosuchdirective 1\n' > "$D/bad.conf" _bo=$("$CHRONYD" -f "$D/bad.conf" -p 2>&1); _brc=$? if bst_execfail "$_bo"; then bst_unrun chrony/conf-bad-directive "$_bo" else case "$_bo" in *"Fatal error : Invalid directive at line 1 in file $D/bad.conf"*) if [ "$_brc" -eq 1 ]; then bst_ok chrony/conf-bad-directive else bst_notok chrony/conf-bad-directive "diagnosed it but exited $_brc, want 1"; fi ;; *) bst_notok chrony/conf-bad-directive "rc=$_brc got:'$(_bst_1line "$_bo")'" ;; esac fi # strerror(ENOENT) reached through chronyd's own fatal path -- the arm that # proves the message above is about the CONTENT of a file that was opened. _mo=$("$CHRONYD" -f "$D/absent.conf" -p 2>&1); _mrc=$? if bst_execfail "$_mo"; then bst_unrun chrony/conf-missing "$_mo" else case "$_mo" in *"Fatal error : Could not open $D/absent.conf : No such file or directory"*) if [ "$_mrc" -eq 1 ]; then bst_ok chrony/conf-missing else bst_notok chrony/conf-missing "diagnosed it but exited $_mrc, want 1"; fi ;; *) bst_notok chrony/conf-missing "rc=$_mrc got:'$(_bst_1line "$_mo")'" ;; esac fi fi ## ## 4. THE DAEMON AND ITS CLIENT -- the cmdmon protocol over the unix socket ## # No network at all: `port 0' takes the NTP socket away and `cmdport 0' takes the # UDP command port, leaving the unix socket as the only way in. So a red in this # section is about chronyd, chronyc, or the protocol between them, and cannot be # about the guest's networking -- section 5 is where that is put at risk. # # `local stratum 10 orphan' makes the daemon its own reference source, which is # what gives tracking something deterministic to report without a peer: refid # 7F7F0101 at stratum 10. CM_IDS="daemon-starts cmd-tracking cmd-refid cmd-stratum cmd-leap cmd-csv-fields cmd-sources-header cmd-activity cmd-serverstats-counts cmd-unrecognized" if [ ! -x "$CHRONYD" ] || [ ! -x "$CHRONYC" ]; then for c in $CM_IDS; do bst_skip "chrony/$c" "this section needs both $CHRONYD and $CHRONYC, and one of them is not installed" done else printf 'local stratum 10 orphan\ndriftfile %s/drift\npidfile %s/d.pid\nport 0\ncmdport 0\nbindcmdaddress %s/d.sock\n' \ "$D" "$RUN" "$RUN" > "$D/d.conf" chrony_start d "$D/d.conf" -x -t 60 if chrony_wait_sock "$RUN/d.sock" 25; then bst_ok chrony/daemon-starts C="$CHRONYC -h $RUN/d.sock" _tr=$($C tracking 2>&1) if bst_execfail "$_tr"; then for c in cmd-tracking cmd-refid cmd-stratum cmd-leap; do bst_unrun "chrony/$c" "$_tr"; done else has chrony/cmd-tracking "Reference ID" "$_tr" # ntp.h:113 NTP_REFID_LOCAL, the 127.127.1.1 the `local' directive adopts. has chrony/cmd-refid "7F7F0101" "$_tr" has chrony/cmd-stratum "Stratum : 10" "$_tr" has chrony/cmd-leap "Leap status : Normal" "$_tr" fi # The same report in CSV, which is a different renderer over the same reply: # field 1 refid, 3 stratum, 14 leap. Reading it by position is the assertion # -- a column that moved would still contain all the right values somewhere. _cs=$($C -c tracking 2>&1) if bst_execfail "$_cs"; then bst_unrun chrony/cmd-csv-fields "$_cs" else _f1=$(printf '%s\n' "$_cs" | cut -d, -f1) _f3=$(printf '%s\n' "$_cs" | cut -d, -f3) _f14=$(printf '%s\n' "$_cs" | cut -d, -f14) check_eq chrony/cmd-csv-fields "7F7F0101|10|Normal" "$_f1|$_f3|$_f14" fi _so=$($C sources 2>&1) if bst_execfail "$_so"; then bst_unrun chrony/cmd-sources-header "$_so" else has chrony/cmd-sources-header "MS Name/IP address" "$_so"; fi # No peers are configured, so every counter is a known zero rather than a # number that merely parsed. _ac=$($C activity 2>&1) if bst_execfail "$_ac"; then bst_unrun chrony/cmd-activity "$_ac" else has chrony/cmd-activity "0 sources online" "$_ac"; fi # serverstats counts the cmdmon requests chronyd has answered, so a nonzero # figure here is the daemon's own account of the exchanges above -- the one # case in this section scored from the SERVER side rather than the client's. _ss=$($C serverstats 2>&1) if bst_execfail "$_ss"; then bst_unrun chrony/cmd-serverstats-counts "$_ss" else _cp=$(printf '%s\n' "$_ss" | sed -n 's/^Command packets received *: *\([0-9][0-9]*\).*/\1/p' | head -1) if [ -n "$_cp" ] && [ "$_cp" -ge 1 ] 2>/dev/null; then bst_ok chrony/cmd-serverstats-counts else bst_notok chrony/cmd-serverstats-counts \ "chronyd reported no command-packet count after several chronyc queries: '$(_bst_1line "$_ss")'"; fi fi _un=$($C nosuchcommand 2>&1); _unrc=$? if bst_execfail "$_un"; then bst_unrun chrony/cmd-unrecognized "$_un" else case "$_un" in *"Unrecognized command"*) if [ "$_unrc" -eq 1 ]; then bst_ok chrony/cmd-unrecognized else bst_notok chrony/cmd-unrecognized "diagnosed it but exited $_unrc, want 1"; fi ;; *) bst_notok chrony/cmd-unrecognized "rc=$_unrc got:'$(_bst_1line "$_un")'" ;; esac fi else _dl=$(cat "$D/d.out" 2>/dev/null) if chrony_env_refused "$_dl"; then bst_skip chrony/daemon-starts "chronyd declined the command socket for a property of this guest's filesystem, not of chrony: $(_bst_1line "$_dl")" for c in cmd-tracking cmd-refid cmd-stratum cmd-leap cmd-csv-fields \ cmd-sources-header cmd-activity cmd-serverstats-counts cmd-unrecognized; do bst_skip "chrony/$c" "no command socket was bound, so chronyc had nothing to talk to" done else bst_notok chrony/daemon-starts "no command socket at $RUN/d.sock after 25s -- $(_bst_1line "$_dl")" for c in cmd-tracking cmd-refid cmd-stratum cmd-leap cmd-csv-fields \ cmd-sources-header cmd-activity cmd-serverstats-counts cmd-unrecognized; do bst_notok "chrony/$c" "the daemon never bound its command socket, so this case never ran" done fi fi fi # chronyc against a socket that is not there. Its own diagnostic, not a shell # error, and exit 1 -- the negative control for every case in section 4. if [ ! -x "$CHRONYC" ]; then bst_skip chrony/cmd-no-daemon "$CHRONYC is not installed on this image" else _nd=$("$CHRONYC" -h "$D/absent.sock" tracking 2>&1); _ndrc=$? if bst_execfail "$_nd"; then bst_unrun chrony/cmd-no-daemon "$_nd" else case "$_nd" in *"Could not open connection to daemon"*) if [ "$_ndrc" -eq 1 ]; then bst_ok chrony/cmd-no-daemon else bst_notok chrony/cmd-no-daemon "diagnosed it but exited $_ndrc, want 1"; fi ;; *) bst_notok chrony/cmd-no-daemon "rc=$_ndrc got:'$(_bst_1line "$_nd")'" ;; esac fi fi ## ## 5. THE NTP WIRE -- one chronyd serving, another measuring against it ## # The section the package exists for, and the only one that needs the network. A # server chronyd binds 123 and offers its own clock at stratum 8; a second # chronyd runs `-Q', which polls it, logs the offset it computed and exits. # # THE SERVER SERVES THIS GUEST'S OWN CLOCK, deliberately -- the offset is then a # few microseconds, so even a kernel that unexpectedly accepted a steer would # have nothing to correct. `-Q' does not ask for one in any case: it measures and # returns, which is also why this arm is bounded without a signal. # # THE SKIP IS GATED ON THE SERVER'S OWN LOG. chronyd says `Could not open NTP # socket on 0.0.0.0:123' when the bind is refused, so the reading that decides # this section comes from the subject itself rather than from a guess about what # this guest allows. WIRE_IDS="wire-server-binds wire-offset-measured wire-server-counted" if [ ! -x "$CHRONYD" ] || [ ! -x "$CHRONYC" ]; then for c in $WIRE_IDS; do bst_skip "chrony/$c" "this section needs both $CHRONYD and $CHRONYC, and one of them is not installed" done else printf 'local stratum 8 orphan\ndriftfile %s/sdrift\npidfile %s/s.pid\nport 123\ncmdport 0\nbindcmdaddress %s/s.sock\nallow 127.0.0.1\n' \ "$D" "$RUN" "$RUN" > "$D/s.conf" chrony_start s "$D/s.conf" -x -t 90 chrony_wait_sock "$RUN/s.sock" 25 _sl=$(cat "$D/s.out" 2>/dev/null) case "$_sl" in *"Could not open NTP socket"*) bst_skip chrony/wire-server-binds "chronyd could not bind the NTP port on this guest: $(_bst_1line "$_sl")" for c in wire-offset-measured wire-server-counted; do bst_skip "chrony/$c" "no NTP server could be stood up on this guest, so there was nothing to poll" done ;; *) if [ ! -S "$RUN/s.sock" ]; then bst_notok chrony/wire-server-binds "the server chronyd never came up: $(_bst_1line "$_sl")" for c in wire-offset-measured wire-server-counted; do bst_notok "chrony/$c" "the server chronyd never came up, so this case never ran" done else bst_ok chrony/wire-server-binds # minpoll/maxpoll 0 with iburst so the exchange completes in seconds # rather than at chrony's default 64s cadence. printf 'server 127.0.0.1 minpoll 0 maxpoll 0 iburst\ndriftfile %s/cdrift\npidfile %s/c.pid\nport 0\ncmdport 0\nbindcmdaddress %s/c.sock\n' \ "$D" "$RUN" "$RUN" > "$D/c.conf" _qo=$("$CHRONYD" -f "$D/c.conf" -Q -d -t 30 2>&1); _qrc=$? if bst_execfail "$_qo"; then bst_unrun chrony/wire-offset-measured "$_qo" else # On success -Q logs the offset it computed and exits 0. Its own # failure text is quoted on a red, because `No suitable source for # synchronisation' at exit 1 is what a poll that never got a usable # reply looks like, and that is a different fault from a bad number. case "$_qo" in *"System clock wrong by"*) if [ "$_qrc" -eq 0 ]; then bst_ok chrony/wire-offset-measured else bst_notok chrony/wire-offset-measured "measured the offset but exited $_qrc, want 0"; fi ;; *) bst_notok chrony/wire-offset-measured \ "rc=$_qrc -- chronyd -Q did not complete an NTP exchange with the local server: '$(_bst_1line "$_qo")'" ;; esac fi # The other end of the same exchange. A client that reported an offset # while the server counted no packets would mean the number came from # somewhere other than the wire. _sv=$("$CHRONYC" -h "$RUN/s.sock" serverstats 2>&1) if bst_execfail "$_sv"; then bst_unrun chrony/wire-server-counted "$_sv" else _np=$(printf '%s\n' "$_sv" | sed -n 's/^NTP packets received *: *\([0-9][0-9]*\).*/\1/p' | head -1) if [ -n "$_np" ] && [ "$_np" -ge 1 ] 2>/dev/null; then bst_ok chrony/wire-server-counted else bst_notok chrony/wire-server-counted \ "the server chronyd counted ${_np:-no} NTP packet(s) while the client reported an offset: '$(_bst_1line "$_sv")'"; fi fi fi ;; esac fi ## ## 6. THE CLOCK THIS SUITE MUST NOT HAVE MOVED ## # Every chronyd above ran under -x or -Q, so none of them asked to steer. This is # where that stops being a claim about the options and becomes a measurement: # 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 600 ]; then bst_ok chrony/clock-untouched else bst_notok chrony/clock-untouched "clock moved from $SUITE_T0 to $NOWEND during this suite" fi rm -rf "$D"