[llvm] [flang-rt] Fix BACKSPACE crash on files without trailing newline (PR #227661)

Kaviya Rajendiran via llvm-commits llvm-commits at lists.llvm.org
Wed Sep 30 04:22:32 PDT 2026


https://github.com/kaviya2510 created https://github.com/llvm/llvm-project/pull/227661

Calling BACKSPACE on a formatted sequential file crashes with `error: "Sequential record missing its terminator"` when the last record does not end with '\n'.

#### Root cause:
`BackspaceVariableFormattedRecord(...)` assumed that every record ends with a newline character`(\n)`. To locate the previous record, it examines the byte immediately before the current file position and expects that byte to be `'\n'`, then scans backward to find the start of the record. When the final record is unterminated, the byte before the current position belongs to the record data rather than a record terminator, it reports `IostatMissingTerminator` error.

#### Fix:
Updated `BackspaceVariableFormattedRecord(...)` to recognize end-of-file as a valid record terminator, matching the behavior of `BeginVariableFormattedInputRecord(...)`. This allows `BACKSPACE(...)` to correctly handle an unterminated final record.

Assisted by: Cursor
Fixes https://github.com/llvm/llvm-project/issues/227630

>From 9f4ce0c0cef9c8504e600a435dffef9bbbedaf69 Mon Sep 17 00:00:00 2001
From: Kaviya Rajendiran <kaviyara2000 at gmail.com>
Date: Wed, 30 Sep 2026 16:32:05 +0530
Subject: [PATCH] [flang-rt] Fix BACKSPACE crash on files without trailing
 newline

---
 flang-rt/lib/runtime/unit.cpp                 | 35 +++++++++-----
 .../test/Driver/backspace-unterminated.f90    | 47 +++++++++++++++++++
 2 files changed, 70 insertions(+), 12 deletions(-)
 create mode 100644 flang-rt/test/Driver/backspace-unterminated.f90

diff --git a/flang-rt/lib/runtime/unit.cpp b/flang-rt/lib/runtime/unit.cpp
index cbf802d3b8225..894a9458fd32a 100644
--- a/flang-rt/lib/runtime/unit.cpp
+++ b/flang-rt/lib/runtime/unit.cpp
@@ -762,39 +762,50 @@ static RT_API_ATTRS const char *FindLastNewline(
 void ExternalFileUnit::BackspaceVariableFormattedRecord(
     IoErrorHandler &handler) {
   // File offset of previous record's newline
-  auto prevNL{
+  auto lastByte{
       frameOffsetInFile_ + static_cast<std::int64_t>(recordOffsetInFrame_) - 1};
-  if (prevNL < 0) {
+  if (lastByte < 0) {
     handler.SignalError(IostatBackspaceAtFirstRecord);
     return;
   }
+  if (!(lastByte >= FrameAt() &&
+          lastByte - FrameAt() < static_cast<std::int64_t>(FrameLength()))) {
+    frameOffsetInFile_ = std::max<std::int64_t>(0, lastByte + 1 - 1024);
+    auto need{static_cast<std::size_t>(lastByte + 1 - frameOffsetInFile_)};
+    if (ReadFrame(frameOffsetInFile_, need, handler) < need) {
+      handler.SignalError(IostatShortRead);
+      return;
+    }
+  }
+  bool terminated{Frame()[lastByte - FrameAt()] == '\n'};
+  if (!terminated && knownSize().value_or(lastByte + 1) != lastByte + 1) {
+    handler.SignalError(IostatMissingTerminator);
+    return;
+  }
+  auto recordEnd{terminated ? lastByte : lastByte + 1};
   while (true) {
-    if (frameOffsetInFile_ < prevNL) {
+    if (frameOffsetInFile_ < recordEnd) {
       if (const char *p{
-              FindLastNewline(Frame(), prevNL - 1 - frameOffsetInFile_)}) {
+              FindLastNewline(Frame(), recordEnd - 1 - frameOffsetInFile_)}) {
         recordOffsetInFrame_ = p - Frame() + 1;
-        recordLength = prevNL - (frameOffsetInFile_ + recordOffsetInFrame_);
+        recordLength = recordEnd - (frameOffsetInFile_ + recordOffsetInFrame_);
         break;
       }
     }
     if (frameOffsetInFile_ == 0) {
       recordOffsetInFrame_ = 0;
-      recordLength = prevNL;
+      recordLength = recordEnd;
       break;
     }
     frameOffsetInFile_ -= std::min<std::int64_t>(frameOffsetInFile_, 1024);
-    auto need{static_cast<std::size_t>(prevNL + 1 - frameOffsetInFile_)};
+    auto need{static_cast<std::size_t>(lastByte + 1 - frameOffsetInFile_)};
     auto got{ReadFrame(frameOffsetInFile_, need, handler)};
     if (got < need) {
       handler.SignalError(IostatShortRead);
       return;
     }
   }
-  if (Frame()[recordOffsetInFrame_ + *recordLength] != '\n') {
-    handler.SignalError(IostatMissingTerminator);
-    return;
-  }
-  if (*recordLength > 0 &&
+  if (terminated && *recordLength > 0 &&
       Frame()[recordOffsetInFrame_ + *recordLength - 1] == '\r') {
     --*recordLength;
   }
diff --git a/flang-rt/test/Driver/backspace-unterminated.f90 b/flang-rt/test/Driver/backspace-unterminated.f90
new file mode 100644
index 0000000000000..ca468ce7f9a6b
--- /dev/null
+++ b/flang-rt/test/Driver/backspace-unterminated.f90
@@ -0,0 +1,47 @@
+! The last record of a formatted sequential file need not be terminated
+! by a newline; BACKSPACE must be able to reposition over such a record.
+
+! RUN: %flang %isysroot -L"%libdir" %s -o %t
+! RUN: env LD_LIBRARY_PATH="$LD_LIBRARY_PATH:%libdir" %t | FileCheck %s
+
+! CHECK: PASS: unterm1
+! CHECK: PASS: unterm2
+
+program backspace_unterminated
+  implicit none
+  call check('unterm1', 'ABCDEFGHIJ', 1)
+  call check('unterm2', 'AAA' // new_line('a') // 'BBB', 2)
+  print *, 'PASS'
+contains
+  subroutine check(name, contents, nrecs)
+    character(*), intent(in) :: name, contents
+    integer, intent(in) :: nrecs
+    character(len=32) :: buf, again
+    integer :: iu, i, stat
+
+    open (newunit=iu, file=name, status='replace', action='write', &
+        form='unformatted', access='stream')
+    write (iu) contents
+    close (iu)
+
+    open (newunit=iu, file=name, status='old', action='read', &
+        form='formatted', access='sequential')
+    do i = 1, nrecs
+      read (iu, '(A)') buf
+    end do
+    backspace (iu, iostat=stat)
+    if (stat /= 0) then
+      print *, 'FAIL: ' // name // ': backspace failed'
+      stop 1
+    end if
+    read (iu, '(A)') again
+    if (again /= buf) then
+      print *, 'FAIL: ' // name // ': reread mismatch'
+      stop 1
+    end if
+    print *, 'PASS: ' // name
+    close (iu, status='delete')
+  end subroutine
+end program
+
+! CHECK: PASS



More information about the llvm-commits mailing list