. "$BST_LIB" # BST-TIMEOUT: 900 # fetchmail 6.4.38 -- second half: the ticket's actual criterion, two sessions # against a scripted POP3 server. Split from 88-fetchmail.test so each half gets # its own guest and its own bound; that file's header carries why. # # THE GATE NEEDS NO MAIL PROVIDER, AND THAT IS DELIBERATE. data/fetchmail-pop3d.sh # is an RFC 1939 server in POSIX sh behind a socat listener on loopback, so the # criterion is a real protocol dialogue this image drives end to end -- greeting, # USER, PASS, STAT, LIST, RETR, QUIT -- rather than a link test or a reachability # check against something outside the guest. # # THE SERVER FIXTURE RUNS NO EXTERNAL COMMAND, AND THAT IS LOAD-BEARING. Its # first version parsed each client line with `cut | tr` and sized the messages # with `wc` -- ordinary POSIX sh, and it took the guest down: a child of the # socat EXEC: process cannot stage an exec on this arm, `wc` produced nothing, # the arithmetic consuming it was an error, and the shell left the script BEFORE # ITS GREETING. fetchmail then held a connection to a server that would never # speak, the guest grew 4.5 GiB/min, and brintos_local_verify ended the boot with # zero cases reported from this file -- which reads as a fetchmail failure and is # not one. Same exec-staging class as brintos/linux#44 and #73. data/fetchmail- # pop3d.sh is now builtins only; keep it that way when editing it. # # THE ELEVEN REDS THIS FILE ONCE CARRIED WERE seteuid(), NOT THE HOSTNAME, AND # THE RECIPE NOW ANSWERS THEM. Recorded because the first diagnosis was WRONG in # a way worth leaving on the record: fetchmail prints two startup lines about # `getaddrinfo failed for "(none)"` -- the harness boots with `--init /bin/sh`, so # nothing applies /etc/hostname and the kernel default stands -- and those lines # were read as the cause. They are not. For POP3 the call is `host_fqdn(0)` # (fetchmail.c:1270), whose `required` arm is the only one that exits; with 0 it # warns, says "Trying to continue with unqualified hostname", and carries on. # The tell was the exit code: the poll ended rc=6, and PS_DNS is 11 -- 6 is # PS_IOERR. What made it look conclusive is that bst's `_bst_1line` keeps the # FIRST 100 characters, and the root warning plus the getaddrinfo line fill # exactly that, so the real error was truncated away every time. # # The real cause, from the untruncated stderr: # # fetchmail: Cannot switch effective user id to 0: Function not implemented # fetchmail: MDA error while fetching from tester@127.0.0.1 # fetchmail: Query status=6 (IOERR) # # open_mda_sink() drops privilege around the MDA popen() and treats a failed # seteuid() as fatal (sink.c:1221); the syscall is ENOSYS on this arm # (brintos/linux#19, #93 -- the whole set*id family is unwired, and glibc's # seteuid() rides setresuid). fetchmail_%.bbappend stanza (4) answers # `ac_cv_func_seteuid=no`, which compiles that block out and nothing else in the # tree; the seteuid abort is gone from the binary and does not come back. # # THAT FIX IS NECESSARY AND NOT SUFFICIENT, AND THE SECOND BLOCKER IS ALSO THE # PLATFORM'S. Measured 2026-08-22 by staging the rebuilt binary into the batch # image and re-running this file: the case counts do not move (pass=11 fail=11, # the same eleven), because the very next statement in open_mda_sink() fails -- # # fetchmail: MDA open failed <- popen(before, "w") returned NULL # fetchmail: MDA error while fetching from tester@127.0.0.1 # fetchmail: Query status=6 (IOERR) # # -- where the seteuid line used to be. The delivery agent is NOT the problem and # this file proves it in the same boot: `procmail -m ` run straight from the suite shell returns rc=0. What differs is HOW the # child is started. glibc implements popen() on top of posix_spawn(), which is # ENOSYS here because clone(CLONE_VM|CLONE_VFORK) is unwired, while the shell's # own fork/vfork+exec works -- the exact asymmetry brintos/linux#14 records. # # So all eleven reds are ONE attributed cause, brintos/linux#14: the MDA is never # spawned, so the poll stops after RETR 1 with an empty mailbox, taking session-rc, # proto-retr, proto-order and the seven mbox-*/mda-* cases with it. They flip green # when #14 closes, with no change to this file or to the port. The startup hostname # warning is unrelated and harmless: fetchmail stamps its Received: trace with the # unqualified name, which is what mbox-received-added asserts. # # THE DELIVERY IS THROUGH A REAL LDA, NOT A SHELL REDIRECTION. `mda "procmail -m # "` is what the ticket means by a local delivery agent, and procmail is # already on this image with its own suite (90-procmail.test) establishing that # it works on this arm. So a red here is a statement about the fetchmail half of # the pipe -- the fork/popen to the MDA, the CRLF stripping, the header # rewriting -- and not about mail delivery in general. # # WHAT THE ASSERTIONS ARE MADE OF. The server appends every client line to a # transcript, so the proto-* cases assert what fetchmail SENT, while the mbox-* # cases assert what came out of the far end. Both halves are needed: a port that # never issued RETR would still leave an empty mbox that a "no unexpected # content" check would accept, and a port that walked a perfect dialogue but # mangled the message on the way to the MDA would still leave a perfect # transcript. # # WHY THE MAILBOX HOLDS TWO MESSAGES. One message cannot distinguish a port that # fetched "the mailbox" from one that fetched "message 1", so the server offers # two, both are retrieved, and the delivered mbox is asserted to hold both in the # order the server listed them. # # NO BYTE COUNTS ARE ASSERTED AGAINST THE DELIVERED FILE, ON PURPOSE. fetchmail # prepends a Received: header carrying the local date, whose rendered length # moves with the day and the zone, so a size constant would be a case that goes # red on a calendar rather than on a defect. The octet counts that ARE asserted # -- 299, 279 and 578 -- are the server's own, computed from the payload it # holds, and they are what fetchmail parsed out of STAT and LIST. # # EVERY EXPECTED VALUE HERE WAS MEASURED, NOT PREDICTED. The same server, the # same two rc files and the same fetchmail 6.4.38 under meta-networking's # CVE-2025-61962.patch were run on an x86_64 Fedora 44 control on 2026-08-21, # with a native procmail 3.22 under meta-oe's seven patches as the MDA. It # produced rc=0, "2 messages for tester at 127.0.0.1 (578 octets).", the # eight-line transcript the proto-* cases look for, both messages in the folder # with headers and bodies byte-identical to the fixture, and rc=3 with # "Authorization failure" for the refusal. # # sslproto '' IS LOAD-BEARING AND IS THE ONE THING THIS PORT HAD TO BE TOLD. # fetchmail 6.4, built with SSL, treats STLS as MANDATORY unless the rc says # otherwise: against a plaintext server it sends CAPA, gets -ERR, and refuses the # session with "STLS is mandatory for this session, but server refused CAPA # command" and AUTHFAIL -- measured on the control before this line was added, so # it is upstream's documented default rather than anything about this arm. With # sslproto '' it skips the CAPA probe entirely, which is why CAPA does not appear # in the transcript the proto-order case pins. # # EVERYTHING IS STAGED IN TMPFS. fetchmail refuses an rc looser than 0700, so the # file must be one chmod(2) can narrow, and a top-level redirection onto the # guest's virtio-fs root can trap a hwjs lane and end the script # (linux-in-the-browser#169; 90-procmail.test carries the diagnosis). # # SPAWN BUDGET. exec on this arm is a fresh wasm module instantiation. This file # spends one socat listener, two fetchmail runs, and the two procmail children # fetchmail forks for delivery. # THE FIRST CASE IS A CANARY AND ASSERTS NOTHING ABOUT THE PACKAGE. # brintos_local_verify.py loses a suite's FIRST BST result line before the host # parser sees it (BRINTOS-1149, our own host tooling rather than the runner or # the guest) and counts the dropped line as a pass. A case that asserts nothing # absorbs that loss, so the harness reports the deficit and every real case below # is still counted -- the same idiom, and the same reason, as 82-double-conversion # and 83-flatbuffers. Remove it when 1149 is fixed. bst_ok fetchmail/suite-canary require fetchmail fetchmail/net-present # socat is the server's transport and procmail is the delivery agent; without # either, every case below would be reporting on the harness rather than on the # port. require socat fetchmail/net-socat-present require procmail fetchmail/net-procmail-present W=${TMPDIR:-/tmp}/bst-fetchmail-net.$$ rm -rf "$W"; mkdir -p "$W" || { bst_notok fetchmail/net-workdir "cannot create $W"; exit 0; } ME=$(id -un 2>/dev/null); [ -n "$ME" ] || ME=root PORT=21434 POP3D_LOG="$W/all.log"; export POP3D_LOG : > "$POP3D_LOG" socat TCP4-LISTEN:$PORT,bind=127.0.0.1,reuseaddr,fork \ EXEC:"/bin/sh $BST_DATA/fetchmail-pop3d.sh" 2>"$W/socat.err" & SPID=$! # A listener has no filesystem artifact to wait on, but the kernel publishes one: # /proc/net/tcp carries the local port in hex with state 0A (TCP_LISTEN). Reading # it costs the shell a file open, where a connect-probe would cost another socat # process -- and on this arm the staging, not the listen(2), is what takes the # time. Lifted verbatim from 88-imapfilter-net.test, whose header records the # three timed-out runs that produced this shape. HEXPORT=$(printf '%04X' $PORT) listening=0; n=0 while [ "$n" -lt 600 ]; do if grep -qi ":$HEXPORT 00000000:0000 0A" /proc/net/tcp 2>/dev/null; then listening=1; break; fi n=$((n+1)); sleep 1 done # NO `wait` HERE, DELIBERATELY. A fork-mode socat is a process group and wait(1) # blocks until the whole group is gone, which with a child still holding the last # connection it need not ever be -- the hang 88-imapfilter-net.test measured # twice. The kill is enough; the runner reaps. unserve() { kill $SPID 2>/dev/null; } lines_since() { tail -n +$(( $1 + 1 )) "$POP3D_LOG" 2>/dev/null; } count_lines() { wc -l < "$POP3D_LOG" 2>/dev/null || echo 0; } # mkrc : an rc that polls the scripted server and # delivers through procmail into . mkrc() { _rc=$W/rc-$1 { echo "poll 127.0.0.1 protocol POP3 port $PORT auth password" echo " user \"tester\" there with password \"$2\" is \"$ME\" here" echo " keep fetchall sslproto ''" echo " mda \"procmail -m $W/procmailrc-$1\"" } > "$_rc" chmod 600 "$_rc" { echo "VERBOSE=off" echo "DEFAULT=$3" echo "LOGFILE=$W/procmail-$1.log" echo ":0:" echo "$3" } > "$W/procmailrc-$1" } OK_CASES="fetchmail/session-rc fetchmail/reported-count fetchmail/proto-user fetchmail/proto-pass fetchmail/proto-stat fetchmail/proto-list fetchmail/proto-retr fetchmail/proto-quit fetchmail/proto-order fetchmail/mbox-created fetchmail/mbox-message-count fetchmail/mbox-headers-intact fetchmail/mbox-body-intact fetchmail/mbox-second-message fetchmail/mbox-received-added fetchmail/mbox-order fetchmail/mda-delivered-twice" REFUSE_CASES="fetchmail/auth-refused-rc fetchmail/auth-refused-message fetchmail/auth-refused-stops fetchmail/auth-refused-no-delivery" fm_bail() { _r=$1; shift; for _i in "$@"; do bst_notok "$_i" "$_r"; done; } # --- session 1: the ticket's criterion --------------------------------------- if [ "$listening" -eq 1 ]; then MBOX=$W/inbox mkrc ok secret "$MBOX" base=$(count_lines) out=$(fetchmail -f "$W/rc-ok" --nosyslog --idfile "$W/idfile" 2>&1); rc=$? log=$(lines_since "$base") if [ "$rc" -eq 0 ]; then bst_ok fetchmail/session-rc elif bst_execfail "$out"; then bst_unrun fetchmail/session-rc "$out" else bst_notok fetchmail/session-rc "rc=$rc $(_bst_1line "$out")"; fi # What fetchmail made of STAT: the count and the total octet size, in its own # words. A port that never parsed the STAT reply would still walk the rest of # the dialogue, so this is the only case that pins the number it read. case "$out" in *"2 messages for tester at 127.0.0.1 (578 octets)."*) bst_ok fetchmail/reported-count ;; *) bst_notok fetchmail/reported-count "$(_bst_1line "$out")" ;; esac # What the port SENT. has() { case "$log" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "not in the server transcript: $2 -- got $(_bst_1line "$log")" ;; esac } has fetchmail/proto-user 'USER tester' has fetchmail/proto-pass 'PASS secret' has fetchmail/proto-stat 'STAT' has fetchmail/proto-list 'LIST 1' has fetchmail/proto-retr 'RETR 2' has fetchmail/proto-quit 'QUIT' # The whole dialogue, in order and with nothing else in it. The has() cases # above name the commands the ticket names; this one is what would catch a # port that also issued something -- a stray UIDL, a DELE against a mailbox # the rc says to keep -- that the individual greps cannot see the absence of. check_eq fetchmail/proto-order \ "USER tester PASS secret STAT LIST 1 RETR 1 LIST 2 RETR 2 QUIT" \ "$(printf '%s\n' "$log" | tr '\n' ' ' | sed 's/ */ /g; s/ $//')" # What came out of the MDA. if [ -s "$MBOX" ]; then bst_ok fetchmail/mbox-created else bst_notok fetchmail/mbox-created "no delivered mailbox at $MBOX -- $(_bst_1line "$out")"; fi check_eq fetchmail/mbox-message-count 2 \ "$(grep -c '^Message-ID: /dev/null || echo 0)" # Headers and body of message 1, each asserted as the whole line, so a port # that had folded, re-wrapped or re-cased a header is visible. inbox() { if grep -qxF "$2" "$MBOX" 2>/dev/null; then bst_ok "$1" else bst_notok "$1" "line absent from the delivered mailbox: $2"; fi } inbox fetchmail/mbox-headers-intact 'Subject: BST-FETCH-ONE fixture' inbox fetchmail/mbox-body-intact 'fetch body line two' inbox fetchmail/mbox-second-message 'Subject: BST-FETCH-TWO fixture' # fetchmail's own trace header. It is the one thing in the delivered message # that fetchmail wrote rather than copied, so it is the only positive # evidence that this mailbox came through this port. if grep -q 'with POP3 (fetchmail-6.4.38)' "$MBOX" 2>/dev/null; then bst_ok fetchmail/mbox-received-added else bst_notok fetchmail/mbox-received-added "no fetchmail Received: trace in the delivered mailbox" fi check_eq fetchmail/mbox-order "one two" \ "$(awk '/^X-Bst-Fetch: /{printf "%s%s", sep, $2; sep=" "}' "$MBOX" 2>/dev/null)" # procmail's own log, which is the MDA saying it was invoked once per message # rather than once for a concatenation of both. check_eq fetchmail/mda-delivered-twice 2 \ "$(grep -c "^ Folder: $MBOX" "$W/procmail-ok.log" 2>/dev/null || echo 0)" else fm_bail "no listener reached state TCP_LISTEN on 127.0.0.1:$PORT within 600s -- $(_bst_1line "$(cat "$W/socat.err" 2>/dev/null)")" \ $OK_CASES fi # --- session 2: the refusal path --------------------------------------------- # A -ERR to PASS. The port must report it and stop; the transcript is what proves # it stopped, since a fetchmail that treated -ERR as success would go on to STAT # and deliver a mailbox it never authenticated for. if [ "$listening" -eq 1 ]; then MBOX2=$W/inbox-bad mkrc bad wrongpass "$MBOX2" base=$(count_lines) out=$(fetchmail -f "$W/rc-bad" --nosyslog --idfile "$W/idfile" 2>&1); rc=$? log=$(lines_since "$base") if [ "$rc" -eq 3 ]; then bst_ok fetchmail/auth-refused-rc elif bst_execfail "$out"; then bst_unrun fetchmail/auth-refused-rc "$out" else bst_notok fetchmail/auth-refused-rc "rc=$rc want=3 -- $(_bst_1line "$out")"; fi case "$out" in *"Authorization failure on tester@127.0.0.1"*) bst_ok fetchmail/auth-refused-message ;; *) bst_notok fetchmail/auth-refused-message "$(_bst_1line "$out")" ;; esac case "$log" in *STAT*) bst_notok fetchmail/auth-refused-stops "the session continued to STAT after a -ERR to PASS: $(_bst_1line "$log")" ;; *PASS*) bst_ok fetchmail/auth-refused-stops ;; *) bst_notok fetchmail/auth-refused-stops "no PASS reached the server: $(_bst_1line "$log")" ;; esac if [ -e "$MBOX2" ]; then bst_notok fetchmail/auth-refused-no-delivery "a mailbox was written for a session that never authenticated" else bst_ok fetchmail/auth-refused-no-delivery fi else fm_bail "no listener reached state TCP_LISTEN on 127.0.0.1:$PORT within 600s" $REFUSE_CASES fi unserve rm -rf "$W"