Skip to content

Rellic produces semantically non-equivalent code #325

Description

@vmihalko
Used version:

0.2.3. (current)

Reproducer:
#include <stdio.h>
int main(int argc, const char** argv) {
	int i = argc;
	if ( i == 1 ) { // no args
		putchar(4 + '0');
		i += 1;
		goto n5;	
	}
	if ( i > 1 ) { // args
		putchar( 5 + '0');
		goto n7;
	}
n5:
	putchar( 6 + '0');
n7:
	putchar( 7 + '0');
}
Steps to reproduce:
clang error.c -o error
clang error.c -S -emit-llvm -o error.ll
rellic-decomp --input error.ll --output de-error.c
clang de-error.c -o de-error
Expected behavior:

Executing error results in: "467"

Current behavior:

But executing de-error results in: "47"

Rellics output:
int main(int arg0, char **arg1);
unsigned int putchar(unsigned int arg0);
int main(int arg0, char **arg1) {
    unsigned int var0;
    unsigned int var1;
    char **var2;
    unsigned int var3;
    var0 = 0U;
    var1 = arg0;
    var2 = arg1;
    var3 = var1;
    if (var3 != 1U && (int)var3 > 1) {
        putchar(53U);
    }
    if (var3 == 1U) {
        putchar(52U);
        var3 = var3 + 1U;
    }
    if (var3 == 1U || (int)var3 <= 1) {
        putchar(54U);
    }
    putchar(55U);
    return var0;
}

Activity

  1. frabert commented on May 19, 2023

    @frabert
    Collaborator

    Can reproduce 👍

  2. added a commit that references this issue on May 19, 2023
  3. humdrum00001010 commented on Oct 5, 2026

    @humdrum00001010

    A standalone example of the load-snapshot problem in this issue:

    declare void @helper()
    define i32 @snapshot(ptr %p) {
    entry:
      %saved = load i32, ptr %p
      store i32 0, ptr %p
      %condition = icmp ne i32 %saved, 0
      br i1 %condition, label %call, label %exit
    call:
      call void @helper()
      br label %exit
    exit:
      %result = phi i32 [1, %call], [0, %entry]
      ret i32 %result
    }

    With v0.2.3 (0cccc98a78d985bd0d1343f82b123dda03f71fcb) and LLVM 15:

    rellic-decomp --input repro.ll --output repro.c

    The generated body stores zero first, then tests a newly loaded *p, rather than %saved:

    *(unsigned int *)p = 0U;
    if (*(unsigned int *)p != 0U) {
        helper();
    }

    A nonzero initial value should call helper and return 1. The generated C takes the other path. This demonstrates the timing issue without a larger input. Current-master load materialization still excludes single-use non-call loads by source inspection; I have not executed a current-master build. Related fix: #326.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions