[GH-ISSUE #2871] [Bug] Swift literal pairs at unresolved jumps leave dossiers looking complete #1015

Closed
opened 2026-10-15 18:30:22 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @thefolahan on GitHub (Oct 13, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2871

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Ghidra provider: Swift String literal decoding in analyze_function, and compare_functions.

Observed behavior

This follows up #2049, from the open review thread there (discussion_r4258738832).

The decoder treats an unconditional jump as a use site only when it has resolved targets and all of them leave the function. A computed br with no resolved target is skipped on purpose, because it can still dispatch inside the function. The skip leaves no structured trace, though: if that jump carries a constant String pair, the dossier still returns swift_string_literal_omissions: [].

swiftLiteralCoverage in src/domain/functionComparisonDimensions.ts then treats the dossier as complete. Comparing it with a build where the same jump resolves as a tail call can report strings_names as changed (a supposed absence) when the honest answer is unknown. The only signal today is free-text limitation wording, which comparison does not read.

Reproduction

An AArch64 function in a Swift program that loads a valid small literal into x0/x1 and ends in br x16, with no resolved target. analyze_function returns no literal and no omission for that site.

Expected behavior

A dossier that skipped a candidate pair at an unresolved jump should say so in structured form, so comparison treats its literal coverage as partial.

Impact and scope

Ghidra, AArch64 Swift programs only. Additive output. Only compare_functions verdicts change, from changed to unknown in this case.

Analysis or suspected cause

There are two possible shapes:

  1. A new omission reason. Add a record to swift_string_literal_omissions with reason: "unresolved_jump", alongside "capacity". It would carry the same fields a capacity omission has (representation, address, byte length, raw words, source_address and registers) and no value. It reuses the existing partial-coverage logic. Unlike capacity, decoding would not stop.
  2. A coverage flag. Add an optional swift_string_literal_coverage: "complete" | "partial" to the dossier, set to partial whenever any candidate is skipped for a reason other than invalid words. This is simpler for consumers, but it loses which site caused it.

I lean towards option 1, since comparison already understands omissions and it keeps the shape from #2049. I am happy to implement whichever shape you prefer, with a br x16 fixture in decoder-safeguards.S and a producer regression next to the ones in #2828.

Originally created by @thefolahan on GitHub (Oct 13, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2871 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Ghidra provider: Swift String literal decoding in `analyze_function`, and `compare_functions`. ### Observed behavior This follows up #2049, from the open review thread there ([discussion_r4258738832](https://github.com/morluto/rea/pull/2049#discussion_r4258738832)). The decoder treats an unconditional jump as a use site only when it has resolved targets and all of them leave the function. A computed `br` with no resolved target is skipped on purpose, because it can still dispatch inside the function. The skip leaves no structured trace, though: if that jump carries a constant String pair, the dossier still returns `swift_string_literal_omissions: []`. `swiftLiteralCoverage` in `src/domain/functionComparisonDimensions.ts` then treats the dossier as complete. Comparing it with a build where the same jump resolves as a tail call can report `strings_names` as **changed** (a supposed absence) when the honest answer is **unknown**. The only signal today is free-text limitation wording, which comparison does not read. ### Reproduction An AArch64 function in a Swift program that loads a valid small literal into x0/x1 and ends in `br x16`, with no resolved target. `analyze_function` returns no literal and no omission for that site. ### Expected behavior A dossier that skipped a candidate pair at an unresolved jump should say so in structured form, so comparison treats its literal coverage as partial. ### Impact and scope Ghidra, AArch64 Swift programs only. Additive output. Only `compare_functions` verdicts change, from changed to unknown in this case. ### Analysis or suspected cause There are two possible shapes: 1. **A new omission reason.** Add a record to `swift_string_literal_omissions` with `reason: "unresolved_jump"`, alongside `"capacity"`. It would carry the same fields a capacity omission has (representation, address, byte length, raw words, `source_address` and registers) and no value. It reuses the existing partial-coverage logic. Unlike capacity, decoding would not stop. 2. **A coverage flag.** Add an optional `swift_string_literal_coverage: "complete" | "partial"` to the dossier, set to partial whenever any candidate is skipped for a reason other than invalid words. This is simpler for consumers, but it loses which site caused it. I lean towards option 1, since comparison already understands omissions and it keeps the shape from #2049. I am happy to implement whichever shape you prefer, with a `br x16` fixture in `decoder-safeguards.S` and a producer regression next to the ones in #2828.
bsakaguchi 2026-10-15 18:30:22 +00:00
Author
Owner

@morluto commented on GitHub (Oct 14, 2026):

Resolved on main by d4bd65c53.

Unresolved Swift register-pair jumps now retain omission records and affect coverage. Current verification checks typed producer facts and their consumers; the original real-Ghidra target was not replayed.

Rechecked on main b8510ee00 in the current focused run: 17 test files passed, 485 tests passed and one platform-dependent test skipped. Relevant regression coverage: src/ghidra/GhidraFunctionValues.test.ts.

<!-- gh-comment-id:6175074127 --> @morluto commented on GitHub (Oct 14, 2026): Resolved on main by d4bd65c53. Unresolved Swift register-pair jumps now retain omission records and affect coverage. Current verification checks typed producer facts and their consumers; the original real-Ghidra target was not replayed. Rechecked on main b8510ee00 in the current focused run: 17 test files passed, 485 tests passed and one platform-dependent test skipped. Relevant regression coverage: src/ghidra/GhidraFunctionValues.test.ts.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1015
No description provided.