llvm / llvm/llvm-project

[flang][flang-rt] Infinite loop in SavedPosition::~SavedPosition when a child I/O statement's namelist look-ahead crosses a record boundary

Open
#222,489 1 comment 0 reactions 0 assignees View on GitHub
flang:runtime hang
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

### Summary

A defined-input (DTIO) procedure that performs a list-directed child `READ` of an
undelimited `CHARACTER` value under a `NAMELIST` parent makes the flang runtime spin
forever at 100% CPU. The program never returns from the `READ`.

The loop is in `SavedPosition::~SavedPosition()`, which restores a look-ahead position by
calling `IoStatementState::BackspaceRecord()` until the record number is back where it
started. For a **child** I/O statement that call is a no-op, so the loop condition can
never become false.

This is a pre-existing defect and is independent of the `iotype` fix in #218314 — it
reproduces both before and after that PR.

### Reproducer

```fortran
module m
type t
character(4) :: c
contains
procedure :: rd
generic :: read(formatted) => rd
end type
contains
subroutine rd(dtv, unit, iotype, vlist, iostat, iomsg)
class(t), intent(inout) :: dtv
integer, intent(in) :: unit
character(*), intent(in) :: iotype
integer, intent(in) :: vlist(:)
integer, intent(out) :: iostat
character(*), intent(inout) :: iomsg
read (unit, *, iostat=iostat, iomsg=iomsg) dtv%c
end subroutine
end module

program main
use m
implicit none
type(t) :: obj
integer :: ios
character(200) :: msg
namelist /grp/ obj
obj%c = 'zzzz'
open (10, status='scratch')
write (10, '(A)') '&GRP OBJ= abcdefg'
write (10, '(A)') ' /'
rewind (10)
ios = 0
msg = ''
read (10, nml=grp, iostat=ios, iomsg=msg) ! never returns
close (10)
print *, 'iostat=', ios, ' c=[', obj%c, '] msg=', trim(msg)
end program
```

```console
$ flang -o repro repro.f90
$ ./repro # hangs; 100% CPU, must be killed
```

The value `abcdefg` is undelimited and longer than the `character(4)` component, so the
child read runs to the end of the record.

### Actual behavior

Infinite loop, `R` state at ~101% CPU. Backtrace (identical on every build tested):

```
#0 Fortran::runtime::io::IoStatementState::BackspaceRecord()
#1 Fortran::runtime::io::SavedPosition::~SavedPosition()
#2 Fortran::runtime::io::IsNamelistNameOrSlash(IoStatementState&)
#3 bool Fortran::runtime::io::EditCharacterInput(...)
#4 bool Fortran::runtime::io::descr::FormattedCharacterIO(...)
#5 DescriptorIoTicket::Begin(WorkQueue&)
#6 bool DescriptorIO(...)
#7 _FortranAioInputCharacter
#8
#9 Fortran::runtime::io::descr::DefinedFormattedIo(...)
#10 DescriptorIoTicket::Continue(WorkQueue&)
#11 bool DescriptorIO(...)
#12 _FortranAioInputNamelist
#13 main
```

### Expected behavior

The statement must terminate. gfortran 13.3.0 on the same source prints:

```
iostat= 0 c=[abcd] msg=
```

Whatever value or `IOSTAT` flang settles on, an unterminated spin in the runtime is not
an acceptable outcome.

### Root cause

`IsNamelistNameOrSlash()` opens a look-ahead scan guarded by a `SavedPosition`
([namelist.cpp#L656-L661](https://github.com/llvm/llvm-project/blob/ffde4cee43ed/flang-rt/lib/runtime/namelist.cpp#L656-L661)),
reached from the list-directed character reader
([edit-input.cpp#L1044](https://github.com/llvm/llvm-project/blob/ffde4cee43ed/flang-rt/lib/runtime/edit-input.cpp#L1044)).
The scan can advance past the end of the current record.

`SavedPosition`'s destructor then rewinds by backspacing
([connection.cpp#L22-L33](https://github.com/llvm/llvm-project/blob/ffde4cee43ed/flang-rt/lib/runtime/connection.cpp#L22-L33)):

```cpp
while (conn.currentRecordNumber > saved_.currentRecordNumber) {
io_.BackspaceRecord();
}
```

`IoStatementState::BackspaceRecord()` dispatches to the active statement state. Only
`InternalIoStatementState` and `ExternalIoStatementState` implement it
([io-stmt.cpp#L143](https://github.com/llvm/llvm-project/blob/ffde4cee43ed/flang-rt/lib/runtime/io-stmt.cpp#L143),
[#L473](https://github.com/llvm/llvm-project/blob/ffde4cee43ed/flang-rt/lib/runtime/io-stmt.cpp#L473));
`ChildIoStatementState`
([io-stmt.h#L687-L710](https://github.com/llvm/llvm-project/blob/ffde4cee43ed/flang-rt/include/flang-rt/runtime/io-stmt.h#L687-L710))
has no override, so it inherits the empty base implementation
([io-stmt.cpp#L43](https://github.com/llvm/llvm-project/blob/ffde4cee43ed/flang-rt/lib/runtime/io-stmt.cpp#L43)):

```cpp
void IoStatementBase::BackspaceRecord() {}
```

`currentRecordNumber` is therefore never decremented, the loop condition never becomes
false, and the destructor spins. Note that `ChildIoStatementState` *does* override
`HandleRelativePosition`/`HandleAbsolutePosition` — record backspacing is the one
position operation it leaves as the no-op.

The look-ahead is gated on `namelistGroup() != nullptr`, which is propagated to a direct
child of a list-directed or namelist parent, so a direct child statement reaches this path.

### Suggested fixes

1. Give `ChildIoStatementState` a `BackspaceRecord()` that forwards to the parent
statement's unit, or
2. make `SavedPosition`'s destructor stop (and report) when the position cannot be
restored — an unbounded `while` over a call that may be a no-op is fragile regardless
of this particular caller, or
3. do not enter the `SavedPosition` look-ahead from a child statement that cannot
backspace.

### Versions tested

x86_64 Linux, release builds with assertions. The reproducer hangs identically on all of
them, so this is long-standing rather than a recent regression:

- `llvm-project` main `5db1f5442ef0c9bcb2f4ca362361daaf2a76acfb` (2026-07-13)
- `llvm-project` main `62b7ae5dc327ba80b54e76a2964f34ff60f6ee91`
- `llvm-project` main `2c4c9d84bd948a92bb78b46a1c34044e246fd187` (77 commits after #218314
landed)

The two sites involved are unchanged at main `ffde4cee43ed`.

Contributor guide

Open the contributing guide

Research direction

Run the Fortran reproducer with flang, then read SavedPosition in flang-rt/lib/runtime/connection.cpp and the BackspaceRecord implementations in io-stmt.cpp and io-stmt.h. Trace the child path from namelist.cpp and edit-input.cpp to confirm why the record number cannot change. Done means the reproducer terminates without an infinite loop and the runtime handles this child look-ahead consistently.

Written by the indexing model from the issue text.

Assessment

Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.