Skip to content

Husk v0.1.9 - Adblock parser hardening

Latest

Choose a tag to compare

@runhusk runhusk released this 30 Jun 21:28

Husk v0.1.9 - Adblock parser hardening

Patch release. The fix v0.1.8 advertised but only half-shipped.

Downloads

File Size SHA-256
Husk-Portable-0.1.9.zip 2.2 MB 1df3f08eb7bd7a7a41d60e08d9a6e17ea86c77b4e10337efb28e1061d8027423
husk.exe (standalone, for in-place update) 4.6 MB b320e45931a0aac22498ce875d8d1dfe0d896947e55d9a045fd5a3d5dd879a69

Verify before running

Get-FileHash .\Husk-Portable-0.1.9.zip -Algorithm SHA256

Match the output against the hash above. If it differs, do not run.

What's fixed

v0.1.8 fixed substring matching. The DEEPER bug was parsing.

v0.1.8 made the JS interceptor match by hostname instead of substring,
which solved the "searching the word facebook on DuckDuckGo dies"
case. But the URL bar still went silently dead under load, the
log added in v0.1.8 told us the Rust top-level navigator was blocking
duckduckgo.com outright. EasyList doesn't ship that rule. So how
did DDG end up in our blocked-domain set?

The culprit was the EasyList line parser, written months ago for the
clean form ||example.com^ and never revisited. EasyList rules in
the wild look like this:

||duckduckgo.com^$image,domain=somesite.com

Which means: block requests to duckduckgo.com only when the page
they're loaded from is somesite.com
. The $domain=… is a scope
modifier. The naive parser walked the line character-by-character
until it hit ^, kept whatever was before it, and added that to the
global blocked-domain set. So any host that appeared anywhere in
a scoped EasyList rule became a global block. DDG was one of hundreds.

After the v0.1.8 EasyList fetch, the set contained 239 short / no-dot
entries that had been chopped at the wrong delimiter, collector-,
faro-collector-prod-, util-, et al. Those are harmless (no real
URL matches them) but they were the smoking gun: the parser was
eating rules it had no business eating.

The fix

Three layers in blocker.rs::parse_into:

  1. Only the canonical ||host^ form is treated as a global block.
    Anything ending in ^$modifier,… is skipped, they're scoped to a
    specific page or request type and need a real rule engine to honour
    correctly, which we don't ship yet.
  2. The parsed host must contain at least one dot. Stops single-label
    entries like com from ever being inserted, even if a source list
    accidentally ships ||com^.
  3. The host-suffix walk in should_block_top_level was already
    correct (duckduckgo.com → com, exact-string match at each level),
    so once the bad entries stop being inserted the walk does the right
    thing automatically.

After the fix the same EasyList drop yields 6 suspicious entries
(real short tracker domains like ad.gt and hh.ru), down from 239
parser artefacts.

Upgrade path

  • From v0.1.8: in-app Settings → About → Check for updates and
    click through, the v0.1.7 updater hardening landed clean.
  • From v0.1.7 or earlier: same in-app flow, or grab the standalone
    husk.exe (above) and drop it next to your existing HuskData/.

What's unchanged from v0.1.8

The substring-to-hostname matcher in the JS interceptor stays. The
per-page PAGE_EXEMPTIONS table (YouTube, Twitch, youtu.be) stays.
The should_block_top_level diagnostic log stays, keyed off
husk-dns.log via the existing privacy-redacting writer, so the next
"URL bar does nothing" report tells us which rule (label level) was
hit without leaking actual hostnames to disk.

Known limitations carried over

  • macOS + Linux still v0.2 targets.
  • YouTube ads pass through (cosmetic filtering on the roadmap).
  • Scoped EasyList rules (||host^$domain=…) are dropped entirely
    rather than applied correctly. A full rule engine is what comes
    next once the host-only path is solid.

If Husk is useful to you, support keeps it free, ad-free and
account-free: husk.run/donate.