. "$BST_LIB" # xinetd is the THIRD daemon on this image (after sysklogd and cronie) and the # first that has to own a LISTENING TCP SOCKET and hand an accepted connection to # another program. That is what this suite is shaped around: almost nothing here # can be asserted from one command's output, because the answer is produced by a # second process that xinetd forks and execs with the socket already on its # stdin/stdout, and the only way to read it is to be the client on the other end. # # TWO PLATFORM GAPS had to be patched before any of it worked, and the cases that # guard them are called out below, because a regression in either is silent: # # * getrlimit(RLIMIT_NOFILE) returns ENOSYS -- the kernel wires no rlimit # syscall at all. set_fd_limit() exits(1) on that from init_daemon(), BEFORE # option parsing, and syscall_failed() cannot print because have_stderr is # not set yet. Unpatched, EVERY invocation -- `xinetd -version` included -- # exits 1 having produced not one byte. `xinetd/version` is the guard: it is # a one-line case whose real job is to prove the process reaches main(). # # * setgid/setuid/setgroups return ENOSYS (the same wall BRINTOS-196 hit in # cronie). set_credentials() answers any failure with _exit(1) and runs for # every service naming a user, ahead of the internal/external split -- so # with the stock configs' "user = root", xinetd accepts a connection and then # kills the child before it emits anything. `xinetd/echo-internal`, # `xinetd/external-server` and `xinetd/discard-internal` all go through it. # # Timing: sleep(1) does not sleep on this image (~0.29 s measured; # linux-in-the-browser#120), so every wait below is a BOUNDED POLL of a real # condition, never a fixed delay. One xinetd instance serves every socket case so # the suite pays the startup wait once against the runner's 60 s budget. require xinetd xinetd/present D=${TMPDIR:-/tmp}/bst-xinetd.$$ CONF=$D/x.conf PIDF=$D/x.pid OUT=$D/x.out rm -rf "$D"; mkdir -p "$D" CLI="tclsh $BST_DATA/xinetd-client.tcl" # --- identity, and the rlimit-regression guard -------------------------------- # -version writes to STDERR and exits 0. Unpatched this printed nothing and # exited 1, so asserting the version string here is what pins the rlimit fix. check_out xinetd/version "2.3.15.4" xinetd -version # The package's files, at the paths oe-core's recipe puts them. sbindir is # /usr/sbin here; itox is the C inetd.conf converter, xconv.pl the perl one. inv="" for f in /usr/sbin/xinetd /usr/bin/itox /usr/bin/xconv.pl /etc/xinetd.conf \ /etc/init.d/xinetd /etc/default/xinetd; do [ -e "$f" ] || inv="$inv $f" done check_eq xinetd/inventory "" "$inv" check_eq xinetd/resolves-to-port "/usr/sbin/xinetd" "$(command -v xinetd)" # The stock service definitions ship as an includedir. Their presence is what # makes `includedir /etc/xinetd.d` in xinetd.conf meaningful. missing="" for s in echo discard daytime time chargen; do [ -f "/etc/xinetd.d/$s" ] || missing="$missing $s" done check_eq xinetd/stock-service-defs "" "$missing" # Nothing in this binary is an unresolved-symbol trapping stub (BRINTOS-197). check_eq xinetd/no-trap-stubs "" "$(strings /usr/sbin/xinetd 2>/dev/null | grep signature_mismatch)" # --- the converters (no daemon needed) ---------------------------------------- printf 'ftp\tstream\ttcp\tnowait\troot\t/usr/sbin/ftpd\tftpd -l\n' > "$D/inetd.conf" check_out xinetd/itox-service "service ftp" sh -c "itox < $D/inetd.conf" check_out xinetd/itox-server "server = /usr/sbin/ftpd" sh -c "itox < $D/inetd.conf" # xconv.pl is the perl half of the pair; it also proves the xinetd-xconv package # and its perl RDEPENDS actually landed. check_out xinetd/xconv-service "service ftp" sh -c "xconv.pl < $D/inetd.conf" check_out xinetd/xconv-nameinargs "NAMEINARGS" sh -c "xconv.pl < $D/inetd.conf" # --- one instance, every socket case ------------------------------------------ # INTERNAL services are matched to a builtin BY SERVICE NAME (echo/discard/ # daytime/time/chargen); `id` is only a label. A block named anything else is # refused with "No such internal service" -- which is why the two access-control # services below are `echo` with distinct ids rather than invented names. # # The FORK/NO_FORK split in the builtin table is the reason daytime and echo are # both here: daytime-stream is NO_FORK, so xinetd answers it INLINE and never # reaches set_credentials(), while echo-stream FORKS and does. Keeping both means # a credential regression shows up as echo failing while daytime still passes, # which localises it immediately. cat > "$CONF" <"$OUT" 2>&1 & XPID=$! # Wait on the pidfile, then on the port actually answering -- the socket is bound # after the pidfile is written, so the second poll is the one that matters. i=0 while [ $i -lt 25 ]; do [ -s "$PIDF" ] && break; i=$((i+1)); sleep 1; done i=0 while [ $i -lt 25 ]; do case "$($CLI 9013 "" 500 2>&1)" in *"RESULT ok"*) break ;; esac i=$((i+1)); sleep 1 done pid=$(cat "$PIDF" 2>/dev/null) case "$pid" in [0-9]*) bst_ok xinetd/pidfile ;; *) bst_notok xinetd/pidfile "no pid in $PIDF: '$pid' ($(_bst_1line "$(cat "$OUT" 2>&1)"))" ;; esac # daytime: INTERNAL, NO_FORK -- answered inline by xinetd itself. RFC 867 is a # human-readable timestamp; assert the year rather than the whole string. check_out xinetd/daytime-internal "2026" $CLI 9013 "" 3000 # echo: INTERNAL, FORK -- the credential path. A round trip proves the child # survived set_credentials() AND that the builtin ran. check_out xinetd/echo-internal "BSTPING" $CLI 9007 "BSTPING " 3000 # discard: INTERNAL, FORK -- correct behaviour is to read and return NOTHING, so # this is the case that would catch a client-side false positive in the two above # (a harness that echoed its own payload back would fail here). check_out xinetd/discard-internal "len=0" $CLI 9009 "THROWMEAWAY " 2000 # The real deliverable: an EXTERNAL server. xinetd forks, sets credentials, and # execs /bin/cat with the accepted socket already on fd 0/1 -- so cat's own stdout # is the network. Nothing before this ticket on this image has done that. check_out xinetd/external-server "HELLO-EXTERNAL" $CLI 9100 "HELLO-EXTERNAL " 3000 # --- native access control (this is what replaces libwrap) -------------------- # only_from is xinetd's own check (AC_ADDRESS), not tcp_wrappers: the port is # built --without-libwrap because tcp-wrappers 7.6 does not compile with a # C23-default clang. A denial is an accept-then-close, so the pair below is # deliberately asymmetric -- the client CONNECTS both times, and only the body # differs. Testing only the deny half would pass just as well against a service # that was never started at all. check_out xinetd/only-from-deny "len=0" $CLI 9008 "NOPE " 2000 check_out xinetd/only-from-allow "ALLOWED" $CLI 9010 "ALLOWED " 3000 # --- shutdown ----------------------------------------------------------------- # Assert on the LISTENER going away rather than on the pid: with -dontfork the # shell's job id is what $! holds, and xinetd's own exit is what matters. kill -TERM "$XPID" 2>/dev/null i=0 while [ $i -lt 25 ]; do case "$($CLI 9007 "" 500 2>&1)" in *connfail*) break ;; esac i=$((i+1)); sleep 1 done if [ $i -lt 25 ]; then bst_ok xinetd/shutdown else bst_notok xinetd/shutdown "port 9007 still accepting after SIGTERM"; fi # --- a bad service definition is isolated, not fatal -------------------------- # An INTERNAL block whose name is not a builtin must be disabled while the rest # of the file keeps serving. Both halves are asserted, because either alone is # weak: the message alone would pass on a daemon that then died, and the closed # port alone would pass on a daemon that never started. # # -filelog is what makes the message readable at all, and the exact combination # matters. Measured on this image, three ways: # plain -- diagnostics go to syslog, and this image runs no syslogd # unless something starts one, so they are DISCARDED. # -d -- they go to the CONTROLLING TERMINAL, and not to this # shell's redirected stdout/stderr: setup_file_descriptors() # dup2()s /dev/null over fds 0 and 1 before any of this # runs, so `>file` on the command line captures nothing. # -filelog FILE -- they land in FILE. The only capturable channel, so it is # the one this case uses. # -d BEATS -filelog when both are given (no file is created at all), which is # why -d is deliberately absent below. Same shape as the finding BRINTOS-196 # recorded for crond, where syslog was the only channel it had. cat > "$D/bad.conf" <<'EOF' defaults { } service nosuchbuiltin { type = INTERNAL UNLISTED id = bst-bogus socket_type = stream protocol = tcp port = 9111 wait = no disable = no } service daytime { type = INTERNAL UNLISTED id = bst-still-alive socket_type = stream protocol = tcp port = 9014 wait = no disable = no } EOF xinetd -dontfork -filelog "$D/bad.log" -f "$D/bad.conf" -pidfile "$D/bad.pid" \ >"$D/bad.out" 2>&1 & BP=$! i=0 while [ $i -lt 25 ]; do case "$($CLI 9014 "" 500 2>&1)" in *"RESULT ok"*) break ;; esac i=$((i+1)); sleep 1 done if grep -q 'No such internal service' "$D/bad.log" 2>/dev/null; then bst_ok xinetd/bad-service-diag else bst_notok xinetd/bad-service-diag "not in filelog: $(_bst_1line "$(cat "$D/bad.log" 2>&1)")"; fi # ...and the same file is the proof that -filelog is a working log channel here, # not just a flag that is accepted. check_out xinetd/filelog "Started working" cat "$D/bad.log" # the bogus service must not be listening... check_out xinetd/bad-service-disabled "connfail" $CLI 9111 "" 1500 # ...while the valid service in the SAME file still answers. check_out xinetd/bad-config-survives "2026" $CLI 9014 "" 3000 kill -TERM "$BP" 2>/dev/null rm -rf "$D"