# BST-TIMEOUT: 600 # # tftp-hpa 5.2 (meta-networking recipes-daemons): H. Peter Anvin's TFTP client # and server -- /usr/bin/tftp-hpa and /usr/sbin/in.tftpd-hpa. Unlike the two # TFTPs already on this image, BOTH HALVES OF THIS ONE WORK HERE, and that is # the result this suite exists to pin: # # inetutils-tftpd RFC 1350, and it only serves under inetd. # atftpd spawns a THREAD per request, and thread-mode clone is # refused on this substrate (linux-in-the-browser#85), so it # binds and cannot answer -- 88-atftp.test carries that red. # in.tftpd-hpa FORKS per request (tftpd.c:922), which this kernel does # support, so it is the first TFTP server on this image that # answers a request from its OWN listening socket. # # THREE WALLS. ONE IS THE ARM'S; THE OTHER TWO ARE tftp-hpa's OWN BUGS, MET # ONLY BECAUSE THIS GUEST IS AN UNUSUAL-LOOKING HOST. # # brintos/linux#19 -- arch/wasm32/kernel/syscall_table.c wires no [105] # setuid, [106] setgid, [113] setreuid, [114] setregid or [116] setgroups, so # each answers -ENOSYS. in.tftpd's forked child drops privilege BEFORE it # reads the request (initgroups, setregid, setreuid), so unpatched it # exit(EX_OSERR)s once per datagram and the client sees only `Transfer timed # out'. The port carries a patch that accepts ENOSYS at those three sites and # ONLY where the requested ids are already both the real and the effective # ones. tftp-hpa-server/drop-privilege-honoured below is the case that asserts # that scope, and it holds on both sides of #19 -- see its own comment. # # NO IPv6 -- socket(AF_INET6) answers EAFNOSUPPORT here. Upstream handles that # deliberately and then dies on the fd it just refused; see # tftp-hpa-server/ipv6-absent-nonfatal. # # ONLY LOOPBACK -- `ip -o addr' on this guest is one line, 127.0.0.1/8 scope # host. glibc's AI_ADDRCONFIG does not count loopback as a configured address, # so getaddrinfo() refuses every AF_INET answer, and in.tftpd resolved its own # -a BIND address with that flag: `cannot resolve local IPv4 bind address: # 127.0.0.1, Name or service not known'. Measured both ways -- `localhost' # fails identically, and `-a :PORT' (nothing to resolve) binds fine -- so it is # the flag, not the string and not the guest's /etc/hosts. The port asks with # AI_PASSIVE at the two bind sites instead. THE MAIN DAEMON BELOW BINDS # 127.0.0.1 ON PURPOSE for that reason: tftp-hpa-server/daemon-starts is the # case that would notice if the fix were lost, and `-a :PORT' would hide it. # # NOTHING HERE WAITS ON A PROCESS AND timeout(1) IS NOT USED, for two separate # reasons that happen to point the same way: # # - signal delivery is unreliable on this kernel (brintos/linux#21), so a # waiter can block forever on a child that has already finished; # - `sleep N' DOES NOT SLEEP on this image. Measured under 86-sysklogd.test: # sleep 1, 2, 3 and 5 all return in ~0.29 s (linux-in-the-browser#120). So # every bound below is an ITERATION COUNT, not a number of seconds, and the # sleep in each loop is a yield that lets the daemon be scheduled. . "$BST_LIB" require /usr/bin/tftp-hpa tftp-hpa/present TFTP=/usr/bin/tftp-hpa TFTPD=/usr/sbin/in.tftpd-hpa D=${TMPDIR:-/tmp}/bst-tftp-hpa.$$ rm -rf "$D"; mkdir -p "$D/srv" "$D/out" || { bst_notok tftp-hpa/workdir "cannot create $D"; exit 0; } chmod 755 "$D" "$D/srv" # Three payloads, each shaped for one question. # # payload.bin every byte class an octet transfer must carry unchanged: a bare # CR, a bare LF, NUL, 0xff, and a CRLF at the very end. # payload.txt ~8 KB of plain text, seventeen 512-byte blocks, so a short # netascii transfer is visible. # boundary.txt 2000 one-character lines. netascii puts \r\n on the wire for # every \n, so each line is 3 bytes there and the 512-byte wire # blocks (512 mod 3 = 2) cut a CR from its LF repeatedly. That is # exactly the case the recipe's # tftp-hpa-bug-fix-on-separated-CR-and-LF.patch exists for, and # nothing else in this suite would notice if it stopped applying. printf 'BIN\rSTART\n\000\001\002\377tail\r\n' > "$D/srv/payload.bin" awk 'BEGIN{for(n=1;n<=140;n++) printf "%04d the quick brown fox jumps over the lazy dog 0123456789\n", n}' > "$D/srv/payload.txt" awk 'BEGIN{for(n=1;n<=2000;n++) printf "a\n"}' > "$D/srv/boundary.txt" chmod 644 "$D/srv/payload.bin" "$D/srv/payload.txt" "$D/srv/boundary.txt" # running -- is that process still ALIVE, as opposed to merely present? # # `kill -0' cannot answer this. It succeeds on a ZOMBIE -- a process that has # exited and whose parent has not reaped it yet -- so a daemon that stopped # exactly as asked reads as still running. This suite hit that and it cost a # false red: SIGTERM on a daemon that had served a request left /proc// # status saying `Z (zombie)' while kill -0 kept succeeding, and whether the # shell had got round to reaping it varied with what else the suite had spawned. # Read the state. running() { case "$(sed -n 's/^State:[ \t]*//p' "/proc/$1/status" 2>/dev/null)" in "") return 1 ;; # gone entirely Z*) return 1 ;; # exited, awaiting reap -- dead for every purpose here *) return 0 ;; esac } # start_tftpd -- one foreground daemon, its own log. -L is # what makes the log useful: the port's third patch adds LOG_PERROR under -L, so # a daemon that refuses a request says why on stderr instead of into a syslog # socket this image has nobody listening on. start_tftpd() { "$TFTPD" -L -v -c -u "$2" -a 127.0.0.1:"$3" "$D/srv" >"$D/srv.$1.log" 2>&1 & eval "PID_$1=\$!" _i=0 while [ $_i -lt 12 ]; do eval "_p=\$PID_$1" running "$_p" || return 1 _i=$((_i+1)); sleep 1 done return 0 } srvpid() { eval "printf '%s' \"\$PID_$1\""; } # client_bg -- the client in the background on its own log, exit # status stamped into it; poll_client waits for the stamp. client_bg() { _t=$1; shift ( "$TFTP" "$@" >"$D/cli.$_t.log" 2>&1; echo "BSTRC=$?" >>"$D/cli.$_t.log" ) & } poll_client() { _i=0 while [ $_i -lt "$2" ]; do grep -q 'BSTRC=' "$D/cli.$1.log" 2>/dev/null && return 0 _i=$((_i+1)); sleep 1 done return 1 } clirc() { sed -n 's/^BSTRC=//p' "$D/cli.$1.log" 2>/dev/null | tail -1; } # reap_client -- a tftp client that did not finish keeps RETRANSMITTING, # and each retransmit drives an alarm(2)/siglongjmp round the kernel narrates on # the shared console. Left alive, one timed-out client turns every later case # into noise and grows the guest. Kill it the moment its case is decided. reap_client() { pkill -f "tftp-hpa 127.0.0.1 $1" >/dev/null 2>&1 || :; } # no_daemon -- report a transfer case that never had a server to talk to, # WITHOUT running a client at a closed port. That is not a stylistic choice: the # client answers a closed port by retransmitting on its own timer, and six of # them doing that simultaneously is what wedged the first run of this suite. no_daemon() { bst_notok "$1" "no daemon on 6979 to ask -- see tftp-hpa-server/daemon-starts: [$(_bst_1line "$(cat "$D/srv.main.log" 2>/dev/null)")]"; } # `wc -c < missing' fails in the SHELL, not in wc, so a 2>/dev/null on the # command does not silence it -- and a stray "No such file" on stdout is a line # the BST parser has to step over. Ask wc about the path instead. size() { [ -f "$1" ] && wc -c "$1" 2>/dev/null | awk '{print $1}' || printf 'absent'; } # Same shape: a failed `> /proc/...' is the shell's error, so the redirect has # to be inside the group the suppression applies to. quiet_printk() { { echo "$1" > /proc/sys/kernel/printk; } 2>/dev/null || dmesg -n "$1" >/dev/null 2>&1; } # --------------------------------------------------------------------------- # The two binaries, and the two DEPENDS THAT DID VANISH IN SILENCE. # # tftp-hpa's version string names the optional libraries it was linked against, # which makes it the cheapest possible check that DEPENDS = "tcp-wrappers # readline" produced linkage rather than merely a build order. A port that lost # either would still answer `tftp-hpa 5.2' and pass a bare version assertion. # # readline-linked is not hypothetical: the STOCK recipe ships a client reading # `without readline' on this image and says nothing about it, because make # re-runs configure during do_compile and the second run cannot link -lreadline # (libreadline wants the termcap globals PC/BC/UP, and tftp-hpa searches only # [termcap terminfo] for tputs while poky ships libtinfo). The port answers the # tputs search and stops the second configure; this case is what notices if # either half is lost again, and the loss is otherwise INVISIBLE -- the build # stays green and the log says readline was found. check_out tftp-hpa/version "tftp-hpa 5.2" "$TFTP" -V check_out tftp-hpa/readline-linked "with readline" "$TFTP" -V check_out tftp-hpa-server/version "tftp-hpa 5.2, with remap" "$TFTPD" -V check_out tftp-hpa-server/tcpwrappers-linked "with tcpwrappers" "$TFTPD" -V # THE ALTERNATIVES WINNER MOVED, AND THAT IS ASSERTED RATHER THAN DISCOVERED. # tftp-hpa registers the `tftp' update-alternative at priority 100 against # inetutils-tftp's 79, so /usr/bin/tftp is now the HPA client on this image. # Upstream's priority, left alone -- but the inetutils client must still be # reachable under its own name, because 88-inetutils.test and # 88-inetutils-net.test both drive /usr/bin/tftp.inetutils explicitly. alt=$("$TFTP" -V 2>&1); bare=$(/usr/bin/tftp -V 2>&1); inet=$(/usr/bin/tftp.inetutils --version 2>&1 | head -1) case "$bare" in *"tftp-hpa 5.2"*) case "$inet" in *inetutils*) bst_ok tftp-hpa/alternative-winner ;; *) bst_notok tftp-hpa/alternative-winner "/usr/bin/tftp is tftp-hpa but /usr/bin/tftp.inetutils no longer answers: [$(_bst_1line "$inet")]" ;; esac ;; *) bst_notok tftp-hpa/alternative-winner "/usr/bin/tftp did not resolve to tftp-hpa (priority 100 vs inetutils' 79): [$(_bst_1line "$bare")]" ;; esac # --------------------------------------------------------------------------- # A package-free loopback control. If this is red, every transfer case below is # reporting the guest's network rather than tftp-hpa. if command -v socat >/dev/null 2>&1; then ( socat -u -T 6 UDP4-LISTEN:9399,reuseaddr CREATE:"$D/udp.rx" >/dev/null 2>&1 ) & sleep 2 printf 'BST-UDP-OK' | socat -u -t 1 -T 3 STDIO UDP4-SENDTO:127.0.0.1:9399 >/dev/null 2>&1 i=0; while [ $i -lt 10 ]; do grep -q 'BST-UDP-OK' "$D/udp.rx" 2>/dev/null && break; i=$((i+1)); sleep 1; done if grep -q 'BST-UDP-OK' "$D/udp.rx" 2>/dev/null then bst_ok tftp-hpa/loopback-control else bst_notok tftp-hpa/loopback-control "a datagram to 127.0.0.1:9399 did not arrive: [$(_bst_1line "$(cat "$D/udp.rx" 2>/dev/null)")]"; fi else bst_skip tftp-hpa/loopback-control "socat is not on this image" fi # --------------------------------------------------------------------------- # THE SERVER, ON ITS OWN LISTENING SOCKET. No inetd, no super-server: -L is # in.tftpd's own standalone mode. # # The request names are ABSOLUTE. Without -s (chroot) tftp-hpa treats its # directory arguments as a prefix whitelist and requires the request to be an # absolute path inside one of them -- a relative name answers `Only absolute # filenames allowed'. -s is deliberately not used: it is a second variable on a # substrate whose chroot behaviour this port does not measure. if start_tftpd main root 6979; then MAIN_UP=1; bst_ok tftp-hpa-server/daemon-starts else MAIN_UP=0 bst_notok tftp-hpa-server/daemon-starts "in.tftpd-hpa exited instead of binding 127.0.0.1:6979 -- the two ways that happens are a lost patch 0005 (\"cannot resolve local IPv4 bind address\", AI_ADDRCONFIG vs a loopback-only host) and a lost patch 0004 (\"cannot open IPv6 socket\" then a bad-fd exit): [$(_bst_1line "$(cat "$D/srv.main.log" 2>/dev/null)")]" fi # THE GUEST HAS NO IPv6, AND THE DAEMON IS BUILT WITH IT. socket(AF_INET6) # answers EAFNOSUPPORT here, which upstream handles deliberately -- if the IPv4 # socket is good it logs "cannot open IPv6 socket, disable IPv6" and carries on # -- and then kills itself two lines later on set_socket_nonblock(-1), which # exits on any error. Patch 0004 puts that inside the guard the rest of the # function already uses. This case is its negative control and holds either way: # a daemon that never met the refusal is fine, and one that met it must still be # serving. It is stated separately from daemon-starts because a regression here # has a specific cause, and reading it off "the daemon did not start" costs the # next person the same afternoon it cost this one. case "$(cat "$D/srv.main.log" 2>/dev/null)" in *"disable IPv6"*) if [ "$MAIN_UP" = 1 ] then bst_ok tftp-hpa-server/ipv6-absent-nonfatal else bst_notok tftp-hpa-server/ipv6-absent-nonfatal "in.tftpd-hpa said it would disable IPv6 and then exited anyway -- patch 0004 is not in this build: [$(_bst_1line "$(cat "$D/srv.main.log" 2>/dev/null)")]"; fi ;; *) if [ "$MAIN_UP" = 1 ] then bst_ok tftp-hpa-server/ipv6-absent-nonfatal else bst_notok tftp-hpa-server/ipv6-absent-nonfatal "the daemon did not start and did not name IPv6 either: [$(_bst_1line "$(cat "$D/srv.main.log" 2>/dev/null)")]"; fi ;; esac # The kernel narrates on the same console the BST protocol uses, so a result # line can be lost inside the narration. Quieten it for the transfers by writing # /proc/sys/kernel/printk directly, which is the path that works here; busybox # dmesg -n stays only as a fallback, and neither is required for a result. PRINTK=$(cat /proc/sys/kernel/printk 2>/dev/null | awk '{print $1}') quiet_printk 1 # octet: every byte class survives, byte for byte. if [ "$MAIN_UP" = 1 ]; then client_bg get-octet 127.0.0.1 6979 -m octet -c get "$D/srv/payload.bin" "$D/out/got.bin" poll_client get-octet 40; reap_client 6979 if cmp -s "$D/srv/payload.bin" "$D/out/got.bin" then bst_ok tftp-hpa/get-octet else bst_notok tftp-hpa/get-octet "$(size "$D/srv/payload.bin") bytes offered, $(size "$D/out/got.bin") moved, rc=$(clirc get-octet) -- cli=[$(_bst_1line "$(cat "$D/cli.get-octet.log" 2>/dev/null)")] srv=[$(_bst_1line "$(tail -3 "$D/srv.main.log" 2>/dev/null)")]"; fi else no_daemon tftp-hpa/get-octet; fi # netascii: the text form. A Unix-to-Unix netascii round trip puts \r\n on the # wire for every \n and takes it off again, so the file must come back # IDENTICAL -- a conversion applied on only one side is exactly what a size # difference here would mean. if [ "$MAIN_UP" = 1 ]; then client_bg get-na 127.0.0.1 6979 -m netascii -c get "$D/srv/payload.txt" "$D/out/got.txt" poll_client get-na 40; reap_client 6979 if cmp -s "$D/srv/payload.txt" "$D/out/got.txt" then bst_ok tftp-hpa/get-netascii else bst_notok tftp-hpa/get-netascii "$(size "$D/srv/payload.txt") bytes offered, $(size "$D/out/got.txt") returned, rc=$(clirc get-na) -- cli=[$(_bst_1line "$(cat "$D/cli.get-na.log" 2>/dev/null)")]"; fi else no_daemon tftp-hpa/get-netascii; fi # ...including when the CR and the LF land in different 512-byte wire blocks, # which is the one thing the plain netascii case above cannot see. if [ "$MAIN_UP" = 1 ]; then client_bg get-bnd 127.0.0.1 6979 -m netascii -c get "$D/srv/boundary.txt" "$D/out/got.bnd" poll_client get-bnd 40; reap_client 6979 if cmp -s "$D/srv/boundary.txt" "$D/out/got.bnd" then bst_ok tftp-hpa/get-netascii-boundary else bst_notok tftp-hpa/get-netascii-boundary "CRLF split across a wire block was not reassembled: $(size "$D/srv/boundary.txt") offered, $(size "$D/out/got.bnd") returned, rc=$(clirc get-bnd) -- cli=[$(_bst_1line "$(cat "$D/cli.get-bnd.log" 2>/dev/null)")]"; fi else no_daemon tftp-hpa/get-netascii-boundary; fi # put: -c lets in.tftpd create the target, so this is a write the server had to # make itself rather than into a file the suite pre-placed. if [ "$MAIN_UP" = 1 ]; then client_bg put 127.0.0.1 6979 -m octet -c put "$D/srv/payload.bin" "$D/srv/uploaded.bin" poll_client put 40; reap_client 6979 if cmp -s "$D/srv/payload.bin" "$D/srv/uploaded.bin" then bst_ok tftp-hpa/put else bst_notok tftp-hpa/put "$(size "$D/srv/payload.bin") bytes offered, $(size "$D/srv/uploaded.bin") landed, rc=$(clirc put) -- cli=[$(_bst_1line "$(cat "$D/cli.put.log" 2>/dev/null)")] srv=[$(_bst_1line "$(tail -3 "$D/srv.main.log" 2>/dev/null)")]"; fi else no_daemon tftp-hpa/put; fi # A name that is not there must be REFUSED and reported. Note what tftp-hpa's # client does and does not do, both measured on an x86-64 control of the same # source: it prints the server's TFTP error, it creates the local file anyway, # and IT STILL EXITS 0. So the assertion is the error text plus an empty local # file -- asserting a non-zero exit would be asserting something this client has # never done, and asserting the file's absence would be asserting the same. if [ "$MAIN_UP" = 1 ]; then client_bg miss 127.0.0.1 6979 -m octet -c get "$D/srv/no-such-file" "$D/out/miss" poll_client miss 40; reap_client 6979 mout=$(cat "$D/cli.miss.log" 2>/dev/null) case "$mout" in *"File not found"*) if [ "$(size "$D/out/miss")" = "0" ] || [ ! -f "$D/out/miss" ] then bst_ok tftp-hpa/missing else bst_notok tftp-hpa/missing "reported and still wrote $(size "$D/out/miss") bytes"; fi ;; *) bst_notok tftp-hpa/missing "no for a missing name: [$(_bst_1line "$mout")]" ;; esac else no_daemon tftp-hpa/missing; fi quiet_printk "${PRINTK:-7}" # --------------------------------------------------------------------------- # A GENUINE PRIVILEGE DROP MUST STILL BE HONOURED -- the case that keeps the # ENOSYS patch narrow. # # The patch accepts ENOSYS only where the requested ids are already ours, so the # stock `-u nobody' still terminates the request on this substrate: nothing is # served and no file is written. A patch that accepted ENOSYS unconditionally # would serve that request AS ROOT and create a root-owned file, which is the # failure this case exists to catch. # # It holds on BOTH sides of #19 by asserting the outcome rather than the wall: # with the setters wired, the put succeeds and the file lands owned by 65534. # Either is a pass; a file owned by root is not. if start_tftpd drop nobody 6981; then client_bg drop 127.0.0.1 6981 -m octet -c put "$D/srv/payload.bin" "$D/srv/dropped.bin" poll_client drop 40; reap_client 6981 if [ ! -f "$D/srv/dropped.bin" ] || [ "$(size "$D/srv/dropped.bin")" = "0" ]; then case "$(cat "$D/srv.drop.log" 2>/dev/null)" in *"cannot set groups"*|*"cannot drop privileges"*) bst_ok tftp-hpa-server/drop-privilege-honoured ;; *) bst_notok tftp-hpa-server/drop-privilege-honoured "nothing was written and the daemon named no credential failure: [$(_bst_1line "$(tail -3 "$D/srv.drop.log" 2>/dev/null)")]" ;; esac else duid=$(ls -n "$D/srv/dropped.bin" 2>/dev/null | awk '{print $3}') if [ "$duid" = "65534" ] then bst_ok tftp-hpa-server/drop-privilege-honoured else bst_notok tftp-hpa-server/drop-privilege-honoured "asked to serve as nobody and wrote a file owned by uid ${duid:-unknown}"; fi fi kill -TERM "$(srvpid drop)" 2>/dev/null; kill -KILL "$(srvpid drop)" 2>/dev/null else bst_notok tftp-hpa-server/drop-privilege-honoured "in.tftpd-hpa -u nobody did not stay up to be asked: [$(_bst_1line "$(cat "$D/srv.drop.log" 2>/dev/null)")]" fi # --------------------------------------------------------------------------- # ...AND IT STOPS WHEN TOLD TO, IN BOTH OF THE STATES IT CAN BE IN. # # in.tftpd's SIGTERM handler only sets exit_signal (tftpd.c:151); the main loop # notices at the top of its next turn, and in standalone mode that loop sits in # select(fdmax+1, &readset, NULL, NULL, NULL) -- a NULL timeout, deliberately # ("Never time out if we're in standalone mode", tftpd.c:875). So the daemon # stops only if the signal actually breaks that select. # # IT DOES, HERE, IN BOTH STATES -- and that is worth a case rather than an # assumption, because the neighbouring atftpd does NOT (88-atftp.test carries # atftpd/stop-on-signal red against brintos/linux#21, where the signal sits # pending until an unrelated datagram wakes the select). Two daemons are used # because they are the two things that can differ: one NO CLIENT HAS EVER # TOUCHED, so nothing but the signal can wake it; and the main one, which has # served and therefore forked. No wake-up datagram is sent to either -- needing # one would itself be the finding. if start_tftpd quiet root 6982; then kill -TERM "$(srvpid quiet)" 2>/dev/null i=0; while [ $i -lt 12 ]; do running "$(srvpid quiet)" || break; i=$((i+1)); sleep 1; done if running "$(srvpid quiet)"; then bst_notok tftp-hpa-server/stop-on-signal "still in select() after SIGTERM on a socket no client has touched (${i} polls) -- that is brintos/linux#21's shape, which in.tftpd did NOT show when this port was written. daemon said [$(_bst_1line "$(tail -2 "$D/srv.quiet.log" 2>/dev/null)")]" kill -KILL "$(srvpid quiet)" 2>/dev/null else bst_ok tftp-hpa-server/stop-on-signal fi else bst_notok tftp-hpa-server/stop-on-signal "in.tftpd-hpa did not stay up to be signalled: [$(_bst_1line "$(cat "$D/srv.quiet.log" 2>/dev/null)")]" fi # The same signal to the daemon that has SERVED, so its select has returned, it # has forked, and its SIGCHLD disposition has been exercised. This is the case # that found the zombie trap above, and it is the one a reader should believe # about "in.tftpd shuts down cleanly". if [ "$MAIN_UP" = 1 ]; then kill -TERM "$(srvpid main)" 2>/dev/null i=0; while [ $i -lt 14 ]; do running "$(srvpid main)" || break; i=$((i+1)); sleep 1; done if running "$(srvpid main)" then bst_notok tftp-hpa-server/stop "still running ${i} polls after SIGTERM, having served: [$(_bst_1line "$(tail -3 "$D/srv.main.log" 2>/dev/null)")]" kill -KILL "$(srvpid main)" 2>/dev/null else bst_ok tftp-hpa-server/stop; fi else no_daemon tftp-hpa-server/stop fi rm -rf "$D"