fix(cfg): wire Swift multi-catch throw edges to every handler (#2195)

visitDo routed the protected body's throw edge only to handlerEntries[0], so a
do { try r() } catch A {} catch {} left the 2nd..Nth catch handlers UNREACHABLE
from ENTRY — orphaned blocks whose error bindings + def/use facts were stranded
in a dead component (a soundness gap for idiomatic Swift typed multi-catch).
Swift tries the catch clauses in order and the thrown type is unknown at CFG
time, so every protected block may reach ANY clause: edge each protected block
to every handlerEntry. Found by the per-language CFG/CDG verification swarm
(reproduced: 2-catch=1, 3-catch=3 unreachable blocks). swift suite 26 passed,
tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-14 21:19:01 +00:00
parent c97d1b2dfc
commit 5d284b8f5f
2 changed files with 18 additions and 3 deletions

View file

@ -740,10 +740,14 @@ class SwiftCfgWalk {
const bodyRes = bodyNode ? this.visitSeq(this.statementsOf(bodyNode)) : null;
this.handlers.pop();
// Conservative exceptional edges: every protected-region block → the handler.
if (catchBlocks.length > 0) {
// Conservative exceptional edges: every protected-region block → EACH catch
// handler. Swift tries the catch clauses in order until one matches; the
// thrown type is unknown at CFG time, so any protected block may reach ANY
// clause. Edging only the first handler orphaned the 2nd..Nth catch blocks
// (unreachable from ENTRY, stranding their error bindings + def/use facts).
if (handlerEntries.length > 0) {
for (let b = protectedStart; b < this.builder.blockCount; b++) {
this.builder.edge(b, doHandler, 'throw');
for (const handler of handlerEntries) this.builder.edge(b, handler, 'throw');
}
}

View file

@ -238,6 +238,17 @@ describe('Swift CfgVisitor — do/catch (error handling)', () => {
expect(reaches(cfg, thr, block(cfg, 'done()'))).toBe(false);
expect(reachable(cfg, block(cfg, 'done()'))).toBe(true); // via the if false branch
});
it('multi-catch: EVERY catch handler is reachable from ENTRY (#2195)', () => {
// The protected body can throw an error matching ANY clause, so the 2nd..Nth
// catch must not be orphaned — the bug routed the throw edge only to the
// first handler, leaving later handlers unreachable from ENTRY.
const cfg = swift.cfgOf(`func f() { do { try r() } catch A { ha() } catch { hb() } }`);
expect(reachable(cfg, block(cfg, 'ha()'))).toBe(true);
expect(reachable(cfg, block(cfg, 'hb()'))).toBe(true);
// the protected `try r()` reaches both handlers.
expect(reaches(cfg, block(cfg, 'try r()'), block(cfg, 'hb()'))).toBe(true);
});
});
describe('Swift CfgVisitor — defer (LIFO scope-exit)', () => {