Skip to content

Commit dd3fb6c

Browse files
authored
Merge pull request #1929 from hvitved/csharp/cfg/finally
C#: Fix CFG for nested `finally` blocks
2 parents fa91ecb + 11f9967 commit dd3fb6c

File tree

12 files changed

+8601
-7759
lines changed

12 files changed

+8601
-7759
lines changed

csharp/ql/src/semmle/code/csharp/controlflow/internal/Splitting.qll

Lines changed: 118 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,17 @@ private module Cached {
2727
cached
2828
newtype TSplitKind =
2929
TInitializerSplitKind() or
30-
TFinallySplitKind() or
30+
TFinallySplitKind(int nestLevel) { nestLevel = FinallySplitting::nestLevel(_) } or
3131
TExceptionHandlerSplitKind() or
3232
TBooleanSplitKind(BooleanSplitting::BooleanSplitSubKind kind) { kind.startsSplit(_) } or
3333
TLoopUnrollingSplitKind(LoopUnrollingSplitting::UnrollableLoopStmt loop)
3434

3535
cached
3636
newtype TSplit =
3737
TInitializerSplit(Constructor c) { InitializerSplitting::constructorInitializes(c, _) } or
38-
TFinallySplit(FinallySplitting::FinallySplitType type) or
38+
TFinallySplit(FinallySplitting::FinallySplitType type, int nestLevel) {
39+
nestLevel = FinallySplitting::nestLevel(_)
40+
} or
3941
TExceptionHandlerSplit(ExceptionClass ec) or
4042
TBooleanSplit(BooleanSplitting::BooleanSplitSubKind kind, boolean branch) {
4143
kind.startsSplit(_) and
@@ -427,23 +429,31 @@ module FinallySplitting {
427429
)
428430
}
429431

432+
/**
433+
* Holds if `innerTry` has a `finally` block and is immediately nested inside the
434+
* `finally` block of `outerTry`.
435+
*/
436+
private predicate nestedFinally(TryStmt outerTry, TryStmt innerTry) {
437+
exists(ControlFlowElement innerFinally |
438+
innerFinally = getAChild(getAFinallyDescendant(outerTry), outerTry.getEnclosingCallable()) and
439+
innerFinally = innerTry.getFinally()
440+
)
441+
}
442+
443+
/** Gets the nesting level of the `finally` block for `try`. */
444+
int nestLevel(TryStmt try) { result = strictcount(TryStmt outer | nestedFinally*(outer, try)) }
445+
430446
/** A control flow element that belongs to a `finally` block. */
431447
private class FinallyControlFlowElement extends ControlFlowElement {
432-
FinallyControlFlowElement() { this = getAFinallyDescendant(_) }
448+
private TryStmt try;
433449

434-
/** Holds if this node is the entry node in the `finally` block it belongs to. */
435-
predicate isEntryNode() {
436-
exists(TryStmt try | this = getAFinallyDescendant(try) | this = first(try.getFinally()))
437-
}
450+
FinallyControlFlowElement() { this = getAFinallyDescendant(try) }
438451

439-
/**
440-
* Holds if this node is a last element in the `finally` block belonging to
441-
* `try` statement `try`, with completion `c`.
442-
*/
443-
predicate isExitNode(TryStmt try, Completion c) {
444-
this = getAFinallyDescendant(try) and
445-
this = last(try.getFinally(), c)
446-
}
452+
/** Gets the immediate `try` block that this node belongs to. */
453+
TryStmt getTryStmt() { result = try }
454+
455+
/** Holds if this node is the entry node in the `finally` block it belongs to. */
456+
predicate isEntryNode() { this = first(try.getFinally()) }
447457
}
448458

449459
/** A control flow element that does not belong to a `finally` block. */
@@ -473,45 +483,60 @@ module FinallySplitting {
473483
*/
474484
class FinallySplitImpl extends SplitImpl, TFinallySplit {
475485
private FinallySplitType type;
486+
private int nestLevel;
476487

477-
FinallySplitImpl() { this = TFinallySplit(type) }
488+
FinallySplitImpl() { this = TFinallySplit(type, nestLevel) }
478489

479490
/**
480491
* Gets the type of this `finally` split, that is, how to continue execution after the
481492
* `finally` block.
482493
*/
483494
FinallySplitType getType() { result = type }
484495

496+
/** Gets the `finally` nesting level. */
497+
int getNestLevel() { result = nestLevel }
498+
485499
override string toString() {
486500
if type instanceof NormalSuccessor
487501
then result = ""
488-
else result = "finally: " + type.toString()
502+
else
503+
if nestLevel > 1
504+
then result = "finally(" + nestLevel + "): " + type.toString()
505+
else result = "finally: " + type.toString()
489506
}
490507
}
491508

492-
private class FinallySplitKind extends SplitKind, TFinallySplitKind {
493-
override int getListOrder() { result = InitializerSplitting::getNextListOrder() }
509+
private int getListOrder(FinallySplitKind kind) {
510+
result = InitializerSplitting::getNextListOrder() + kind.getNestLevel()
511+
}
494512

495-
override string toString() { result = "Finally" }
513+
int getNextListOrder() {
514+
result = max(int i | i = getListOrder(_) + 1 or i = InitializerSplitting::getNextListOrder())
496515
}
497516

498-
int getNextListOrder() { result = InitializerSplitting::getNextListOrder() + 1 }
517+
private class FinallySplitKind extends SplitKind, TFinallySplitKind {
518+
private int nestLevel;
519+
520+
FinallySplitKind() { this = TFinallySplitKind(nestLevel) }
521+
522+
/** Gets the `finally` nesting level. */
523+
int getNestLevel() { result = nestLevel }
524+
525+
override int getListOrder() { result = getListOrder(this) }
526+
527+
override string toString() { result = "Finally (" + nestLevel + ")" }
528+
}
499529

500530
private class FinallySplitInternal extends SplitInternal, FinallySplitImpl {
501-
override FinallySplitKind getKind() { any() }
531+
override FinallySplitKind getKind() { result.getNestLevel() = this.getNestLevel() }
502532

503533
override predicate hasEntry(ControlFlowElement pred, ControlFlowElement succ, Completion c) {
504-
succ.(FinallyControlFlowElement).isEntryNode() and
534+
succ = any(FinallyControlFlowElement entry |
535+
entry.isEntryNode() and
536+
this.getNestLevel() = nestLevel(entry.getTryStmt())
537+
) and
505538
succ = succ(pred, c) and
506-
this.getType().isSplitForEntryCompletion(c) and
507-
(
508-
// Abnormal entry must enter the correct splitting
509-
not c instanceof NormalCompletion
510-
or
511-
// Normal entry only when not entering a nested `finally` block; in that case,
512-
// the outer split must be maintained (see `hasSuccessor()`)
513-
pred instanceof NonFinallyControlFlowElement
514-
)
539+
this.getType().isSplitForEntryCompletion(c)
515540
}
516541

517542
override predicate hasEntry(Callable c, ControlFlowElement succ) { none() }
@@ -525,53 +550,90 @@ module FinallySplitting {
525550
(exists(succ(pred, _)) or exists(succExit(pred, _)))
526551
}
527552

528-
/** Holds if `pred` may exit this split with completion `c`. */
529-
private predicate exit(ControlFlowElement pred, Completion c) {
553+
/**
554+
* Holds if `pred` may exit this split with completion `c`. The Boolean
555+
* `inherited` indicates whether `c` is an inherited completion from a `try`/
556+
* `catch` block.
557+
*/
558+
private predicate exit(ControlFlowElement pred, Completion c, boolean inherited) {
530559
this.appliesToPredecessor(pred) and
531560
exists(TryStmt try, FinallySplitType type |
532561
type = this.getType() and
562+
nestLevel(try) = this.getNestLevel() and
533563
pred = last(try, c)
534564
|
535-
if pred.(FinallyControlFlowElement).isExitNode(try, c)
536-
then (
565+
if pred = last(try.getFinally(), c)
566+
then
537567
// Finally block can itself exit with completion `c`: either `c` must
538568
// match this split, `c` must be an abnormal completion, or this split
539569
// does not require another completion to be recovered
540-
type.matchesCompletion(c)
541-
or
542-
not c instanceof NormalCompletion
543-
or
544-
type instanceof NormalSuccessor
545-
) else (
546-
// Finally block can exit with completion `c` derived from try/catch
570+
inherited = false and
571+
(
572+
type.matchesCompletion(c)
573+
or
574+
not c instanceof NormalCompletion
575+
or
576+
type instanceof NormalSuccessor
577+
)
578+
else (
579+
// Finally block can exit with completion `c` inherited from try/catch
547580
// block: must match this split
581+
inherited = true and
548582
type.matchesCompletion(c) and
549583
not type instanceof NormalSuccessor
550584
)
551585
)
586+
or
587+
// If this split is normal, and an outer split can exit based on a inherited
588+
// completion, we need to exit this split as well. For example, in
589+
//
590+
// ```
591+
// bool done;
592+
// try
593+
// {
594+
// if (b1) throw new ExceptionA();
595+
// }
596+
// finally
597+
// {
598+
// try
599+
// {
600+
// if (b2) throw new ExceptionB();
601+
// }
602+
// finally
603+
// {
604+
// done = true;
605+
// }
606+
// }
607+
// ```
608+
//
609+
// if the outer split for `done = true` is `ExceptionA` and the inner split
610+
// is "normal" (corresponding to `b1 = true` and `b2 = false`), then the inner
611+
// split must be able to exit with an `ExceptionA` completion.
612+
this.appliesToPredecessor(pred) and
613+
exists(FinallySplitInternal outer |
614+
outer.getNestLevel() = this.getNestLevel() - 1 and
615+
outer.exit(pred, c, inherited) and
616+
this.getType() instanceof NormalSuccessor and
617+
inherited = true
618+
)
552619
}
553620

554621
override predicate hasExit(ControlFlowElement pred, ControlFlowElement succ, Completion c) {
555-
this.appliesToPredecessor(pred) and
556622
succ = succ(pred, c) and
557623
(
558-
// Entering a nested `finally` block abnormally means that we should exit this split
559-
succ.(FinallyControlFlowElement).isEntryNode() and
560-
not c instanceof NormalCompletion
561-
or
562-
exit(pred, c)
624+
exit(pred, c, _)
563625
or
564-
exit(pred, any(BreakCompletion bc)) and
626+
exit(pred, any(BreakCompletion bc), _) and
565627
c instanceof BreakNormalCompletion
566628
)
567629
}
568630

569631
override Callable hasExit(ControlFlowElement pred, Completion c) {
570632
result = succExit(pred, c) and
571633
(
572-
exit(pred, c)
634+
exit(pred, c, _)
573635
or
574-
exit(pred, any(BreakCompletion bc)) and
636+
exit(pred, any(BreakCompletion bc), _) and
575637
c instanceof BreakNormalCompletion
576638
)
577639
}
@@ -582,11 +644,11 @@ module FinallySplitting {
582644
succ = any(FinallyControlFlowElement fcfe |
583645
if fcfe.isEntryNode()
584646
then
585-
// Entering a nested `finally` block normally must remember the outer split
586-
c instanceof NormalCompletion
647+
// entering a nested `finally` block
648+
nestLevel(fcfe.getTryStmt()) > this.getNestLevel()
587649
else
588-
// Staying in the same `finally` block should maintain this split
589-
not this.hasEntry(pred, succ, c)
650+
// staying in the same (possibly nested) `finally` block as `pred`
651+
nestLevel(fcfe.getTryStmt()) >= this.getNestLevel()
590652
)
591653
}
592654
}

0 commit comments

Comments
 (0)