I was expanding pfBlockerNG-IP coverage on my pfSense box tonight and hit a nasty little bug that eats your entire feed update, not just the IPv6 part. Writing it up because it took a few passes to diagnose and other pfSense users are going to hit exactly the same wall.
Setup: pfSense 2.9 (release build), pfSense-pkg-pfBlockerNG-devel-3.2.17_1 installed via pkg. All-IPv4 pfBlockerNG-IP config was working fine (PRI1 alias, six curated feeds, ~16k entries). I wanted to add an IPv6 alias fed by Spamhaus DROP v6 so my WAN would block inbound from known-bad v6 space too. This is a stock, obvious thing to do.
The crash
Created a new v6 alias PRI1_v6, action Deny_Both, one row: https://www.spamhaus.org/drop/dropv6.txt. Ran a force IP update:
/usr/local/bin/php /usr/local/www/pfblockerng/pfblockerng.php updateip
The IPv4 process completed, feeds downloaded, [ Pass ] on every one. The IPv6 process started, the Spamhaus feed downloaded 200 OK, then this landed in the log and killed the entire process:
PHP ERROR: Type: 1, File: /usr/local/share/pear/Net/IPv6.php, Line: 684,
Message: Uncaught ValueError: str_repeat(): Argument #2 ($times) must be
greater than or equal to 0 in /usr/local/share/pear/Net/IPv6.php:684
Stack trace:
#0 /usr/local/share/pear/Net/IPv6.php(684): str_repeat()
#1 /usr/local/share/pear/Net/IPv6.php(1157): Net_IPv6::uncompress()
#2 /usr/local/share/pear/Net/IPv6.php(499): Net_IPv6::_ip2Bin()
#3 /etc/inc/util.inc(1522): Net_IPv6::getAddressType()
#4 /etc/inc/util.inc(1446): is_linklocal()
#5 /usr/local/pkg/pfblockerng/pfblockerng.inc(3679): is_ipaddrv6()
#6 /usr/local/pkg/pfblockerng/pfblockerng.inc(9634): validate_ipv6()
#7 /usr/local/www/pfblockerng/pfblockerng.php(186): sync_package_pfblockerng()
Same result with dropv6.json in place of dropv6.txt, so it is not a feed-format issue.
Why this matters more than “an IPv6 update failed”
The nasty part is what happens to the rest of the run. sync_package_pfblockerng() processes IPv4 first, then IPv6, then commits new pf tables at the end. When it hits the fatal ValueError during the IPv6 phase, the whole process aborts before any pf tables are updated. So:
- Your new IPv4 feeds show
[ Pass ]in the log, which looks like success. - The pf tables never actually get the new entries.
- Because your IPv4 tables never get updated, you might not notice the failure at all until you check
pfctl -t pfB_PRI1_v4 -T show | wc -land see the same count as before.
That is what got me: I added Blocklist.de and URLhaus to my IPv4 alias, hit updateip, saw Pass on both, saw the download logs, and my v4 table count did not change. Removing the v6 alias made both new v4 feeds land normally on the very next run.
Root cause
The traceback tells the whole story once you know where to look. Net_IPv6 is part of PEAR, the old-school PHP library repository from the pre-Composer era. Net_IPv6::uncompress() at line 684 in the version pfSense ships is calling str_repeat() with a negative count. Something in the IPv6 feed produced a string that uncompress() cannot make sense of, and the code that computes the pad-out width returns a negative number.
Before PHP 8, str_repeat($string, -1) silently returned an empty string and processing carried on. In PHP 8, the same call raises an Uncaught ValueError. pfSense 2.9 runs modern PHP. PEAR’s Net_IPv6 predates that stricter semantics by many years. The library never got the guard it now needs, because PEAR itself has been effectively EOL for a long time and nobody is patching it.
The bug chain is:
validate_ipv6()inpfblockerng.incgets called on some row from the feed.- It calls the pfSense-internal
is_ipaddrv6()inutil.inc. is_ipaddrv6()callsis_linklocal()to filter link-local addresses.is_linklocal()usesNet_IPv6::getAddressType().getAddressType()normalizes the string via_ip2Bin()._ip2Bin()callsuncompress().uncompress()computes a padding width that goes negative for whatever input this row contained.str_repeat()explodes.
None of those layers has a try block around the PEAR call, so the exception propagates all the way up and out of the CLI process.
The workaround
Until either PEAR Net_IPv6 gets a str_repeat guard or pfBlockerNG sanitizes inputs before validating them, you have three practical options:
- Do not use IPv6 aliases in pfBlockerNG-IP. This is what I ended up doing after the diagnosis. IPv4 coverage stays intact.
- If you must have some v6 blocking, avoid feeds that expose the poison rows. I could not find a “safe” Spamhaus alternative quickly; both their JSON and plaintext v6 exports triggered it in my tests. YMMV with smaller v6 feed sources.
- Patch Net_IPv6 locally. A one-line guard in
uncompress()that clamps the pad-out width to zero would prevent the crash. This is a per-box change that will get overwritten by any PEAR package upgrade, so it is not really a fix, just a survival tactic.
Reported
Filed on the Netgate community forum: IPv6 alias update crashes with str_repeat() ValueError in PEAR Net_IPv6::uncompress. If you hit the same crash, adding your evidence there gives the maintainer more to work with.
The broader lesson
PEAR is effectively unmaintained. A lot of pfSense (and by extension pfBlockerNG) still leans on it for utility code that predates Composer. Every PHP release since 8.0 has quietly tightened argument validation on standard-library functions that used to silently coerce their way through weird inputs. Any old dependency that did not follow along will surface exactly this shape of bug: a “worked yesterday” call that now throws.
If you maintain something that inherits Net_IPv6, or any other PEAR package, this is a good time to audit for str_repeat, substr, str_pad, array_fill, preg_match_all count arguments, and anywhere else PHP 8 hardened its numeric-argument checks. Wrap suspect calls in try or add explicit max(0, ...) guards. This bug class is not done biting people.
Contact
- Kevin Kessler
- kevin@glyph.sh
- https://glyph.sh