DEV Community

Vladimir Garenitsa
Vladimir Garenitsa

Posted on

Android's external/ libraries: hunting n-days in code that upstream already fixed

"Android's external/ libraries: hunting n-days in code that upstream already fixed"

(Alternative: "I built a fuzzing platform on a 2-core VPS. Here's what actually works for solo vuln research")

Draft text

By Vladimir Garenitsa / MalwareDante

Everyone hunting memory-corruption bugs in open-source media libraries
competes with Google's OSS-Fuzz — a cluster running the same fuzzers on the
same code, 24/7. On a 2-core VPS you lose that race by definition.

So I stopped racing and changed the question: not "how do I fuzz more", but
"what does nobody fuzz?"

Three surfaces that aren't crowded

1. Vendor forks, not upstream.
AOSP ships vendored copies of parsing libraries under external/ — and they
lag upstream, sometimes catastrophically:

  • external/libexif is 0.6.21 — last upgraded June 2015. Eleven years of upstream fixes never merged (though Google cherry-picks CVE fixes — the hot paths are patched; the rest is a gray zone).
  • external/flac is 1.4.3 vs upstream 1.5.0, which includes real parser-hardening: residual-in-32-bit-overflow checks, metadata over-read/allocation guards. Those checks do not exist in shipped code.

If you fuzz the shipped version instead of upstream, you hunt in
pre-mapped territory — and a hit is an n-day on every Android device.

2. The write path.
Nearly every media-fuzzer feeds files into decoders. Almost nobody fuzzes
encode→serialize→reparse loops — yet that path touches different
machinery (box writers, CRC builders, metadata serialization). I built a
round-trip harness (decode HEIF → re-encode via x265 → write → reparse →
decode). On a mature decoder, the write path is the least-exercised code in
the tree.

3. Sanitizer classes people skip.
Everyone runs ASan+UBSan. Fewer run MemorySanitizer — it requires
building the entire dependency tree under -fsanitize=memory, which is
tedious, so coverage stays thin. UMR-class bugs hide precisely there.

The platform

  • 12 libFuzzer targets + AFL++ CmpLog + guided MSan, in systemd-run scopes with CPU quotas (a 2-core VPS shares it fine)
  • Daily git pull → incremental rebuild → relaunch via systemd timer — "bleeding edge" commits get fuzzed hours after landing
  • Automated triage: stack-signature dedup, append-only crash index, 1-minute telemetry, watchdog restarts. I check in twice a day; the fleet runs itself.

Honest results (as of writing)

778 triaged artifacts, 97 signatures — all UB-class, zero memory corruption
so far. One real unfixed bug reported upstream:
libde265#546 —
attacker-controlled invalid enum load in SEI decoded_picture_hash, live in
release 1.1.2 and git master.

The honest pitch: solo fuzzing won't out-muscle OSS-Fuzz, but it can
out-position it. The edge isn't compute — it's surface selection.

Source, harnesses and methodology:
github.com/malwaredante/fuzzing-infra

Vladimir Garenitsa / MalwareDante — independent vulnerability research.

Top comments (2)

Collapse
 
magistral profile image
magistral •

The most interesting part here is the shift from raw fuzzing throughput to target selection. A smaller setup can still be valuable if it focuses on vendor forks and code paths that are less exercised upstream. I'm curious how you prioritize which outdated libraries are worth targeting first, especially when some fixes are already selectively backported.

Collapse
 
malwaredante profile image
Vladimir Garenitsa • • Edited

Great question — prioritization is where most fuzzing efforts quietly die, so we formalized it into a pipeline rather than trusting intuition. Our selection criteria, roughly in order of weight:

Before spending a single CPU-hour, we audit what's already fuzzed at scale. Anything continuously covered by industrial infrastructure (e.g. OSS-Fuzz targets) is deprioritized on principle — incremental exec/s against a datacenter is wasted work. The interesting territory is the gaps: components everyone assumes are fuzzed but actually aren't.

"Selectively backported" is exactly the right phrase — we treat backport patterns as an oracle. We diff release branches across versions per library and score the delta. Three regimes emerge: actively patched (noise, bugs get found upstream first), occasionally patched (interesting — partial coverage implies blind spots), and fully frozen (the sweetest target: zero noise, and any crash is almost certainly unfixed anywhere).

Patched code doesn't just mark healed bugs — it marks where bugs live. We weight regions adjacent to historical fixes higher, on the well-established principle that vulnerabilities cluster. Backport commits are effectively free triage. We score targets by whether naive mutation even reaches the interesting code. Parsers with length fields, checksums, or magic-byte validation reject most random input at the door — which means standard coverage-guided tools under-explore them, which in turn means lower prior coverage and higher expected bug density. Where a gate exists, we close it ourselves rather than accepting the shallow frontier.

Single-shot file fuzzing misses an entire bug class — anything requiring state carried across inputs. We prioritize formats where the same parser object must survive reuse, re-entry, or mid-stream format switches. In short: we're not fuzzing harder, we're fuzzing where nobody looks. A small fleet aimed at the intersection of frozen code + unparsed complexity + zero backport noise will out-produce a large fleet aimed at well-trodden targets. Which specific libraries meet all five criteria — I'll keep that to the write-up. 🙂