[flang] stack overflow in _FortranAAssign for LHS-aliased deferred-length allocatable character concat in a loop
- 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
Assessment
This issue has not been assessed yet.