Lessons: makeinfo wrapper disaster, Sept 2026

Parent: radagast

Context

During ports bootstrap on radagast (FreeBSD 15.1 VM on pomelo), openssh-portable failed to build because its dependency devel/autoconf tried to build .info documentation using makeinfo with a -c flag not supported by the base system makeinfo (GNU texinfo 4.8). A series of escalating mistakes followed.

What went wrong

1. Backup after replacement

The command cp /usr/bin/makeinfo /usr/bin/makeinfo.real was issued as a background SSH task that ran AFTER /usr/bin/makeinfo had already been replaced with the wrapper script. Result: both makeinfo and makeinfo.real were shell scripts calling each other in a loop. The real binary was unrecoverable.

Rule: always capture the original BEFORE touching it. cp original original.real && cp wrapper original — never the other way around, and never in a command where ordering is uncertain.

2. Wrapper that called itself

The first wrapper did exec /usr/bin/makeinfo $args — i.e. called itself. This was caught, but only after processes had been forking for several minutes, consuming CPU and exhausting sshd connection slots.

Rule: when replacing a system binary with a wrapper, the wrapper must call the .real backup, not the original path. Double-check this before deploying.

3. Background task exit codes are unreliable

Using $? inside a double-quoted sh -c string caused the variable to expand in the parent shell before sh -c ran, so the status file always got 0. The fix is single-quoted sh -c strings: nohup sh -c '...make...; echo $? > status'.

Rule: always use single quotes around sh -c bodies that contain $?, $!, or other variables that must expand inside the child shell.

4. Polling loops DOS the target

Multiple concurrent until-loops SSHing to radagast every 15s overwhelmed sshd's MaxStartups limit. The VM became intermittently unreachable, compounding every other problem.

Rule: one check at a time. Never leave more than one background polling loop running against a target. Kill old ones before starting new ones. Prefer a single persistent tail pipe over repeated short connections.

5. QEMU SLIRP does not forward general UDP to the host

Syslog forwarding via UDP from radagast to pomelo (10.0.2.2 or 192.168.0.2) did not work. QEMU user networking (SLIRP) only forwards DNS UDP (port 53 to 10.0.2.3) and proxies outgoing TCP. Arbitrary UDP to the host gateway is silently dropped, even to the host's real LAN IP.

The correct approach for log streaming from a QEMU user-net guest: use a persistent SSH tail pipe — ssh root@guest 'tail -f /var/log/messages' >> /var/log/radagast.log — one long-running shell session, not tunneling.

6. makeinfo is not in FreeBSD 15.1 base

The base.txz for 15.1-RELEASE does not contain /usr/bin/makeinfo. The binary present on radagast was a leftover from an earlier FreeBSD installation carried through the upgrade chain. It is gone once overwritten and cannot be recovered from base.txz.

The fix: a stub shell script at /usr/bin/makeinfo that outputs a plausible --version string and touches any -o output file. This satisfies automake's missing script, which then allows the build to proceed without actual info file generation.

7. OPTIONS_UNSET=DOCS does not suppress unconditional build steps

Modern FreeBSD ports use OPTIONS_UNSET to gate optional install targets. But autoconf builds its .info files unconditionally during the build phase, not as an install option. OPTIONS_UNSET=DOCS has no effect on it. MAKE_ENV+=MAKEINFO=true also had no effect because the missing script calls makeinfo directly by name, not via the MAKEINFO variable.

The correct approach (for next time)

version 1  ·  updated 2026-10-06  ·  tags radagast, freebsd, lessons