llvm / llvm/llvm-project

[flang] stack overflow in _FortranAAssign for LHS-aliased deferred-length allocatable character concat in a loop

Open
#194,828 0 comments 0 reactions 0 assignees View on GitHub
flang
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

**AI Disclosure: I used AI to help isolate and investigate this bug and draft this bug report. I can attest to the accuracy of findings about what code works with what compiler. I also have relatively strong confidence in the attribution of the issue to `_FortranAAassign` and the other diagnostic details.**

Flang generates code for an LHS-aliased deferred-length allocatable character concat-with-self inside a loop that allocates the expression temporary on the stack via `alloca` but does not release that temporary until the enclosing subroutine returns. With the default 8 MB Linux thread stack, the cumulative consumption (which grows quadratically as `N(N+1)/2`) exhausts the stack at roughly N=4078 iterations, producing a SIGSEGV inside `_FortranAAssign`.

`gfortran` 14, `gfortran` 15, and `ifx` 2025.2.2 compile and run the same code without symptom.

### Versions

Reproduces on:

```
flang version 20.1.8 (Debian trixie, /usr/bin/flang)
flang version 21.1.8 (apt.llvm.org llvm-toolchain-trixie-21, snapshot ++20251221033036+2078da43e25a)
flang version 22.1.2 (apt.llvm.org llvm-toolchain-trixie-22, snapshot ++20260313123427+6121df77a781)
flang version 22.1.4 (Homebrew, macOS)
flang version 23.0.0 (apt.llvm.org llvm-toolchain-trixie main snapshot ++20260318082838+fce100e26e7e — near main HEAD)
```

Accepted by:

```
$ gfortran --version | head -1
GNU Fortran (Debian 14.2.0-19) 14.2.0

$ ifx --version | head -1
ifx (IFX) 2025.2.2 20251210
```

### Minimal reproducer

```fortran
subroutine grow_string(n)
integer, intent(in) :: n
character(len=:), allocatable :: s
character(len=1) :: c
integer :: i
allocate(character(len=0) :: s)
do i = 1, n
c = char(mod(i, 26) + ichar("a"))
select case (c)
case ("z")
s = s // c
case default
s = s // c
end select
end do
print *, "len:", len(s)
end subroutine

program p
call grow_string(5000)
end program
```

### Observed

```
$ flang -g -std=f2018 repro_minimal.f90 -o t && ./t
Segmentation fault (core dumped)
```

### Expected

```
$ gfortran -g -std=f2018 repro_minimal.f90 -o t && ./t
len: 5000
$ ifx -g -stand f18 repro_minimal.f90 -o t && ./t
len: 5000
```

### Diagnosis

valgrind on the crashing binary reports a stack overflow inside `_FortranAAssign`:

```
==xxxxx== Stack overflow in thread #1: can't grow stack to 0x1ffe801000
==xxxxx==
==xxxxx== Process terminating with default action of signal 11 (SIGSEGV)
==xxxxx== Access not within mapped region at address 0x1FFE801CA0
==xxxxx== at _FortranAAssign
==xxxxx== The main thread stack size used in this run was 8388608.
==xxxxx==
==xxxxx== HEAP SUMMARY:
==xxxxx== in use at exit: 150,763 bytes in 12 blocks
==xxxxx== total heap usage: 4,090 allocs, 4,078 frees, 8,459,691 bytes allocated
==xxxxx== ERROR SUMMARY: 0 errors from 0 contexts
```

The heap is clean; this is a stack-only failure.

For `s = s // c` where `s` is `character(len=:), allocatable` and appears on both sides of the assignment, flang allocates the expression temporary on the stack via `alloca`. The alloca'd buffers are not released between iterations — the lifetime extends to the end of `grow_string`. Cumulative stack consumption is therefore Σ k for k=1..N, which exceeds the default 8 MB thread stack at N≈4078:

```
Σ k for k=1..4078 = 4078*4079/2 = 8,317,081 bytes ≈ 8 MB
```

The `8,459,691 bytes allocated` line in the valgrind heap summary is the heap activity (each iteration also reallocates the LHS proper on the heap and frees the old buffer, hence the matched 4078 alloc/free pairs); it is unrelated to the stack-side leak. The 8 MB stack figure and the 4078-iteration crash threshold match precisely.

### Trigger / minimization

- The loop must be inside a subroutine or function — putting the same loop directly in a `program` block does not crash, presumably because flang lays out the loop temporaries differently or the codegen path is different.
- The `select case` block inside the loop is required to reproduce. A plain `s = s // c` body without the `select case` does **not** crash even at n=5000 (cumulative ~12.5 MB if alloca'd, suggesting the codegen for that simpler shape does free between iterations).
- The crash threshold tracks the stack ulimit. With `ulimit -s 16384` (16 MB), N=5000 succeeds and the threshold rises to ~5800. With `ulimit -s 4096` (4 MB), the threshold drops to ~2880.
- The bug does not require nested subroutine calls; a single-frame loop is enough.

### Workarounds

For source code that cannot wait for a fix:

1. **Wrap each LHS-aliased concat in a `block ... end block` construct.** The block scope gives flang a per-iteration anchor at which to release the alloca'd expression temporary. This is the smallest source-level change and is the workaround we deployed in the rojff JSON parser library where we first encountered the bug:

```fortran
block; s = s // c; end block
```

Tested at n=5,000,000 without crash on flang 22.1.2.

2. Pre-allocate a fixed-size buffer with explicit length tracking; double the buffer when full. Avoids the LHS-aliased concat entirely. More invasive.

The following are **not** valid workarounds:

- `tmp = s // c; call move_alloc(tmp, s)` (allocatable tmp + `move_alloc`) — flang still alloca's the expression `s // c` itself before assigning to `tmp`:

```fortran
subroutine grow_string(n)
integer, intent(in) :: n
character(len=:), allocatable :: s, tmp
character(len=1) :: c
integer :: i
allocate(character(len=0) :: s)
do i = 1, n
c = char(mod(i, 26) + ichar("a"))
select case (c)
case ("z")
tmp = s // c
call move_alloc(tmp, s)
case default
tmp = s // c
call move_alloc(tmp, s)
end select
end do
print *, "len:", len(s)
end subroutine
program p
call grow_string(5000)
end program
```

Crashes at n=5000 (same threshold as `repro_minimal.f90`).

- `s = (s) // c` (parenthesizing the LHS reference) — also still crashes.

### Real-world context

This was first encountered in the rojff JSON parser (https://gitlab.com/everythingfunctional/rojff) inside `parse_json_string`, where the parser builds a string character-by-character. Strings longer than ~4078 characters in input JSON cause a SIGSEGV when the library is built with flang. The rojff CI does not run flang, so the bug went undetected upstream. A workaround MR using the block construct is in flight: https://gitlab.com/everythingfunctional/rojff/-/merge_requests/8.

### Notes

- I have not investigated whether the same alloca-in-loop pattern affects deferred-length allocatable arrays (`integer, allocatable :: a(:); a = [a, x]`) or only the character-concat case.
- The codegen difference between "subroutine body with select case" and "program body without select case" suggests the bug is in a specific lowering path, possibly the one that handles `select case` arms uniformly.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.