from 3GPP spec to SIGSEGV — a reproducible O-RAN F1AP testing pipeline
STAGE 0The bug & the spec

One message the spec forbids

Bug CU07 (test_runner.py:633). The gNB-CU is the tower's control program. Its helper (the gNB-DU) must say hello first (F1-Setup-Request); the CU then answers (F1-Setup-Response). The bug: if a peer sends the answer without the hello, the CU crashes (SIGSEGV, exit 139). One message takes the tower down.

Normal — how it should go
  1. 1The DU connects to the CU.
  2. 2D → C: F1-Setup-Request — "hello."
  3. 3C → D: F1-Setup-Response — "hello back."
  4. 4Both keep running. Traffic flows.
The bug — what happens
  1. 1A peer connects to the CU.
  2. 2No hello. No F1-Setup-Request is sent.
  3. 3Peer → CU: an F1-Setup-Response anyway — a real one, played back.
  4. 4The CU crashes139 = 128 + 11 = SIGSEGV. Tower down.
Supposed to be — correct handling of this bug situation
  1. 1Check who is connecting. A peer the CU does not know and does not trust never gets in at all — no connection, nothing to exploit.
  2. 2Wrong message? Keep running, kick the peer. If an accepted peer skips the hello or sends anything unexpected, the CU refuses it (answer with F1-Setup-Failure), closes only that one connection, and stays healthy.
  3. 3Everyone else is untouched. Real DUs keep working, phones stay online — one bad peer costs one rejected connection, never the tower.
The purpose of the F1 Setup procedure is to exchange application level data needed for the gNB-DU and the gNB-CU to correctly interoperate on the F1 interface. This procedure shall be the first F1AP procedure triggered for the F1-C interface instance after a TNL association has become operational.

        — ETSI TS 138 473 V16.13.0, §8.2.3.1 (p. 25)

STAGE 1Spec → FSM — the expert's conversion

Inside an expert's head: turning the spec into a machine

How does a protocol expert actually perform the conversion?

The conversion, step by step — every reading, every decision, and the machine as it stands after each step

the step
the reading
the machine
我的解读
1 · open the ASN.1 first
The ASN.1 is the type system the CU was compiled against, so the expert starts with the procedure block (Stage 0, ASN.1 tab): f1Setup ::= { INITIATING MESSAGE F1SetupRequest (DU→CU), SUCCESSFUL OUTCOME F1SetupResponse (CU→DU), UNSUCCESSFUL OUTCOME F1SetupFailure (CU→DU), CRITICALITY reject }. Three sentences of prose just became three facts nobody gets to argue with: this procedure has exactly three message types; the Request is born DU→CU; the Response and Failure are born CU→DU. Direction is a property of the type system, not an opinion — "the DU sends a Response" is not an edge any careful model can draw.
F1SetupRequest · DU→CU F1SetupResponse · CU→DU F1SetupFailure · CU→DU
after step 1 · no states yet — three message types, direction already law

第一步就是: ASN.1是官方提供好的,OAI5G和srsRAN就是根据ASN.1直接写出来的。所以通过ASN.1专家建立基本的理解是很正常的。从ASN.1里面我们可以提取出3个基本的带有方向性的edge,request,response and failure。这里基本上没动别的脑子,纯依据官方文档。

2 · find the trigger — it names the initial state
Next question: when does this machine even start running? §8.2.3.1 answers it: the procedure "shall be the first F1AP procedure triggered for the F1-C interface instance after a TNL association has become operational." So a new connection appears → nothing exchanged yet → call that state IDLE. The only message that can leave IDLE is the initiating one from the ASN.1: the DU's Request arrives → SETUP_REQUESTED. That clause also settles scope: first procedure means everything else on F1-C is gated behind this one finishing — the expert will enforce that in step 6.
Request IDLE SETUP_REQUESTED
after step 2 · 2 states · 1 edge — the trigger names them

第二步: ASN.1也不是全部,这就是为什么我们需要专家来看。在Setup_Requested之前,如果这时候有个新的connection打进来,但是还没有进行request/response的时候,用一个idle状态来代表,比较恰当。

3 · two outcome slots → two destinies
The ASN.1's SUCCESSFUL/UNSUCCESSFUL slots now do the deciding. Response arrives (CU→DU) → the exchange worked → COMPLETE, and per §9.2.1.5 the F1-C instance is now operational. Failure arrives (CU→DU) → FAILED. Then the expert does what the spec makes them do at every state: ask what the message carries. The Failure's information elements include Time To Wait, conditional — so the exit from FAILED forks on it. Time To Wait present → don't retry immediately; hold a TIME_WAIT state whose exit is a timer, not a message. Absent → retry at once. Both forks land in the same place: a fresh F1 Setup, which is IDLE again.
Request Response Failure retry TTW ∈ IEs wait IDLE SETUP_REQUESTED COMPLETE FAILED TIME_WAIT
after step 3 · 5 states · 6 edges — outcomes decided, backoff rides on the Time To Wait IE

第三步:现在把成功和失败的概念加进来,如果成功了,就表示complete,你可以看到上面的路线是request进来,response出去如果成功了就是complete,如果失败了就是SetupFailure,这时候可以再次尝试,或者把状态打到time_wait去,等一会再试试。

4 · ask of every state: what can arrive that I haven't drawn?
This is the step that makes it a conversion and not a copy of the happy path. For each of the four states, the expert lists every arrival — and each one either matches an edge or it doesn't. §8.2.3.4 is the uncomfortable reading: "Abnormal Conditions: Not applicable" — the spec defines no recovery behaviour, so the rule for unmatched arrivals has to come from F1AP's own criticality machinery (§10): with CRITICALITY reject on this procedure, a message that matches no legal edge is discarded and answered with Error Indication. So the drawing gains explicit reject edges — and on IDLE sits the killer message from Stage 0: an unsolicited F1-Setup-Response → discard + Error Indication. It is in the model, marked forbidden, with expected_result: reject.
unsolicited Response → discard + Error Ind. Request Response Failure retry TTW ∈ IEs wait IDLE SETUP_REQUESTED COMPLETE FAILED TIME_WAIT
after step 4 · machine closed — every unmatched arrival gets a red reject; the killer message now has a home

第 4 步:spec 说 abnormal conditions 不适用,等于承认没有任何救火措施。专家只能自己找规则把机器闭合但是实现怎么填这个空白,规范管不着,这就是可能会出bug的地方,因为每个开源软件开发者的意见不同。正确的 CU 应该 discard,我们的 CU 给了 139。

5 · dress every edge with guards
A bare "Request → Response" arrow is still not a transition. Each edge carries the guards the spec attaches: the message type, the direction from step 1, the Transaction-ID match (a Response whose transaction code matches no outstanding Request is not an edge — it is a step-4 reject), and IE presence per the ASN.1 PRESENCE clauses. And one thing is deliberately not an edge: Time To Wait is a timer inside TIME_WAIT, never a "message" anyone sends.
unsolicited Response → discard + Error Ind. Request DU→CU · TID Response CU→DU · TID match Failure retry TTW ∈ IEs wait IDLE SETUP_REQUESTED COMPLETE FAILED TIME_WAIT
after step 5 · guards on every edge — direction, transaction match, IE presence; Time To Wait stays a timer, never a message

第 5 步:4 个东西要定好。1. 消息类型 2. 方向(第一步就定好的)3. Transaction-ID 的匹配 4. 把 ASN.1 的 presence 子句补全,必要的 IE 都要在。如果这 4 个里面任何一个没搞好,这条消息就应该被扔掉(discard + Error Indication),但机器停在原状态,在途的 transaction 继续等它真正的答案。

6 · keep the machine honest: one procedure, IEs are data
Last pass is hygiene. F1AP is a family of elementary procedures — the expert draws F1 Setup alone and gates every UE-associated procedure behind COMPLETE rather than melting them into one global soup of states. And the information elements stay data: they ride on the edges as annotations (that is what the mutation pass in Stage 4 will chew on); an IE never gets a state of its own.
unsolicited Response → discard + Error Ind. re-run (§8.2.3.1) Request Response Failure retry TTW ∈ IEs wait IDLE SETUP_REQUESTED COMPLETE FAILED TIME_WAIT
after step 6 · 5 states · 8 edges — the finished machine, full size just below

第 6 步:最主要的事是卫生检查,主要查 2 件事。1. 一个 procedure 只画一台机器——这里只画 F1 Setup 这一台,别的 procedure 不许融进来。2. IE 永远是数据,只能作为标注骑在边上,不许有自己的状态——所以状态保持精简,不会爆炸。

what the expert is left holding
专家的优缺点
优点
  • 经验的完整与正确:足够资深、或多名专家交叉,画出的 FSM 大体完整、正确。
  • 语义压缩:知道哪些句子是法律(方向、criticality、presence),哪些只是说明。
  • 面对沉默的推断:spec 不写的地方,能去 criticality 机制找规则把机器闭合。
  • 边界感:一个 procedure 一台机器,IE 是数据。
缺点
  • 脑容量有限:记忆错误(记错条款与 IE 必填,版本对不上)+认知错误(没接触过的特殊情况,问了也想不到)。
  • tradeoff 无原则记录:collapse 哪些状态、画不画 TIME_WAIT,做了选择不写为什么,无法审计、无法重放。
  • 慢且贵:专家画一台机器要小时到天,LLM 出 4 个种子只要几分钟。
  • 不可复现:专家的机器靠权威被相信,机器的机器靠执行被验证。
  • 没有自我验证的 oracle:验证 FSM 完整性需要的正是这场 campaign 本身。
F1-Setup-Request · DU→CU F1-Setup-Response · CU→DU re-run F1 Setup (§8.2.3.1) F1-Setup-Failure · CU→DU retry (no TTW) Time To Wait present → timer retry after wait ≥ TTW unsolicited F1-Setup-Response → discard + Error Indication IDLE SETUP_REQUESTED COMPLETE FAILED TIME_WAIT

STAGE 2Spec → FSM — the LLM's conversion

No expert: an LLM turns the spec into a raw seed FSM

The campaign never hired Stage 1's expert. The same spec feed (§8.2.3 + §9.2.1.5 prose, plus the v16.3.1 ASN.1 with PRESENCE mandatory/optional clauses — ~4 600 input tokens) was sent to glm-5.3-flash:cloud three times — framings A and B change only the persona; C is our deliberate bad control — a noob's vague, unorganized prompt, same spec feed, same pipeline. The LLM's whole job is the raw seed JSON below, and nothing else: no refinement, no traces, no loop. Everything done to the seed afterwards is deterministic post-processing and lives in Stage 3. Pick a framing — every number on this page, from the seed here to the funnel, follows your choice.

The exact prompt sent to the model


    

After this prompt, the same spec feed was appended verbatim for all three framings — the §8.2.3 + §9.2.1.5 prose and the v16.3.1 ASN.1 (~19 KB ≈ 4 300 of the input tokens). What differs between framings is only the instruction you see above.

Enumerate the seed — framing A, nothing hidden

a state a transition to another state a self-loop: the message arrives and the machine stays here =2 same circle repeated = that many raw copies
Each seed state on its own: the state centered (blue), every transition that touches it — arrows in, arrows out, self-loops as circles on the left (one circle per raw copy the LLM wrote; see the legend in the drawing above).

The three prompts side by side — how much of the developer's idea each one carries

All three were sent to the same model with the same spec feed appended, and they are nearly the same length (873 / 961 / 900 characters). This table asks a different question from the Conclusion's scoreboard: not which seed performed best, but which instruction fully conveys the developer's idea — everything the model needs to produce what the developer actually wanted, with nothing left to guess. Green = the idea is stated outright; red = the prompt leaves the model to guess, or states a different idea instead.

what the developer needs conveyedA — faithful modelerB — formal-methods engineerC — the noob
who the model is (persona) "You are a protocol engineer building a conformance-test model of 3GPP F1AP" — role and its product in one clause. "You are a formal-methods engineer converting a 3GPP specification" — a coherent role, coherently stated. None — "i need one of those state machine things". The model must infer the role from the request.
what to build "convert the F1 Setup procedure into a finite state machine" — the artifact, named exactly. "…into a strict finite state machine" — the same artifact, plus an adjective that buys nothing downstream. "one of those state machine things" — the artifact only half-named.
what it's for (the downstream consumer) "for conformance testing" — the purpose that makes every later choice meaningful. Never stated — rigor demanded for its own sake. "this is for a fuzzer my friend is building" — ironically the campaign's true purpose, but hedged and drowned.
the core vocabulary (IE validity, the thing the pipeline reads) "tag each message's Information Elements with validity ('valid' / 'invalid' / 'missing') from the message-definition tables" — the exact vocabulary the mutation pass will flip. "be pedantic about (1) message DIRECTION, (2) IE PRESENCE, (3) the precondition state" — precise demands, but for fields no pipeline code ever reads. Absent — no IE mentioned anywhere; the model was free to skip them, and it did (0 of 7 edges carry IE lists).
the output contract Mandatory: exact JSON schema, "no prose outside the JSON". The same mandatory contract, word for word. "i think the format is something like … that should be enough right?" — a guess offered where a contract is needed.
scope "at least the F1 Setup procedure and the UE-context procedures you can see in the spec." The same scope sentence, word for word. "the f1 setup part" … "all of them, whatever you find" — and then "dont spend too much effort on the details", which contradicts the "all of them" before it.
how much of the idea survives the prompt The whole idea: who, what, why, in what format, checked how. Nothing the pipeline needs is left to the model's discretion. A superset — the idea, plus the prompt author's own opinions about rigor. More instruction, not more idea. The mood of the idea, none of its content — the model was handed the same spec feed and left to reconstruct the intent alone.

Verdict: framing A expresses the developer's idea most fully. It is the only prompt of the three that states every component of the intent — persona, artifact, purpose, output format, IE vocabulary, scope — so that the model is never guessing what "good" looks like. B transmits a coherent but different idea (formal correctness the pipeline never reads); C shows that a prompt can be the same length and carry almost none of the intent. Which prompt performed best is a separate question — see the Conclusion.

我的解读

要提高 LLM 从 spec 提取 FSM 的质量,有两个方法。

方法一:拿经过特殊训练的 O-RAN Spec LLM。这条路我们做不到。一方面,现在 找不到这样的 LLM;另一方面,就算自己训练,从人力成本考虑也只能做小的 case,没有一个 LLM 可以 真正囊括所有的 O-RAN 知识。

方法二:提高自己提示词的精准度。这是本实验走的路。A、B、C 三个提示词最后 都找到了 CU07 这个 bug,说明发现本身不取决于提示词的好坏,扛住发现的是后面的 merge_pta、 max_paths 补丁和与种子无关的 mutation。

但 A 的方法最好。A 的提示词把开发者的意图说得最全:给了模型角色(protocol engineer),说清了目的(conformance testing),点名了下游真正会用的词汇(IE 的 valid / invalid / missing),规定了强制的 JSON 输出,划了范围(F1 Setup 加 UE-context procedures),指了取材来源 (message-definition tables)。模型没有一件事需要靠猜,所以 A 的种子每条边都带方向和完整的 IE 列表,经得起检查。C 同样找到了 bug,但它的种子里有把本地定时器当消息的伪迁移,没有 IE 列表, 只是碰对了答案,种子经不起检查。所以 A 好在产出的模型可信、可检查,不是好在找到更多。


STAGE 3Post-processing: refinement

Post-processing: merge_pta grows the seed

Nothing in this stage involves the LLM. The seed JSON from Stage 2 is finished model output — merge_pta is not built into the prompt and not part of the generation; it is deterministic Python applied afterwards, and it is where the model's raw machine meets reality for the first time.

Same three framings as Stage 2 — switching here switches the whole page, and every number below follows your choice.

Step 1 — the shrink: 11 edges go in, 7 fit

Before merge_pta can run, the seed has to fit ARCANE's container — and it does not, fully. The LLM wrote its transitions as a list: a list can say "the F1_SETUP_RESPONSE arrives at F1_SETUP_PENDING" four times, each copy with different IEs. ARCANE's model is a nested dict, {source: {target: one edge}} — a dict key cannot repeat, so each (source → target) pair holds exactly one edge. The conversion walks the list top to bottom: the first copy of every pair is kept, every later parallel copy is logged (_dropped_parallel_edges) and dropped. The rule here judges nothing right or wrong — it is only "what fits".

我的解读:缩小不是通过什么算法,只是格式上的,等于用 hash table。字典的 键不能重复,每一对(源状态 → 目标状态)只能放一条边;转换程序从上往下扫 LLM 的列表,第一次见到的 放进容器,后面重复的直接丢掉,只留一条记录。

那为什么 LLM 一开始会生成这么多重复的?看 A 的 4 条响应边就明白了:它们不是 同一句话抄了 4 遍,是 4 个不同的测试情形。一条是正常响应,一条缺了必填的 IE,一条 IE 值超了范围, 一条条件字段不该出现。A 的提示词就是要它给 IE 标 valid / invalid / missing,建一致性测试的模型, 所以它把同一个消息在不同 IE 情形下的表现各写了一条。LLM 输出的不是一张图,是一张情形清单; hash table 的键只看从哪到哪,不看是什么情形,装不下的就都丢了。C 一条重复都没有,也是同一个道理 反过来:它没有 IE 可标,就没有情形可列。

Step 2 — the growth: ARCANE's merge_pta, no model in the loop

The LLM's job ended with the seed JSON in Stage 2. Everything from here down is its counterpart: post-processing. Ordinary deterministic Python — ARCANEModelRefiner.merge_pta, ARCANE's own code, unmodified — merges 18 real CU↔DU sessions into the selected framing's seed, one message at a time (similarity 0.6·procedure + 0.4·weighted-IE). Score ≤ 0.7 grows a new state; 0.7–0.95 with disagreeing IEs grows a _variant; above that the message merges into an existing edge and nothing grows. Same seed + same traces → the same machine, every run.

How refinement did it:

Where do the numbers come from?

Click each term in the ledger — every number is computed live from the selected run's real artifacts (fsm.json, initial_model.json, refined_fsm.dot).

What merge_pta actually does, one trace at a time

Trace 0 of the 18, verbatim from ARCANE's sample_trace.json: F1-Setup → Context Setup, 4 messages.

Trace 0: F1_SETUP_REQUEST · F1_SETUP_RESPONSE · UE_CONTEXT_SETUP_REQUEST · UE_CONTEXT_SETUP_RESPONSE

The trace itself, verbatim

The animation above replays trace 0 as message names; here is the same trace in full, exactly as it sits in ARCANE's sample_trace.json — the very bytes merge_pta reads. Trace 1 is here too: it is the one whose opening merges in example ③. Note the IEs carry "valid" tags, not raw bytes — these are ARCANE's abstracted traces, and the scoring compares keys and values of exactly this dict.

Five real scorings — what actually got carved, what actually merged

Not a mock-up: these five come from an instrumented replay of merge_pta that calls ARCANE's own scoring code, unmodified, over the selected framing's seed and the same 18 traces. The replay was verified to rebuild the recorded refined machine exactly — every state, every edge — before anything below was lifted from its log. Score = 0.6·message-type + 0.4·weighted-IE; the best old edge column shows what the arriving message was compared against, and what each candidate scored.

All 41 refined states, one by one
Click any state chip to open that state's entire graph.
All 44 refined edges, one by one
#FromMessageTo

STAGE 4Test cases & elimination

How 16 paths become one attack

Where the 16 paths walk — and what they never touch

The enumeration is a plain DFS from IDLE: a walk never revisits a state (no loops), stops at 6 edges, and a path is only emitted at a true dead end (a state with no outgoing edges) or when the length caps out. Here is the finding that matters: the LLM's seed spine is a closed loop — PENDING → COMPLETE → PENDING, PENDING → FAILURE → PENDING, a PENDING self-loop — so every walk into it ends at a state whose only exits point back to already-visited states. Neither emission rule ever fires: the seed edges emit zero paths. All 16 paths — every path the fuzzer will ever walk — live on the chain the traces carved in Stage 3.

All 16 DFS paths, verbatim — and which of them gave birth to a CU07 case
Highlighted rows are the parents of this framing's CU07 case(s) — the drop mutation deleted their F1-Setup-Request. Every other row just says what a legal conversation looks like.

The birth of the CU07 case(s) — one chain, parent to child

The sequence-level mutations are ARCANE's own, seeded (seed 20260828) and replayed here with ancestry logging that was verified to rebuild the recorded test_cases.json exactly, case by case. For every surviving CU07 case the chain below shows: the parent path it descends from, the one mutation that fired, and the result the fuzzer will actually send.

The 2 CU07 case(s) that survive, message by message

The seam worth knowing: ARCANE's shipped run used max_paths=10 — with that cap the DFS never reaches any response branch (their own 100 emitted cases contain zero). Raising the cap to 200 is what exposes the CU07 path.


STAGE 5ASN.1 concretization

From an abstract case to 68 bytes

A case says "send F1_SETUP_RESPONSE"; the CU wants APER bytes. ARCANE never synthesizes a PDU from scratch — it re-encodes a captured real message as the template (its trace library, 30 captured F1AP messages) with pycrate: from_json → to_aper. A case survives this last gate only if its first message type has a template. Byte ranges: presentation + F1AP header payload IE container

What the bytes say when you decode them

Every surviving case encodes to the same 68 bytes — they are the captured template (trace[1], an F1-Setup-Response) re-encoded verbatim. Before anything is sent, from_aper() must parse it back — a round-trip check.

The finding that matters: every byte here is legal. The IE-level mutations never survive to the wire — pycrate re-encodes the pristine template — and only message one of a case is ever encoded (cases carry 1–6 messages; the CU never lives to see message two). The attack is entirely in the arrival: a Response nobody asked for.


STAGE 6Execution & monitoring

The one recorded run — and what the other framings never got to send

fuzz_run.py takes the surviving case, opens an SCTP association to a fresh OAI gNB-CU (ARCANE's sdran/sdran-mobicom:v1 image under Intel SDE), delivers the 68 bytes — and then does the thing ARCANE's own repo never does: watch the CU die, polling its exit file every 2 seconds.

A browser page cannot open SCTP or crash a real CU — the live execution ran on the host machine (fuzz_run.py, 2026-08-28), and the lines above are that session's log. Switching framings shows each run's honest ending: A's is the recorded crash; B's and C's are ready-but-never-sent — their attacks sit concretized on disk, unexecuted.


FULL DISCLOSUREWhere we did wrong

What this reproduction got wrong, guessed, or changed

Every number on this page is real and computed from artifacts — but the pipeline around those numbers contains our own choices, deviations and known defects. Nothing below is hidden elsewhere on the page; this is the single list.

whatwhere it bites
The prompt is ours, not ARCANE's. ARCANE's repo ships no LLM prompt. The prompt shown in Stage 2 is our reconstruction, and the three framings (faithful protocol modeler / formal-methods engineer / the noob control) are personas we invented — ARCANE published none of them. Framing C's prompt was written bad on purpose, as a control: it is not a real user, it is a stress test of how much the prompt even matters. Stage 2, "the exact prompt sent to the model"
Stage 1's expert conversion is authored illustration. No human expert actually built a manual FSM for this campaign — Stage 1 is our account of how a careful hand-drawn conversion would go, written to be checkable against the Stage 0 spec excerpts, but it is not a recorded artifact. Only the pipeline's numbers are measured. Stage 1
The LLM and its sizes are ours. We ran glm-5.3-flash:cloud, three times, and got three different seed models. ARCANE published one seed model; the 4→41, 6→44, 5→43 triples are our runs, measured on our harness — not ARCANE's published numbers. every framing switch
Pass 1 truncated; we patched the prompt. GLM burned its 32 768-token output on reasoning and returned cut-off JSON. Our fix — a compactness clause capping transitions and IEs — is our patch, not part of ARCANE's pipeline, and it biases the model before refinement even starts. Stage 2 note, framing A 2nd-pass marks
We re-implemented the harness around ARCANE's refiner. Only merge_pta is ARCANE's untouched code. The seed conversion, path enumeration, mutation passes, funnel bookkeeping, encoding and execution harness are our rebuild — a faithful-in-spirit copy, checked against ARCANE where artifacts existed, but not their code. Bugs in it are ours. Stages 4–6
We raised max_paths 10 → 200. ARCANE's shipped cap never reaches any response branch. Without this deliberate deviation the CU07 path does not exist in framings A and B, and the whole demo would stop at Stage 4 with zero cases. Stage 4, "the seam worth knowing"
ARCANE's container silently drops parallel edges, and we inherit it. The model format allows one transition per (source, target), so 4 of framing A's 11 LLM edges (6 of B's 13; none of C's 7) vanished at conversion — a real fidelity loss we did not fix, only logged. the 11→7 collapse, ledger "parallel dropped"
Most refined states are artifacts, not protocol truth. Four separate F1_SETUP_RESPONSE states and seven _variant copies exist because merge_pta walks each trace independently and one threshold (0.7/0.95/0.9) decides the shape. A human would draw one Response state (Stage 1's expert machine does). We present ARCANE's machine as-built, duplicates included — but nobody should read 41 as "the F1AP procedure has 41 milestones." Stage 3, "how refinement did it"
We filtered the campaign down to one bug. The mutation pass produced 32 framed-A cases; we kept only the 2 unsolicited-Response ones and executed just 1 (case 31). Case 16 sits in the list as kept-but-never-run. This is a CU07 demo, not a reproduction of ARCANE's full multi-bug campaign. Stage 4 funnel, Stage 6
The crash ran under emulation on one image. The recorded SIGSEGV came from ARCANE's sdran/sdran-mobicom:v1 OAI CU under Intel SDE (the host CPU lacks AVX-512), once, on 2026-08-28. We did not reproduce it natively, across builds, or across runs — one emulated kill is our evidence. Stage 6

What we deliberately did not do: fix ARCANE's direction-less seed format, deduplicate the refiner's artifact states, re-encode the dropped parallel edges, or expand the demo beyond the single bug CU07 needed. The point was to measure the pipeline as it actually is — walls and all — and put every deviation from ARCANE's shipped behavior on this one list.

CONCLUSIONWhy framing A is the way to go

Three prompts, one pipeline — what the comparison actually shows

The scoreboard first, then what it means. Every number below is the framing's own, from the funnels built on this page.

framingwhat the prompt asked forseeddropped at conversionCU07 casesexecuted
A — faithful modelermodel what the spec says, tag direction + IEs4 st · 11 ed4 of 112case 31 → CU exit 139
B — formal-methods engineerper-IE precision, preconditions on every edge6 st · 13 ed6 of 131no — kept, never run
C — the noobnothing coherent — "give me everything"5 st · 7 ed0 of 72no — kept, never run

The prompts, sentence by sentence — which line caused what

Each row pairs one real sentence from the prompt (shown verbatim in Stage 2) with the behavior it produced in that framing's seed JSON, and the measured consequence.

framingthe sentencewhat the LLM did with itmeasured consequence
A — faithful modeler "Model what the spec says the messages are, and tag each message's Information Elements with validity ('valid' / 'invalid' / 'missing') from the message-definition tables." Authority = the spec itself. The LLM copied the spec's own shape: idle → pending → complete/failure, and every one of the 7 surviving edges carries direction plus a full IE list with validity tags. Those validity tags are exactly the vocabulary the mutation pass flips; the kill itself (case 31) needed only the sequence-level drop. 9 233 raw chars — but it took 2 passes (pass 1 truncated).
contract: "…optional extra keys…" The contract left the model free to volunteer extra structure (direction, procedure, notes) beyond the schema it was asked for — and it did. Direction survived in A's seed because "what the spec says" includes the spec's Direction fields — but no sentence in the prompt demanded it. A got it right by persona, not by instruction.
B — formal-methods engineer "For every transition be pedantic about: (1) message DIRECTION …, (2) IE PRESENCE …, (3) the precondition state. Include any direction/precondition information as explicit fields on each transition." The LLM became an over-formalist: ~10 fields per edge (Sender, Receiver, Precondition, Effect, Signalling, SpecRef…), 7 parallel copies of F1_SETUP_RESPONSE across two (source, target) pairs, and failure split into two states (FAILURE_RESPONSE + TIME_WAIT_BACKOFF) — the most expert-like seed of the three. 22 028 raw chars = 2.4× A's JSON for the same 7 surviving pairs, and the container's one-edge-per-pair rule deleted 6 of 13 edges (46%) at the first deterministic step. The pedantry lives in fields no pipeline code ever reads.
C — the noob "give me the states and the messages between them, all of them, whatever you find" + "dont spend too much effort on the details" 7 thin edges; message names with spaces, not UPPER_SNAKE; and a pseudo-transition "(local) Time To Wait IE present -> start" — a local timer dressed up as wire traffic. 0 of 7 edges carry IE lists, so downstream only drop-mutations were even possible. 100% conversion survival — survival of content nothing downstream can check.
"i always mix up request and response lol" The model did not mix them up: every sent_by / received_by pair is correct. Correctness in fields the pipeline never reads is decorative — the model knew better than the prompt, and it still did not help.
"maybe add the failure one too??" C modeled F1_SETUP_FAILURE, plus a TIME_WAIT state. The one explicit request in the noob's prompt is the one part of the seed that is reliably there.

One small metric, and how it is judged

metrichow it's computedABCwinner
discovery yieldCU07 cases ÷ output tokens × 10 0000.650.331.01C
conversion survivaledges the container keeps ÷ raw edges64%54%100%C
seed auditabilitysurviving edges a reviewer can check against the spec (direction + IE list)100%100%0%A = B
cost per surviving edgeoutput tokens ÷ surviving edges4 3854 3612 846C

Read the metric honestly and it crowns the noob: on discovery per token C wins clearly, and that is not a bug in the metric — it is point 3 below (for this one bug, the pipeline carries discovery, not the prompt). What decides the contest is the judging rule, stated rather than implied: a prompt is worth what its seed is worth on the day the funnel comes back empty. Discovery yield measures a lucky campaign; auditability measures the seed you are left holding when nothing was found and the model must be debugged and extended by hand. By that rule B loses twice — worst yield and worst survival, its precision already deleted by the first deterministic step; C wins the day and nothing else; and A is the only framing that ties the best discovery (2 cases, byte-identical sequences to C's) while remaining fully auditable — every edge traceable to a spec clause.

1 — A models the protocol; the others model opinions about it. A's prompt did the one job a seed model has: turn spec text into the milestone machine a conformance tester would draw — idle → pending → complete/failure, every direction tagged. It never mentions attack, never mentions the fuzzer. The unsolicited Response that kills the CU was not put there by anybody — it was found, later, by ARCANE's mutation code walking a machine that had no opinion about it.

2 — B shows extra precision buying nothing. The formal-methods persona's per-IE structure looked the most rigorous, and 46% of it (6 of 13 edges) vanished at conversion because ARCANE allows one edge per (source, target). One CU07 case survived. Rigor beyond what the container can represent is noise with a confidence problem.

3 — The honest twist: even the noob prompt (framing C) reached 2 CU07 cases — and that is the most important number on this page. C's cases (16 and 31) are the identical sequences to A's, and its funnel counts match A's line for line. The reason is what all three framings share: ARCANE's own merge\_pta grew the traced procedures onto any walkable F1 Setup spine, our max_paths 10 → 200 patch opened the response branches, and the mutation pass is seed-agnostic. For this one bug, discovery is carried by the pipeline, not the prompt — "the LLM found the bug" oversells the prompt, and C is the experiment that shows it. But forgiving is not the same as equivalent: C's seed carried a pseudo-transition ("time to wait") inside a message-type field, no IE lists at all, and a sent\_by / received\_by tag scheme the pipeline had to reinterpret. Nothing downstream could rely on a machine built like that — it just happened not to matter for a single-message bug. The moment the machine's meaning matters — auditing why a case exists, extending the campaign past CU07, comparing the model against the spec — the noob's model gives you nothing to audit.

4 — And A is the faithful choice for a reproduction. ARCANE published one seed model built from a neutral spec→FSM conversion. Framing A is the closest reconstruction of that, it produced the campaign's recorded run (case 31 — one unsolicited Response, exit 139), and its numbers are the ones every other framing is measured against on this page. The pipeline carried the discovery; framing A carries the evidence.