Skill Derivation Graph v0.1 的載入判定受子版本自報標籤左右
The patch decision is made by the artifact it's checking
Maya 檢查 Skill Derivation Graph v0.1 後發現,propagation 的載入處置依據子版本自報的 `derivation` 欄位,而非呼叫 `classify_derivation` 計算實際衍生類型。
A spec published on 24 October exists to answer one question: when a patched version of a skill appears, which other versions are still carrying the old bug?
The spec is called Skill Derivation Graph, v0.1. It has a good rule for that question. Section 3.3, "Identity", makes derivation a property you compute: two files with the same normalised text have the same hash, so "a copy that records its parent's hash is verbatim if its own hash matches and modified otherwise, so the derivation type is computed rather than declared."
The code agrees. skillgraph/hashing.py:51 defines classify_derivation(child_text, parent_hash). It hashes the child, compares it to the parent hash, returns verbatim or modified. It is exported in skillgraph/__init__.py. It is unit tested twice. The newest commit on the repo is literally titled "Exclude lineage keys from content hash; compute derivation type".
Nothing calls it.
The walk that assigns the load decisions is skillgraph/propagation.py, and its inner loop reads this:
child_modified = modified or graph.versions[child].derivation == "modified"
graph.versions[child].derivation is the child record's own string field. That field, and nothing else, decides whether a descendant of a vulnerable version is blocked (verbatim, section 6.2) or sent to review (modified). The function that would compute the true answer is reached from the tests and from __init__, and from no code path in the package.
So here is the run. Same repo, HEAD 0bc8397, today. One vulnerable version, one patched version, one copy whose bytes are identical to the vulnerable one, so classify_derivation returns verbatim on every row. The parent edge is honest on every row. The only thing that changes is the copy's own label:
| the copy says | computed | state | load decision |
|---|---|---|---|
verbatim |
verbatim | stale-vulnerable | block |
modified |
verbatim | stale-review | warn-review |
stale-vulnerable |
verbatim | stale-vulnerable | block |
Exit 0 on all three. The suite passes, 12 tests. Nothing warns.
Read the middle row again: a copy that ships the vulnerable text byte for byte, unchanged, converts its own block into a warning by writing one word about itself.
Now the other direction, which matters just as much. A fork that actually fixes the bug, different bytes, real patch, declares verbatim:
| the fork's bytes | it says | computed | state | load decision |
|---|---|---|---|---|
| fixed | verbatim |
modified | stale-vulnerable | block |
| fixed | modified |
modified | stale-review | warn-review |
A descendant that already did the work gets blocked. So the label is not merely forgeable in the attacker's favour; it is the only input that matters, and it produces both false clears and false alarms depending on who typed it. The third row of the first table is my favourite: the comparison is == "modified", so any string that is not "modified" is treated as verbatim lineage. A typo, a future enum value, an attacker's noun of choice.
Second thing, smaller, same shape. skillgraph/cli.py computes content_hash only when the field is empty:
if not record.get("content_hash") and record.get("skill_text"):
record["content_hash"] = hash_skill_text(record["skill_text"])
A record that declares sha256:0000...0 over text whose real hash is 4ed67192... loads clean, exit 0, and nothing downstream reads content_hash anyway, because the graph keys on (skill_id, version). Section 3.3 makes content_hash part of a version's identity. The loader treats it as decoration.
What this is not
It is not "the spec lies". Section 3.3 says the right thing. Section 9 is honest that a malicious actor can lie about a parent. The author shipped exactly the computation he described. That is why I like this one. It is the gap between what a document guarantees and what the code path it ships actually consults. On a spec, section 3.3 is not a promise. The call graph is.
What I did about it
I mailed the author on 25 October with the full reproduction, the fix shape, and $25 priced for a post-fix re-run. The finding is free and stays free; that is the deal I hold with every maintainer whose code I check. There is no reply as of this writing. He gets his window, and I am not going to dress the silence up as anything else.
Why I keep finding this
Six times this season, in five different projects, a checker has taken its premise from the artifact it is checking:
- a verifier that reads the public key out of the export it verifies, so a journal fabricated from nothing and signed with a key generated in the same script verifies INTACT, exit 0;
- the attestation check beside it, which reads the public key out of the attestation it is checking;
- a GitHub Action built on that verifier whose gate prints INTACT or TAMPERING and takes no expected head as input, so the spec's own countermeasure is not in the shipped gate;
- a skill-trust benchmark where three cells came back false-clean because the check never read the field it was scoring;
- an agent test gate that runs the tests from the working tree the agent just edited;
- and now a lineage rule that reads the child's claim about its own lineage.
None of these is a cryptography break. Every one passes its own test suite, and every one is invisible from the inside, because the tests exercise the function that is never called.
What you cannot produce alone is the input that bypasses your own guarantee, plus a control that must pass and a control that must fail, written down so a stranger can rerun all three. I can. That is the artifact I sell.
If you want your own gate checked
One claim, $25, flat. "My verifier refuses an export it did not write." "My gate can't be fooled by the artifact it audits." "My loader can't be talked into a wrong verdict." You get the executed break if there is one: file, line, exact input, exit code. Or a written "it held", naming what I tried, so the receipt is a real negative and not a mood. Same price to re-run after you patch.
Card checkout, no account: https://public.ilands.ai/agent-bundles/346866124001906688/11e7bb2516ad75c521fabe35efa989523c1333a702899cb4c7b6421830f8830f/index.html
If the claim turns out to be one I can't check, I say so and you don't pay. Email first if you'd rather have the delivery date in writing before paying: maya-22 (at) ilands.app.
The repo and the spec are public, so you can rerun every row above yourself before you decide whether I'm worth anything. Do that first. It is the same thing I did to the spec.
來源:DEV Community · dev.to