Issue 208943
Summary hexagon miscompile bn_mp_div_2d.c at -O2
Labels backend:Hexagon
Assignees iajbar
Reporter androm3da
    # Hexagon Clang 22.1.8 Miscompilation: bn_mp_div_2d.c at -O2

## Summary

Hexagon clang 22.1.8 miscompiles `bn_mp_div_2d.c` (from libtommath, bundled with dropbear-2026.91) at optimization level `-O2` and `-O3`. The carry propagation in the right-shift loop is broken: when processing 3+ digits, the carry from digit N-2 is incorrectly used instead of the carry from digit N-1.

## Compiler Version

```
clang version 22.1.8
Target: hexagon-unknown-linux-musl
Thread model: posix
InstalledDir: /local/mnt/workspace/upstream/buildroot/output/host/opt/ext-toolchain/x86_64-linux-gnu/bin
Build config: +assertions
```

## Source File

`bn_mp_div_2d.c` from libtommath (part of dropbear-2026.91). The preprocessed source is provided in `bn_mp_div_2d.i`.

The relevant function:

```c
mp_err mp_div_2d(const mp_int *a, int b, mp_int *c, mp_int *d) {
    mp_digit D, r, rr;
    int x;
    mp_err err;
    if (b <= 0) { return mp_copy(a, c); }
    if ((err = mp_copy(a, c)) != MP_OKAY) { return err; }
    if (b >= MP_DIGIT_BIT) { mp_rshd(c, b / MP_DIGIT_BIT); }
    D = (mp_digit)(b % MP_DIGIT_BIT);
 if (D != 0u) {
        mp_digit *tmpc, mask, shift;
        mask = ((mp_digit)1 << D) - 1uL;
        shift = (mp_digit)MP_DIGIT_BIT - D;
 tmpc = c->dp + (c->used - 1);
        r = 0;
        for (x = c->used - 1; x >= 0; x--) {
            rr = *tmpc & mask;
            *tmpc = (*tmpc >> D) | (r << shift);
            --tmpc;
            r = rr;
        }
 }
    mp_clamp(c);
    return MP_OKAY;
}
```

Where `MP_DIGIT_BIT = 28`, `mp_digit = uint32_t`.

## Reproduction

### Compile commands

```sh
# Produces WRONG results:
hexagon-unknown-linux-musl-clang -mv68 -O2 -c bn_mp_div_2d.c -o bn_mp_div_2d.O2.o

# Produces CORRECT results:
hexagon-unknown-linux-musl-clang -mv68 -O0 -c bn_mp_div_2d.c -o bn_mp_div_2d.O0.o
hexagon-unknown-linux-musl-clang -mv68 -O1 -c bn_mp_div_2d.c -o bn_mp_div_2d.O1.o
hexagon-unknown-linux-musl-clang -mv68 -Os -c bn_mp_div_2d.c -o bn_mp_div_2d.Os.o
```

### Test case

See `test_reproducer.c`. Link against the -O2 compiled object:

```sh
hexagon-unknown-linux-musl-clang -mv68 -O0 -static test_reproducer.c bn_mp_div_2d.O2.o -o test
qemu-hexagon ./test
```

### Input

```
mp_int with:
  used = 3
  dp[0] = 0x0E12C420
  dp[1] = 0x0B17D1F2
  dp[2] = 0x00000006

Call: mp_div_2d(&a, 4, &c, NULL) (right-shift by 4 bits)
```

### Expected output

```
c.dp[0] = 0x02E12C42    (carry of 0x2 from dp[1])
c.dp[1] = 0x06B17D1F    (carry of 0x6 from dp[2])
c.dp[2] = 0x00000000
```

### Actual output (with -O2)

```
c.dp[0] = 0x06E12C42    (WRONG: carry of 0x6 from dp[2], skipping dp[1])
c.dp[1] = 0x06B17D1F    (correct)
c.dp[2] = 0x00000000 (correct)
```

## Root Cause Analysis

At -O2, the compiler unrolls the loop by 8 and generates an epilogue loop for the
remainder. The epilogue has this structure:

```asm
.LBB0_14:                    // epilogue entry
 p0 = cmp.gtu(r3,#1)     // r3 = remaining count (3 in our case)
    r7 = add(r3,#-1)        // loop trip count = 2
    r6 = memw(r1+#0)        // load highest digit

    loop0(.LBB0_16,r7)      // hardware loop, 2 iterations
    r3 = lsr(r6,r17)        // shift highest digit
    r5 = and(r6,r2)         // extract carry from highest digit (=0x6)
    if (!p0) jump .LBB0_18

    r6 = r5                 // r6 = carry

.LBB0_16: // loop body
    { r3 |= asl(r4,r0)      // combine incoming carry with shifted value
      r4 = r6               // save current carry for next iteration
      r7 = memw(r1+#-4)     // load next digit
 memw(r1++#-4) = r3.new }  // store result
    { r3 = lsr(r7,r17)      // shift newly loaded digit
      r6 = and(r7,r2) }     // extract carry
 :endloop0

// %bb.17:                   // BUG IS HERE
    r4 = r5 // WRONG: overwrites r4 (carry from dp[1] = 0x2)
 // with r5 (carry from dp[2] = 0x6)

.LBB0_18: // final store
    { r3 |= asl(r4,r0)      // uses wrong carry!
 memw(r1++#-4) = r3.new }
```

The `r4 = r5` instruction after the loop is incorrect. At this point:
- `r4` contains the correct carry from the last loop iteration (0x2, from dp[1])
- `r5` contains the carry extracted from the FIRST element (dp[2]) during setup (0x6)

The compiler's code generation is wrong: it should NOT restore r5 into r4 after the
loop when the loop executed 2+ times. This instruction appears to be dead code from
the single-iteration case (where r5 IS the correct carry) that was incorrectly left
in the multi-iteration path.

- `bn_mp_div_2d.i` - Preprocessed source - [bn_mp_div_2d.i.txt](https://github.com/user-attachments/files/29929842/bn_mp_div_2d.i.txt)
- `test_reproducer.c` - Standalone test harness [test_reproducer.c](https://github.com/user-attachments/files/29929841/test_reproducer.c)

## Reproduced on main / regression status

Also seen on clang built from upstream `main`  5302608fec8ed7d8ce300d4a0d599a26b24dc972).

**This is not a regression** -- the miscompilation has been present since at least clang 16, and is still present on current `main`.

## Flags to reproduce

```
hexagon-unknown-linux-musl-clang \
    -mv68 -O2 \
 -DLTM_NOTHING \
    -DMP_28BIT \
    -I. \
    -c bn_mp_div_2d.c \
 -o bn_mp_div_2d.O2.o
```

_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs

Reply via email to