[flang][flang-rt] Infinite loop in SavedPosition::~SavedPosition when a child I/O statement's namelist look-ahead crosses a record boundary
- 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
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