Skip to content

[core] region maker drops an else and runs the else-branch unconditionally (java/class input) #2909

Description

@obus-globus

Issue details

On the java/class input path, IfRegionMaker can drop an else and emit the else-branch plus the shared tail unconditionally. This is a miscompilation (wrong control flow), not just noisy output.

IfRegionMaker.process asks findOutBlock(then, else) for the block where the two branches converge; that block becomes the region exit and everything after it is emitted at the outer level. When a secondary break inside one branch creates an intermediate merge block, the "unique block in the intersection of the two branches' dominator frontiers" heuristic returns that intermediate block instead of the true post-dominator (the block that actually post-dominates both branches). The then region then swallows the real tail (duplicating it), the else region is emitted as a plain continuation so the else is lost, and the intermediate block plus tail are emitted after the if. The else-branch therefore runs unconditionally.

Sample

The trigger is a specific CFG (the else reached by fall-through, plus a secondary break in the then-branch). javac does not emit this shape, so the input is hand-written JVM bytecode. To reproduce, drop the raung below into jadx-core/src/test/raung and decompile (jadx's raung test harness reads it directly), or assemble it with raung-asm's executeForSingleClass and run jadx on the class.

.version 50
.class public super DropElse
.super java/lang/Object
.auto frames

.method public static f(ZZI)V
    iload 0
    ifne :NOPIC
    bipush 10
    istore 3
    iload 2
    istore 4
    iload 1
    ifeq :TAIL
    bipush 30
    istore 3
    goto :AFTER
  :NOPIC
    bipush 20
    istore 3
  :AFTER
    iconst_0
    istore 4
  :TAIL
    iload 4
    ifeq :T2
    iload 3
    invokestatic DropElse sink (I)V
    goto :T3
  :T2
    iload 3
    ineg
    invokestatic DropElse sink (I)V
  :T3
    iload 3
    invokestatic DropElse sink (I)V
    return
.end method

.method public static sink(I)V
    return
.end method

Intended control flow: exactly one of the two assignments runs, then the shared tail once. CFR 0.152 recovers it correctly with labeled breaks:

public static void f(boolean bl, boolean bl2, int n) {
    int n2;
    int n3;
    block3: {
        block4: {
            block2: {
                if (bl) break block2;
                n3 = 10;
                n2 = n;
                if (!bl2) break block3;
                n3 = 30;
                break block4;
            }
            n3 = 20;
        }
        n2 = 0;
    }
    if (n2 != 0) {
        DropElse.sink(n3);
    } else {
        DropElse.sink(-n3);
    }
    DropElse.sink(n3);
}

Current output

master emits (note the two Code duplicated warnings; i2 = 20/i3 = 0 is the else/nopic branch, now unconditional, and the tail is duplicated):

public static void f(boolean z, boolean z2, int i) {
    int i2;
    int i3;
    if (!z) {
        i2 = 10;
        i3 = i;
        if (z2) {
            i2 = 30;
        }
        if (i3 != 0) {
            sink(i2);
        } else {
            sink(-i2);
        }
        sink(i2);
    }
    i2 = 20;
    i3 = 0;
    if (i3 != 0) {
        sink(i2);
    } else {
        sink(-i2);
    }
    sink(i2);
}

The else/nopic branch runs unconditionally after the real branch's tail. So f(false, false, 5), which should call sink twice (sink(10), sink(10)), calls it four times and ends with o == 20 on master.

Jadx version

master ada2a85e

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions