# FreeBSD probe: nine runtime bugs on top of PR #135

Tested PR #135 (jaredmauch:master, last commit 2025-09-11) on FreeBSD.
It compiles cleanly, but does not run correctly. Nine bugs found and fixed:
11 files, +337/-21. Patch: `freebsd-runtime-fixes.patch`.

With these applied the probe initializes, runs all measurement types, and
completes the registration handshake up to `Permission denied (publickey)`,
which is the expected result for a key not yet submitted via
`https://atlas.ripe.net/apply/swprobe/`.

**Two of these break Linux builds too** (#1, #9), plus stdout pollution (#8).

## Findings

### Build system

**1. Every `ATLAS_*` path compiles to `""`.** `[all platforms]`
`atlas_path.h` generation moved from `config/Makefile.am` (automake, where
`$(atlas_spooldir)` et al. are defined) into `probe-busybox/Makefile`, where
they are not. Result:

```c
#define ATLAS_SPOOLDIR  ""      /* upstream: "/var/spool/ripe-atlas" */
#define ATLAS_RUNDIR    ""      /* upstream: "/var/run/ripe-atlas"   */
```

This is the cause of every `insecure file` error reported in the PR thread --
`rebased_validated_filename()` compares against an empty base, so all paths
fail validation. The validator itself is correct.
Fix: pass the eight path variables through `AM_MAKEFLAGS`.

**2. The FreeBSD script set is never loaded.** `bin/config.sh.in`
`get_sub_arch()` returns non-empty for freebsd, which trips the
`DEVICE_NAME="generic"` branch, so nothing in `bin/arch/freebsd/` is used.

**3. `freebsd-functions.sh` is never installed.** `bin/Makefile.am` line 10
hardcodes `arch/linux/linux-functions.sh`. Fixed with the existing
`PROBE_TYPE_FREEBSD` conditional.

### Shell layer

**4. `freebsd-functions.sh` is a 5-function stub** against
`linux-functions.sh`'s 31. Missing `epoch`, `config_lookup`,
`hash_ssh_pubkey`, `dfrm` and every applet wrapper. Rewritten to source the
common base and override only what differs: `hash_ssh_pubkey` (`sha256 -q`,
not `sha256sum`), `kill_perds` (`pgrep`, not `pidof`), `sos`
(`kern.boottime`, not `/proc/uptime`), `buddyinfo` (no-op).

**5. `get_ether_addr()` returns the interface name, not the MAC**, and never
exports `ETHER_ADDR` / `ETHER_SCANNED`, so every RESULT line is emitted with
an empty probe identifier. Its `findpid_ssh` / `kill_ssh` also match `sshd`,
but the probe runs an ssh *client*; upstream uses a pid file.

### C code

**6. `SIOCGIFCONF` iterated with a fixed stride.** `networking/rptaddrs.c:277`
uses `ifc_len/sizeof(ifreq[0])` with `ifreq[i]`. BSD returns variable-length
records (`sizeof(ifr_name) + max(sizeof(struct sockaddr), sa_len)`).
Reproducer: `ifconf_stride.c`.

```
ifc_len=640  sizeof(struct ifreq)=32  sizeof(struct sockaddr)=16

fixed stride (current code)          variable stride (correct)
  [0] re0        EADDRNOTAVAIL         [1] lo0    sa_len=28 reclen=44  ok
  [1] lo0        ok                    [4] ix0    sa_len=16 reclen=32  ok
  [2] <garbage>  ENXIO                 [12] aq0   sa_len=16 reclen=32  ok
  ... through [19]                     ... 17 valid records
```

AF_INET6 records are 44 bytes against a 32-byte stride, so entry 2 onward
reads a misaligned `ifr_name`. Second bug in the same loop: entries for
unconfigured interfaces are AF_UNSPEC, `SIOCGIFNETMASK` returns
`EADDRNOTAVAIL`, and the code treats that as fatal -- so it fails on the
*first* record regardless of stride.

The PR adds `getifaddrs()` replacements to this file but marks the caller
`static int __attribute__((unused)) get_portable_routing_info(FILE *of)`.
It is dead code; the Linux path is still live.

Fix: `getifaddrs()` for addresses (netmask comes from `ifa_netmask`, so the
second ioctl disappears), `sysctl(NET_RT_DUMP)` for routes replacing
`/proc/net/route`. Linux path preserved verbatim under `#else`.
Verified 13/13 routes matching `netstat -rn -f inet`, including the default
route, whose destination sockaddr has `sa_len == 0`.

**7. `/proc/net/route`, `/proc/net/if_inet6`, `/proc/net/ipv6_route`** are
still the live path in `rptaddrs.c` (lines 48-50, unchanged in the PR).
Covered by the same fix. Only the v4 half is implemented and tested.

**8. `src_addr` reported as `0.0.0.0`.** `connect()` on a raw socket does not
select a source address on FreeBSD, so `getsockname()` returns the
unspecified address. Reproducer: `srcaddr.c`.

```
raw ICMP + connect + getsockname -> 0.0.0.0
udp      + connect + getsockname -> 10.11.12.10
```

Fix: new `libbb/fixup_sockname.c` following the `bind_interface.c` pattern --
falls back to a throwaway connected UDP socket. Written portably with an
early return, so it is a no-op on Linux.

Side effect: this silenced traceroute's `ready_callback4: changed source` /
`weird destination` output without touching those printfs -- they fire on a
mismatch that no longer exists. Two unconditional debug printfs also removed
from `eperd/ping.c:440,442` (`loc_sin6:` / `loc_sa:`), which write to stdout
from inside a measurement applet on every platform.

**9. `dfrm` destroys measurement data.** `[all platforms]`
`coreutils/dfrm.c:81,84` compute `f_bfree * (f_bsize/1024)`. ZFS reports
`f_bsize` as 512, so `512/1024` is 0 and free space is always zero. The probe
concludes the disk is full and deletes everything in `data/new` and
`data/out` on every cycle.

```
before: "bfree": 807946240, "free": 0          + rm'd every result file
after:  "bfree": 807946288, "free": 403973144  + no deletions
```

Fix: multiply before dividing, 64-bit intermediate. Affects any filesystem
with a block size under 1024, not just FreeBSD.

## How far it gets with the patch applied

```
Generating public/private rsa key pair... probe_key saved
RESULT 9000 done ... STARTING ATLAS system initialized (reboot count 0)
RESULT 9006 done ... no reginit.vol start registration
ATLAS registration starting
REASON_FOR_REGISTRATION NEW NO previous state files
TOKEN_SPECS freebsd 1000 5110 freebsd/15.0/amd64
ssh -p 443 atlas@reg03.atlas.ripe.net INIT
  -> atlas@reg03.atlas.ripe.net: Permission denied (publickey).
```

Strict host-key verification against the shipped `known_hosts` passes. The
probe correctly self-reports `Issh-permission-denied...` via SOS and fails
over to reg04.

Measurements, against real targets, no `ATLAS_UNSAFE`:

```
evping       8.8.8.8            src_addr 10.11.12.10, rtt 2.43ms
evtraceroute 8.8.8.8            src_addr 10.11.12.10, hops resolve
evtdig       SOA freebsd.org.   rt 0.052ms, valid abuf
evhttpget    www.freebsd.org    res:301, src_addr 10.11.12.10
rptaddrs                        13/13 routes match netstat
```

## Not fixed

- `libbb/bind_interface.c` returns success on FreeBSD **without binding**
  ("not critical for basic functionality"). For a measurement tool this is
  worse than failing: the measurement runs from the wrong interface and is
  reported as if it had not. Should return -1 until implemented.
- `config/freebsd/ripe-atlas.rc.d` is generated but never installed.
- That rc.d script hardcodes `/etc/ripe-atlas`; a FreeBSD install uses
  `@atlas_sysconfdir@` (`/usr/local/etc/ripe-atlas`). README line 96 has the
  same wrong path for `probe_key.pub`.
- `rchoose` can select an IPv6 registration server on a v4-only host with no
  fallback inside a single attempt.
- `eperd/ping.c` and `eperd/traceroute.c` (the non-event variants) still fail
  to build on FreeBSD in upstream master; the PR fixes them.

## Environment

FreeBSD 16.0-CURRENT host, build and test in a 15.0-RELEASE jail userland,
clang 19.1.7, amd64. Host is IPv4-only for internet egress, so no IPv6 path
was exercised -- the v6 halves of #6 and #7 are unimplemented and untested.

