. "$BST_LIB" # macchanger 1.7.0 (meta-networking, recipes-support) — GNU MAC Changer, # installed as /usr/bin/macchanger. It views and manipulates the MAC address of # a network interface through THREE ioctls on an AF_INET SOCK_DGRAM socket: # SIOCGIFHWADDR reads the current address, SIOCETHTOOL/ETHTOOL_GPERMADDR reads # the permanent burned-in one, and SIOCSIFHWADDR writes. Alongside that it ships # an 18 897-entry IEEE OUI database plus a 39-entry wireless list, and resolves # every address it prints against them. # # THIS SUITE REACHES FURTHER THAN THE OTHER DEVICE-GATED PORTS ON THIS IMAGE, # and the reason is worth stating. 63-tunctl can only prove its ioctl was # ISSUED, because CONFIG_TUN is not set and TUNSETIFF can never succeed here. # macchanger has no such gate: lo is a real, registered netdev — the only one on # this machine — the runner is root, and Linux loopback accepts SIOCSIFHWADDR. # So the write half is not theory here; it is asserted against the kernel's own # view of the interface afterwards. # # The suite is ordered by what each section needs, so a failure localises: # A packaging — no syscalls beyond exec # B usage — option parser only; no socket is opened # C vendor database— fopen(3) only; -l exits BEFORE any socket # D device read — socket() + SIOCGIFHWADDR + SIOCETHTOOL # E MAC parser — reached only AFTER the read ioctls (upstream orders it so) # F device write — SIOCSIFHWADDR, runtime-probed, and LAST because it is # IRREVERSIBLE (see the note above section F) # # ONE PLATFORM GAP SHOWS UP HERE AND IS RECORDED RATHER THAN WORKED AROUND: # /dev/hwrng's node exists but has no backing driver, so open(2) succeeds while # read(2) fails ENODEV. macchanger seeds its PRNG from /dev/hwrng first and does # not check read(2), so its seed is uninitialised stack memory and every -e/-r # yields the SAME address within a boot. The two variability cases are gated on # a runtime probe for that and turn back into real assertions the day the node # gets a driver; the deterministic parts of the randomiser (the BIA and unicast # bits) are asserted regardless. require macchanger macchanger/present tmp=${TMPDIR:-/tmp}/bst-macchanger.$$ out=$tmp.out; bare=$tmp.bare; filt=$tmp.filt; kw=$tmp.kw DATA=/usr/share/macchanger # lo's hardware address as the KERNEL reports it — the independent view every # differential below is scored against. rtnetlink FIRST, deliberately: macchanger # reads through an ioctl, so scoring it against `ip` compares two DIFFERENT kernel # interfaces rather than two spellings of one, which is the same distinction the # image's net-tools entry draws. sysfs is the fallback for an image without # iproute2. (rtnetlink is also the only one of the three that cannot go stale: it # always answers for the caller's own network namespace, whereas a /sys inherited # across an unshare(CLONE_NEWNET) still shows the namespace it was mounted in — # which is exactly the false red this suite hit on its own control run.) kmac() { _m=$(ip -o link show lo 2>/dev/null | sed -n 's|.*link/[a-z]* \([0-9a-f][0-9a-f:]*\).*|\1|p') if [ -n "$_m" ]; then printf '%s\n' "$_m"; return 0; fi [ -r /sys/class/net/lo/address ] && cat /sys/class/net/lo/address } # ------------------------------------------------------------- A packaging --- # macchanger installs to bindir. Asserting where it RESOLVES catches a packaging # change that leaves it off the PATH the runner actually has, and there is no # busybox or toybox applet of this name, so a bare `macchanger` is this port. check_eq macchanger/resolves-to-bin "/usr/bin/macchanger" "$(command -v macchanger)" # The vendor database is not documentation — mc_maclist_init() reads BOTH files # at startup and returns -1 if either is missing, which exits the program before # any device is touched. A packaging change that dropped them would break every # invocation, including the ones that never open a socket. check macchanger/data-oui-present test -s "$DATA/OUI.list" check macchanger/data-wireless-present test -s "$DATA/wireless.list" # --------------------------------------------------------------- B usage ------ # -V and -h disagree about the program's own name — "GNU MAC changer" from -V, # "GNU MAC Changer" from -h. Both spellings are pinned deliberately: they come # from two different string literals in main.c, so a suite that normalised them # would stop noticing if one changed. check_rc macchanger/version-rc 0 sh -c "macchanger -V > '$out' 2>&1" check_out macchanger/version-string "GNU MAC changer 1.7.0" cat "$out" check_out macchanger/version-author "Alvaro Lopez Ortega" cat "$out" check_rc macchanger/version-long-rc 0 sh -c "macchanger --version >/dev/null 2>&1" check_rc macchanger/help-rc 0 sh -c "macchanger -h > '$out' 2>&1" check_out macchanger/help-title "GNU MAC Changer" cat "$out" check_out macchanger/help-usage "Usage: macchanger [options] device" cat "$out" check_out macchanger/help-mac "--mac=XX:XX:XX:XX:XX:XX" cat "$out" check_out macchanger/help-list "--list[=keyword]" cat "$out" check_out macchanger/help-bia "Pretend to be a burned-in-address" cat "$out" # Everything goes to STDOUT here — the exact inverse of tunctl, whose help is an # error path on stderr. A suite written to that sibling's convention would be red. check_eq macchanger/help-stderr-empty "" "$(macchanger -h 2>&1 >/dev/null)" # No device argument: print_usage() then exit(EXIT_OK). Note rc 0 — this is NOT # an error path, which is again the opposite of tunctl. check_rc macchanger/usage-rc 0 sh -c "macchanger > '$out' 2>&1" check_out macchanger/usage-hint "Try \`macchanger --help' for more options." cat "$out" # getopt rejects the option, macchanger prints its help and STILL exits 0. That # is upstream's behaviour (the '?' case falls into print_help + exit(EXIT_OK)), # and it is asserted rather than corrected so the port's CLI is described # truthfully. check_rc macchanger/badopt-rc 0 sh -c "macchanger -Z > '$out' 2>&1" check_out macchanger/badopt-msg "invalid option" cat "$out" check_out macchanger/badopt-help "GNU MAC Changer" cat "$out" # ----------------------------------------------------- C vendor database ------ # -l exits before the device argument is even read, so this whole section runs # with NO socket and NO ioctl. On a machine whose networking is whatever # HardwareJS gives it, that makes these the cases that cannot be blamed on the # net stack when something else goes wrong. check_rc macchanger/list-rc 0 sh -c "macchanger -l > '$bare' 2>&1" check_out macchanger/list-misc-header "Misc MACs:" cat "$bare" check_out macchanger/list-wireless-header "Wireless MACs:" cat "$bare" check_out macchanger/list-column-header "Num MAC Vendor" cat "$bare" # The first row, verbatim: "%04i - %02x:%02x:%02x - %s". Pins the format string # and the fact that entry 0 came out of OUI.list in file order. check_out macchanger/list-first-row "0000 - 00:00:00 - XEROX CORPORATION" cat "$bare" # Bounded from BELOW, not pinned: the database grows upstream, and an exact # count would go false on the next version bump for no defect at all. check macchanger/list-row-count sh -c "test \"\$(wc -l < '$bare')\" -ge 18000" # The wireless list is a SEPARATE file and its entries are numbered from 0 again. check_out macchanger/list-wireless-row "0000 - 00:00:8f - Raytheon Raylink/WebGear Aviator2.4" cat "$bare" # The keyword filter is a substring match over the vendor NAME. Asserted as a # differential — filtered strictly shorter than unfiltered — because "contains # XEROX" alone would pass on the unfiltered list too. macchanger --list=XEROX > "$filt" 2>&1 check_out macchanger/list-filter-hit "XEROX CORPORATION" cat "$filt" check macchanger/list-filter-narrows \ sh -c "test \"\$(wc -l < '$filt')\" -lt \"\$(wc -l < '$bare')\"" # Case-sensitive strstr(3): the lowercase spelling matches nothing, and that is # the differential proving a real comparison rather than a match-everything. macchanger --list=xerox > "$out" 2>&1 check macchanger/list-filter-case-sensitive \ sh -c "test \"\$(wc -l < '$out')\" -lt \"\$(wc -l < '$filt')\"" # A keyword matching nothing still prints both section headers and exits 0. check_rc macchanger/list-filter-empty-rc 0 sh -c "macchanger --list=ZZZNOPE > '$out' 2>&1" check_out macchanger/list-filter-empty-header "Wireless MACs:" cat "$out" check macchanger/list-filter-empty-norows \ sh -c "! grep -q ' - ..:..:.. - ' '$out'" # UPSTREAM CLI WART, PINNED SO A FIX IS NOTICED. --list takes an optional # argument, but the SHORT optstring is "VasAbrephlm:" — l carries no colon — so # `-l KEYWORD` silently ignores the keyword and prints the whole database, and # `-lKEYWORD` dies with "invalid option -- 'K'". Only `--list=KEYWORD` filters. # Asserted as an equality against the unfiltered run: the day upstream fixes # this, this case goes red and says so, rather than the filter quietly starting # to work while the suite claims nothing. macchanger -l XEROX > "$kw" 2>&1 check macchanger/list-short-keyword-ignored sh -c "cmp -s '$kw' '$bare'" # ------------------------------------------------------- D device read -------- # The first thing that opens a socket. A nonexistent interface is the cleanest # probe of the boundary: socket(AF_INET, SOCK_DGRAM, 0) must SUCCEED and only # then can SIOCGIFHWADDR fail with ENODEV. check_rc macchanger/nodev-rc 1 sh -c "macchanger -s bst-no-such-dev0 > '$out' 2>&1" check_out macchanger/nodev-msg "[ERROR] Set device name: No such device" cat "$out" # THE DIFFERENTIAL THAT MAKES THE CASE ABOVE MEAN SOMETHING. If AF_INET dgram # sockets were unavailable on this target, macchanger would have failed one step # earlier with "[ERROR] Socket" and never reached the ioctl at all. Asserting # the ABSENCE of that message is what separates "the ioctl ran and said ENODEV" # from "we never got a socket". check macchanger/nodev-not-socket-failure sh -c "! grep -q '\[ERROR\] Socket' '$out'" # lo is the only interface on this machine — the kernel brings it up and # auto-assigns 127.0.0.1/8, and no virtio-net device is registered (the same # fact 60-net-tools records). So it is lo or nothing for every case below. if [ -e /sys/class/net/lo ] || ip link show lo >/dev/null 2>&1; then check_rc macchanger/show-rc 0 sh -c "macchanger -s lo > '$out' 2>&1" check_out macchanger/show-current "Current MAC:" cat "$out" check_out macchanger/show-permanent "Permanent MAC:" cat "$out" # A bare invocation with no mode flag falls through to the same read-only # path and exits 0 ("default to show"). check_rc macchanger/bare-device-rc 0 sh -c "macchanger lo >/dev/null 2>&1" # THE READ IS SCORED AGAINST THE KERNEL, not against a constant. If # SIOCGIFHWADDR returned garbage — or macchanger printed a compiled-in # default — this is the case that catches it. mc_cur=$(macchanger -s lo 2>/dev/null | sed -n 's/^Current MAC: *\([0-9a-f:]*\).*/\1/p') check_eq macchanger/read-agrees-with-kernel "$(kmac)" "$mc_cur" # The vendor annotation is applied to the address just read, so it proves # the database and the ioctl result meet. lo's address is all zeros, whose # OUI 00:00:00 is the first row of OUI.list asserted above. check_out macchanger/read-vendor-annotated "(XEROX CORPORATION)" \ sh -c "macchanger -s lo 2>&1" # -b WITHOUT -r is refused with a warning and still exits 0 down the # read-only path. main() makes this check itself (`set_bia && !random`); # getopt has no opinion about it, so it is macchanger's own logic under test. # Read-only, so it sits here rather than behind the write probe below. check_rc macchanger/bia-without-random-rc 0 sh -c "macchanger -b -s lo > '$out' 2>&1" check_out macchanger/bia-without-random-msg \ "[WARNING] Ignoring --bia option that can only be used with --random" cat "$out" # ------------------------------------------------- E the MAC parser ------- # Upstream parses -m's argument only AFTER the two read ioctls have run and # their results printed, so these cases need a working device — which is why # they sit here and not up with the option parser. Two distinct rejections: # a length check, and a separator check at the right length. check_rc macchanger/mac-short-rc 1 sh -c "macchanger -m 00:11:22 lo > '$out' 2>&1" check_out macchanger/mac-short-msg "MAC length should be 17" cat "$out" check_rc macchanger/mac-sep-rc 1 sh -c "macchanger -m 00-11-22-33-44-55 lo > '$out' 2>&1" check_out macchanger/mac-sep-msg "[ERROR] Incorrect format: 00-11-22-33-44-55" cat "$out" check macchanger/mac-sep-not-length \ sh -c "! grep -q 'length should be 17' '$out'" # NO HEX VALIDATION: "zz:zz:zz:zz:zz:zz" is 17 chars with colons in the right # places, so strtoul(3) turns it into 00:00:00:00:00:00 and the value reaches # the ioctl. Pinned because it is the difference between "rejected by the # parser" and "rejected by the kernel", and only the second is true here. macchanger -m zz:zz:zz:zz:zz:zz lo > "$out" 2>&1 check macchanger/mac-nonhex-passes-parser \ sh -c "! grep -q 'Incorrect format' '$out'" # ------------------------------------------------ F device write ---------- # IRREVERSIBLE, AND DELIBERATELY SO. Linux validates a new hardware address # with eth_validate_addr(), which rejects the all-zero address — so once lo's # MAC has been changed it CANNOT be put back to the 00:00:00:00:00:00 it # booted with, by macchanger -p or by anything else. Rather than pretend # otherwise this suite (a) runs last, (b) leaves lo at a fixed, documented, # locally-administered sentinel instead of a random address, and (c) asserts # the failed restore, which is itself a real statement about the kernel. # No suite on this image reads lo's hardware address, and loopback does not # use L2 addressing, so the residue is inert. SENTINEL=02:00:00:00:00:01 if macchanger -m "$SENTINEL" lo >/dev/null 2>&1; then check_eq macchanger/write-kernel-agrees "$SENTINEL" "$(kmac)" check_out macchanger/write-reports-new "New MAC: $SENTINEL" \ sh -c "macchanger -m $SENTINEL lo 2>&1" # -e is documented as "Don't change the vendor bytes", and that is not # quite what it does. main() calls mc_mac_random(mac, 3, set_bia=1), and # the set_bia arm ends in `mac->byte[0] &= ~2` — so octets 2 and 3 are # held EXACTLY while octet 1 keeps every bit except the # locally-administered one, which is forced OFF. Split into three cases # because they have three different causes, and found only because the # sentinel above is a locally-administered address (02:…): a sentinel # with that bit already clear would have hidden it entirely. before=$(kmac) macchanger -e lo >/dev/null 2>&1 after=$(kmac) b0=$(echo "$before" | cut -d: -f1); a0=$(echo "$after" | cut -d: -f1) check_eq macchanger/ending-keeps-octets-2-3 \ "$(echo "$before" | cut -d: -f2-3)" "$(echo "$after" | cut -d: -f2-3)" check macchanger/ending-clears-la-bit sh -c "test \$(( 0x$a0 & 2 )) -eq 0" check_eq macchanger/ending-keeps-other-octet-1-bits \ "$(( 0x$b0 & 0xfd ))" "$(( 0x$a0 & 0xfd ))" # THE PLATFORM FACT THAT DECIDES THE TWO CASES BELOW, asserted from the # TARGET side rather than read off a config. /dev/hwrng exists here as a # character node (10, 183) but has NO BACKING DRIVER: open(2) SUCCEEDS # and read(2) fails ENODEV. That combination is the harmful one — # the usual probe idiom is open(), so a caller that probes correctly is # still misled. macchanger is exactly such a caller: random_seed() tries # /dev/hwrng FIRST, its `||` chain short-circuits on the successful open, # and it does not check read(2)'s return value — so `seed` keeps whatever # was on the stack and srandom() gets the same value on every fresh # process. /dev/random and /dev/urandom both work fine here; nothing is # wrong with srandom()/random() themselves. # Established by A/B: bind-mounting /dev/urandom over /dev/hwrng makes # macchanger's output vary immediately, and restoring it makes it # constant again. hwrng_bytes=$(dd if=/dev/hwrng bs=4 count=1 2>/dev/null | wc -c) if [ -e /dev/hwrng ] && [ "$hwrng_bytes" -ne 4 ]; then bst_ok macchanger/hwrng-node-without-driver elif [ ! -e /dev/hwrng ]; then bst_skip macchanger/hwrng-node-without-driver "no /dev/hwrng node at all on this machine" else # The node started delivering bytes — the platform gap closed. bst_skip macchanger/hwrng-node-without-driver "/dev/hwrng now returns bytes; the ENODEV gap this suite recorded has closed" fi if [ "$hwrng_bytes" -eq 4 ]; then # Retried rather than asserted once: the three low octets are random, # so a single run can legitimately repeat itself (1 in 255^3). Four # tries makes a false red impossible without weakening the assertion. changed=0; prev=$(kmac) for _ in 1 2 3 4; do macchanger -e lo >/dev/null 2>&1 if [ "$(kmac)" != "$prev" ]; then changed=1; break; fi done check macchanger/ending-changes-suffix sh -c "test $changed -eq 1" # -r randomises all six octets, so the vendor prefix moves too. prev=$(kmac); changed=0 for _ in 1 2 3 4; do macchanger -r lo >/dev/null 2>&1 if [ "$(kmac)" != "$prev" ]; then changed=1; break; fi done check macchanger/random-changes-mac sh -c "test $changed -eq 1" else # Gated on the RUNTIME probe above, not on a hardcoded platform # assumption, so both turn into real assertions by themselves the day # /dev/hwrng gets a driver. Everything else about the randomiser is # still asserted below — the BIA and unicast bits are deterministic # and do not depend on the seed varying. for id in ending-changes-suffix random-changes-mac; do bst_skip "macchanger/$id" "macchanger seeds from /dev/hwrng, whose node exists but read()s ENODEV; upstream ignores read(2)'s return so the seed is constant per boot and every -e/-r yields the same address" done fi # THE BIA BIT IS THE ONE PART OF A RANDOM ADDRESS THAT IS NOT RANDOM, so # it is assertable exactly. Without -b, mc_mac_random ends in # `byte[0] |= 2` and the result is always LOCALLY ADMINISTERED; with -b it # ends in `&= ~2` and always claims to be burned-in. Two runs, opposite # and deterministic — this is what proves the -b flag is wired at all, # which no amount of "the address changed" ever could. macchanger -r lo >/dev/null 2>&1 r0=$(kmac | cut -d: -f1) check macchanger/random-is-locally-administered sh -c "test \$(( 0x$r0 & 2 )) -eq 2" macchanger -r -b lo >/dev/null 2>&1 rb0=$(kmac | cut -d: -f1) check macchanger/random-bia-clears-la-bit sh -c "test \$(( 0x$rb0 & 2 )) -eq 0" # The unicast bit is cleared for both (`& 0xFC`), so neither can produce a # multicast address — which the kernel would refuse outright. check macchanger/random-is-unicast sh -c "test \$(( 0x$rb0 & 1 )) -eq 0" # -p asks for the PERMANENT address back. On lo that is all zeros, which # eth_validate_addr() refuses — so this reports the kernel's refusal, and # the suite asserts it rather than treating it as a failure of the port. check_rc macchanger/permanent-rejected-rc 1 \ sh -c "macchanger -p lo > '$out' 2>&1" check_out macchanger/permanent-rejected-msg "Cannot assign requested address" cat "$out" # Leave a deterministic end state. macchanger -m "$SENTINEL" lo >/dev/null 2>&1 check_eq macchanger/write-final-sentinel "$SENTINEL" "$(kmac)" else why=$(macchanger -m "$SENTINEL" lo 2>&1 | sed -n 's/.*permissions: //p' | head -1) [ -n "$why" ] || why="SIOCSIFHWADDR refused on lo" for id in write-kernel-agrees write-reports-new \ hwrng-node-without-driver \ ending-keeps-octets-2-3 ending-clears-la-bit \ ending-keeps-other-octet-1-bits ending-changes-suffix \ random-changes-mac random-is-locally-administered \ random-bia-clears-la-bit random-is-unicast \ permanent-rejected-rc permanent-rejected-msg write-final-sentinel; do bst_skip "macchanger/$id" "SIOCSIFHWADDR on lo: $why" done fi else for id in show-rc show-current show-permanent bare-device-rc \ read-agrees-with-kernel read-vendor-annotated \ bia-without-random-rc bia-without-random-msg \ mac-short-rc mac-short-msg mac-sep-rc mac-sep-msg mac-sep-not-length \ mac-nonhex-passes-parser write-kernel-agrees write-reports-new \ hwrng-node-without-driver \ ending-keeps-octets-2-3 ending-clears-la-bit \ ending-keeps-other-octet-1-bits ending-changes-suffix \ random-changes-mac random-is-locally-administered \ random-bia-clears-la-bit random-is-unicast \ permanent-rejected-rc permanent-rejected-msg write-final-sentinel; do bst_skip "macchanger/$id" "no lo interface on this machine — every device case needs one" done fi rm -f "$tmp".* 2>/dev/null