llvm / llvm/llvm-project

[Flang] Wrong codegen for type(c_ptr) bind(C) when integer(c_intptr_t) interface to same C function is declared first

Open
#192,655 1 comment 0 reactions 0 assignees View on GitHub
flang
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

*Context*: I was playing around trying to build the open source [ELPA](https://elpa.mpcdf.mpg.de) library (Eigenvalue SoLvers for Petaflop-Applications) using flang-new. I managed to do so with gfortran, ifx, ifort, and AMD's flang, but when using flang-new, I ran into a bunch of strange segfaults.
The following analysis was mostly done by Claude Opus, but for what it's worth, I think it merits an expert's look.

# The issue

When a module declares two `bind(C)` interfaces for the **same C function** with different first-argument types:

1. `integer(c_intptr_t) :: a` (declared **first**)
2. `type(c_ptr) :: a` (declared **second**)

flang-new applies the `integer(c_intptr_t)` calling convention to calls through the `type(c_ptr)` interface, adding a spurious pointer dereference (`mov rdi, [rdi]`). This passes the **value** of the `c_ptr` (e.g. `c_null_ptr` → `NULL`) to the C function instead of the **address** of the `c_ptr` variable.

**Godbolt (flang-20, `-O3 -fPIC`):** https://godbolt.org/z/Whz5nxPqW

## Minimal reproducer

### `bug_mod.f90` — Fortran module (paste into Godbolt)

```fortran
module bug_mod
use, intrinsic :: iso_c_binding
implicit none

! Interface 1: integer(c_intptr_t) first argument — declared FIRST
interface
function alloc_host_intptr_c(a, nbytes) result(istat) &
bind(C, name="allocHostFromC")
use, intrinsic :: iso_c_binding
implicit none
integer(kind=c_intptr_t) :: a
integer(kind=c_intptr_t), intent(in), value :: nbytes
integer(kind=c_int) :: istat
end function
end interface

! Interface 2: type(c_ptr) first argument — same C name, declared SECOND
interface
function alloc_host_cptr_c(a, nbytes) result(istat) &
bind(C, name="allocHostFromC")
use, intrinsic :: iso_c_binding
implicit none
type(c_ptr) :: a
integer(kind=c_intptr_t), intent(in), value :: nbytes
integer(kind=c_int) :: istat
end function
end interface

contains

function alloc_host_intptr(a, nbytes) result(success)
use, intrinsic :: iso_c_binding
implicit none
integer(kind=c_intptr_t) :: a
integer(kind=c_intptr_t), intent(in) :: nbytes
logical :: success
success = alloc_host_intptr_c(a, nbytes) /= 0
end function

function alloc_host_cptr(a, nbytes) result(success)
use, intrinsic :: iso_c_binding
implicit none
type(c_ptr) :: a
integer(kind=c_intptr_t), intent(in) :: nbytes
logical :: success
success = alloc_host_cptr_c(a, nbytes) /= 0
end function

end module
```

### `stubs.c` — C stub (for runtime verification)

```c
#include
#include
#include

int allocHostFromC(intptr_t *a, size_t nbytes) {
fprintf(stderr, "allocHostFromC: a=%p, nbytes=%zu\n", (void*)a, nbytes);
if (!a) { fprintf(stderr, " BUG: a is NULL!\n"); return 0; }
*a = (intptr_t)malloc(nbytes);
fprintf(stderr, " OK: *a=%p\n", (void*)*a);
return 1;
}
```

### `main.f90` — Test driver

```fortran
program test_bug
use bug_mod
use, intrinsic :: iso_c_binding
implicit none

type(c_ptr) :: ptr
logical :: ok

ptr = c_null_ptr
write(*,'(A)') "Calling alloc_host_cptr (type(c_ptr) interface)..."
ok = alloc_host_cptr(ptr, 256_c_intptr_t)
if (.not. ok) then
write(*,'(A)') "FAIL: C function received NULL (bug)"
error stop 1
end if
write(*,'(A)') "PASS"
end program
```

### Build & run

```bash
gcc -O2 -fPIC -c stubs.c -o stubs.o
flang-new -O3 -fPIC -shared -o libbug.so bug_mod.f90 stubs.o
flang-new -O3 -o test_bug main.f90 -L. -lbug -Wl,-rpath,. -no-pie
LD_LIBRARY_PATH=. ./test_bug
```

**Output (flang-new):**
```
Calling alloc_host_cptr (type(c_ptr) interface)...
allocHostFromC: a=(nil), nbytes=256
BUG: a is NULL!
FAIL: C function received NULL (bug)
```

**Output (gfortran / ifx):**
```
Calling alloc_host_cptr (type(c_ptr) interface)...
allocHostFromC: a=0x7ffd74594298, nbytes=256
OK: *a=0x...
PASS
```

## Expected codegen

`alloc_host_cptr` should pass `%rdi` (address of the `type(c_ptr)` variable) through to `allocHostFromC` unchanged:

```asm
; gfortran / ifx (CORRECT):
alloc_host_cptr:
push rax
mov rsi, qword ptr [rsi] ; nbytes: by-ref → by-value, correct
call allocHostFromC@PLT ; rdi unchanged = address of c_ptr variable
...
ret
```

## Actual codegen (flang-new)

```asm
; flang-new (BUG):
alloc_host_cptr:
push rax
mov rsi, qword ptr [rsi] ; nbytes: correct
mov rdi, qword ptr [rdi] ; ← BUG: dereferences c_ptr, passes VALUE not ADDRESS
call allocHostFromC@PLT ; rdi = c_null_ptr = 0 → NULL on C side
...
ret
```

## Affected versions

| Compiler | Version | Result |
|----------|---------|--------|
| flang-new **17** | 17.0.6 (Ubuntu) | **BUG** |
| flang-new **18** | 18.1.8 (apt.llvm.org) | **BUG** |
| flang-new **19** | 19.1.7 (apt.llvm.org) | **BUG** |
| flang-new **21** | 21.1.8 (apt.llvm.org) | **BUG** |
| flang-new **23** | 23.0.0 dev, Apr 3 2026 | **BUG** |
| AOCC flang 5.1 | AMD fork of LLVM 17 | OK |
| gfortran | 13.3.0 | OK |
| ifx | 2025.3.2 | OK |

The bug exists in all upstream LLVM flang releases tested (17 through 23-dev). AMD's AOCC 5.1 flang (proprietary fork of LLVM 17) generates correct code.

## Key observations

1. **Declaration order matters.** The bug triggers ONLY when the `integer(c_intptr_t)` interface is declared BEFORE the `type(c_ptr)` interface for the same C function name. Reversing the order produces correct code.

2. **Single interface is fine.** With only the `type(c_ptr)` interface (no intptr variant), codegen is correct.

3. **The warning is a clue.** flang-new emits `-Wexternal-interface-mismatch` for the dual interfaces, but the codegen is silently wrong — it applies the first-seen interface's argument-passing convention to both.

## Workaround

Route the `type(c_ptr)` wrapper through the `integer(c_intptr_t)` bind(C) interface using `transfer`:

```fortran
function alloc_host_cptr(a, nbytes) result(success)
use, intrinsic :: iso_c_binding
type(c_ptr) :: a
integer(kind=c_intptr_t), intent(in) :: nbytes
integer(kind=c_intptr_t) :: a_raw
logical :: success
success = alloc_host_intptr_c(a_raw, nbytes) /= 0
a = transfer(a_raw, a)
end function
```

Contributor guide

Open the contributing guide

Research direction

Start by building bug_mod.f90, stubs.c, and main.f90 with the provided flang-new commands, then compare code generation when the two interfaces are reordered or reduced to one. Trace how the first bind(C) declaration affects the type(c_ptr) call and verify completion when the generated call preserves the pointer address and the runtime test prints PASS.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, fortran
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.