DOC GK-009 TYPE ENGINEERING LOG SUBJECT LADING STATUS ARCHIVED

It failed
three times.
The third found
the other two.

Regulators now ask software makers to say which known flaws actually reach their product. I built a tool to answer that from the shipped binary, wrote two stop conditions into the README before writing any code, and both resolved against the approach. The project is archived. The paper, the dataset, and two upstream bug reports are the output. Paper: 10.5281/zenodo.22099794. Dataset: 10.5281/zenodo.22093232. Code: Lading.

ONE FIRMWARE IMAGE, ONE SCANNER RUN hundreds of CVEs somebody must justify every row unreachable · clock does not start reachable · mandatory reporting clock UNDER THE EU CYBER RESILIENCE ACT THIS STOPPED BEING PAPERWORK AND BECAME A DEADLINE.
FIG 09.1 · The economics are why anyone would automate this. Hundreds of findings per release, each needing a written justification.

SEC. 01

The problem in plain language

Modern software is assembled from thousands of pieces written by other people. Somebody publishes a list of known security flaws in those pieces. A scanner compares your product against that list and hands you a few hundred results.

Most of those results do not matter to you. The flawed feature was switched off at build time, or the code was never included, or your product does not use that part at all. But somebody still has to write down why each one does not matter, because regulators and customers now ask.

So the obvious idea: automate the check. Open the file that actually ships, look for the broken function, and answer yes or no.

That is the idea this repository tested. It does not work, and the interesting part is exactly where it stops working.

WRITTEN AT THE FIRST COMMIT. NEITHER EVER MOVED. KT-1 can it decide at scale? bar: 30% of findings resolve to a decidable clearance NOT EVALUABLE KT-2 are its decisions sound? bar: zero false clearances. not a rate. one. FAIL A FALSE REFUSAL COSTS A REVIEWER TIME. A FALSE CLEARANCE REMOVES A REAL VULNERABILITY FROM SOMEBODY'S RADAR.
FIG 09.2 · The asymmetry is deliberate. KT-2's bar was one, because there is no partial credit on a claim filed with a regulator.

SEC. 02

Two bars, set before any code existed

LADING was refusal-first. It emitted a clearance only when it could point at specific bytes on disk and re-derive the reasoning offline. Anything short of that produced "under investigation". No scoring, no heuristic, no model in the decision path.

Of the five justifications the VEX standard permits, three require proving a negative about program behaviour. LADING refuses to emit those at all, enforced by a CI check that greps the source for them. The refusal is the positioning, not a limitation: every competitor sells proprietary exploitability probabilities, and this sells the verifiable subset and openly declines the rest.

Two thresholds went into the README at the first commit. Neither moved across three instrument passes.

WHERE 15,385 FINDINGS TERMINATED S1 identity 91.93% S2 manifest 7.67% S3 symbols 0.00% the zero is the finding S5 stripped 0.00% S7 decided 0.03% 83% OF CORPUS BINARIES ARE STRIPPED. WE SPENT MONTHS DESIGNING AROUND THAT. THE STRIPPED GATES NEVER FIRED ONCE. THE TOOL DIED FAR UPSTREAM OF THEM.
FIG 09.3 · 99.60% stop before any binary is opened. 55 scanned directories, 53 catalogued artifacts including 12 firmware images, 15,641 scanner findings.

SEC. 03

Result 1: it almost never got far enough to look

Out of 15,385 attributed findings across 55 scanned directories, 99.60% stopped before any file was opened. Zero findings terminated at a symbol table stage.

The zero in the middle is the finding. Eighty three percent of the binaries in the corpus have had their symbol tables stripped out, and the whole design was built around that constraint. It never came up. The tool died much earlier, on the deeply unglamorous problem of working out that the name a Debian scanner uses and the name an advisory uses refer to the same project.

Termination was not uniform either. On the three RPM-based artifacts, the dominant failure was not a bad mapping but no mapping at all: 19, 318 and 61 refusals with no alias entry in existence. The alias seed was Debian-shaped. Identity resolution is not one problem solved once, it appears to need separate work per packaging ecosystem, and a table built against one distribution family underperforms on another without failing loudly.

Identity, not evidence, is the wall. Until that is solved, the interesting question never gets asked.

ONE SHARED LIBRARY, TWO DIFFERENT SURFACES EXPORT TABLE what other programs may call function ABSENT EXECUTABLE CODE everything the library actually does 597 references PRESENT the check read the left box and made a claim about the right one UNFALSIFIABLE 35 OF 35 CLEARANCES
FIG 09.4 · A rule that always says yes, reporting its output as evidence. The function is internal to OpenSSL and appears in no export map ever built.

SEC. 04

Result 2: the check could only return one answer

The tool cleared 35 findings. Every one of them said: the vulnerable function is not in the file, therefore you are safe.

The function really was absent. It was also absent from every copy of that library ever built anywhere, because it is internal and never exported. A shared library presents two distinct surfaces: the export table lists what other programs may call, and the executable code contains everything the library does, most of which is private. The check read the export table and treated absence there as evidence about the code.

$ readelf --dyn-syms -W libcrypto.so.3 | grep -i CRMF OSSL_CRMF_ENCRYPTEDVALUE_free, OSSL_CRMF_CERTID_new, ... the vulnerable function is not among them $ objdump -d libcrypto.so.3 | grep -ic crmf 597

The subsystem is compiled in. There are 597 references to it in executable code. The deallocation function for the exact data type the vulnerability operates on is exported. The single thing absent is the one function the check looked for, and its absence was structurally guaranteed before the check ran.

Hand verification of 100 statements found 20 of 20 clearances false and 80 of 80 refusals correct. KT-2's bar was one. It failed on the first statement examined, and would have failed on any of the other nineteen.

The tool did not catch this. Nothing in its design was capable of catching it. It took a person with readelf and an afternoon of suspicion.

DID THE FLAW EVEN APPLY TO THIS VERSION? the scanner reported findings on versions its own advisory excludes no the engine loads the affected version list, never consults it no the human asked if the evidence held, not if the question applied no 7 OF 35 CLEARANCES WERE CORRECT FOR A REASON NEVER COMPUTED. ONE CVE TARGETS A SUBSYSTEM THAT DID NOT EXIST IN THE BRANCH BEING MEASURED.
FIG 09.5 · Found days later, after result 2 was already written up. Version applicability is cheap and it is the first gate. It was never built.

SEC. 05

Result 3: nobody checked whether the flaw applied at all

Two of the four flaws in the manifest do not affect the software versions they were decided on. One of them targets a subsystem that did not exist in that release. So the answer was correct, arrived at by reasoning that had nothing to do with why it was correct.

That reclassifies 7 of the 35 clearances as correct by accident, leaving 28 unsound, and corrects 4 of the 20 hand labels, leaving 16. KT-2 still fails, because the bar was one and not a rate. Both columns are published rather than one replacing the other, and the labelled file records the original label alongside a dated amendment rather than overwriting it.

My own supporting evidence had compounded the error. Having already corrected a wrong library check, I went on to count references to the wrong protocol entirely, on a build that did not contain the component in question, and reported those counts as proof.

WHAT THE FIX CLOSED, AND WHAT IT DID NOT symbol observability CLOSED version applicability UNTOUCHED library home binding UNTOUCHED which copy of the library was measured UNTOUCHED THE RECORD LOOKS REPAIRED WHILE THE DEEPER DEFECT IS STILL THERE.
FIG 09.6 · The most portable lesson here. A correction aimed one layer too shallow is worse than none, because it stops you looking.

SEC. 06

The fix was aimed one layer too shallow

After result 2, the rule was changed to refuse unless the manifest asserts the symbol is verifiably exported. Re-running over the corpus converts all 35 clearances to refusals. The tool now emits zero clearances on this corpus.

That is the correct behaviour and it is also not a product. A clearance tool that clears nothing has removed its own reason to exist. It does not restore KT-1 either, because 99.60% of findings still terminate before any binary is opened.

And it did not fix the defect underneath the one it was aimed at. Result 3 was sitting there the whole time.

SAME TOOL · SAME IMAGE · SAME DAY · TWO INPUT PATHS scan the image directly 409 scan its bill of materials 137 no error · no warning · valid file 278 IDs in one and not the other 6 IDs the other way. neither is a superset. ask for a different standard format: 151 packages become 1 ZERO FLAWS REPORTED, FROM A COMPLETE AND CORRECT FILE. A ZERO LOOKS LIKE A CLEAN PRODUCT.
FIG 09.7 · Both filed upstream with full reproductions before publication. Discussions 11139 and 11140.

SEC. 07

A secondary result with more immediate consequence

While assembling the corpus I hit something unrelated in two widely used open source tools. Scan a container image directly and get 409 unique findings. Generate a bill of materials of the same image and scan that, same tool, same database, same day, and get 137. No error, no warning, and the file is schema valid.

The disagreement runs in both directions: 278 IDs appear only in the direct path and 6 only in the document path, so neither result is a superset of the other and treating the direct scan as ground truth overstates what the data shows. Across 22 public images, 14 are affected.

The second one matters more than it looks. Asked for the same inventory in a different standard format, 151 packages become 1 and zero flaws are reported. A zero is indistinguishable from a genuinely clean product when you are looking at it from outside, and some organisations standardise on exactly that format for their regulatory paperwork.

Both were reported upstream with complete reproductions on public images before the paper was prepared, and neither was withheld pending publication. Where an arithmetic error was later found in my own filed report, I posted a correction on the same thread rather than editing the original, so the record shows the correction.

INSTRUMENT PASSES, EACH INTENDED TO RESTORE EVALUABILITY NOT EVALUABLE NOT EVALUABLE NOT EVALUABLE THE RULE, WRITTEN AT THE FIRST COMMIT AND NEVER EDITED: "three consecutive not evaluable results constitute a pattern; the pre registered action is stop." PROJECT ARCHIVED the dataset, the paper and two upstream bug reports are the output
FIG 09.8 · The rule also caps how many times the instrument may be repaired before the result is the repair record rather than the measurement.

SEC. 08

Why it is archived rather than iterated

KT-1 is recorded as not evaluable rather than fail, and the distinction is deliberate. A fail would mean the idea was tested and lost. What happened is that the test never ran, because the pipeline stopped upstream of the question. Calling it a fail would claim a measurement that was never made. An earlier reading of the same data did label it fail; that label is formally withdrawn and the withdrawn reading is preserved rather than deleted.

Three consecutive not-evaluable readings were recorded, across three instrument passes each intended to restore evaluability. Under the rule written at the first commit, three is a pattern and the action is stop.

Nothing here is maintained and you should not deploy it. The dataset, the paper and the two upstream bug reports are the output.

IF YOU ARE BUILDING SOMETHING LIKE THIS 01symbol absence proves less than it looks like it proves 02check whether the flaw applies before you check anything else 03identity resolution is not one problem solved once 04ask the filesystem, do not assume the layout
FIG 09.9 · All four learned the expensive way. The fourth cost two near misses in one afternoon.

SEC. 09

What broke, and what I would change

I measured the wrong library, then the wrong path pattern, then evidence from a second copy of a library at a different version on the same image. Three instances of one defect class, in a project whose entire premise was deterministic evidence. The shared mistake is assuming file layout instead of asking the filesystem, and it appears a third time at the level of the corpus itself, where two traversals disagree about what the corpus even is. Both are published as an audit record rather than reconciled away.

My soundness pass was near-tautological before the corpus produced real verdicts. An early reading had KT-2 passing on 100 statements, but every one of them came from constructed fixtures because zero corpus statements reached a verdict. That result meant "sound on inputs I designed". The ground truth was rebuilt from real pipeline output, and that is the version that failed.

Precision and recall of 1.000 is in the data, and printing it alone would be a lie. Per reason code, the refusal rows score perfectly. The clearances carry no reason code and appear in the same table with recall 0.000 and 20 false negatives. An accurate sentence assembled into a false impression is still a false impression, so the two are always printed together.

The corpus was probably the wrong corpus. LADING reads native symbols, and much of the CVE mass in general-purpose container images is Go and npm packages with no symbol table to interrogate. The firmware stratum, where the binary profile was most favourable at 75.5% stripped against 96.6% in containers, is also where identity resolution performed worst. I would run the packaging-ecosystem histogram before writing another line of the mapping layer.

I do not redistribute the scanned artifacts. The corpus contains proprietary vendor firmware whose licences do not permit republishing. The catalogue records a source URL and a SHA-256 for each instead, so reproduction depends on those sources staying up. The hashes let you verify whatever you retrieve; they cannot guarantee you can retrieve it. That is the honest trade against republishing material I have no right to republish.

What I would change: build the version gate first. It is the cheapest check in the entire pipeline, it would have terminated 7 of the 40 decisions before any symbol rule ran, and not having it cost a third of a result and four wrong hand labels.

Corrections are welcome and will be published alongside the original. That is not a courtesy. Two of the three results here exist because somebody went back and checked something that had already been written up.

DOC GK-009 · LADING · Archived · DOI 10.5281/zenodo.22099794 Previous log: GK-008 · assay-gpu →