[Flang] Wrong codegen for type(c_ptr) bind(C) when integer(c_intptr_t) interface to same C function is declared first
- 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
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