[flang-commits] [flang] [llvm] [flang-rt] Honor TMPDIR/TMP/TEMP/TEMPDIR for OPEN(STATUS='SCRATCH') (PR #199517)

Eugene Epshteyn via flang-commits flang-commits at lists.llvm.org
Mon May 25 05:03:39 PDT 2026


https://github.com/eugeneepshteyn updated https://github.com/llvm/llvm-project/pull/199517

>From 0a589aa0e86d4e073cbd8570ad4eaa6398d001c8 Mon Sep 17 00:00:00 2001
From: Eugene Epshteyn <eepshteyn at nvidia.com>
Date: Sun, 24 May 2026 22:58:18 -0400
Subject: [PATCH 1/2] [flang-rt] Honor TMPDIR/TMP/TEMP/TEMPDIR for
 OPEN(STATUS='SCRATCH')

The POSIX branch of openfile_mkstemp() hardcoded the scratch directory
to /tmp, ignoring user-specified temp locations. This breaks deployments
where /tmp is small, noexec, missing (some containers/chroots), or on a
different filesystem than the user's chosen scratch space, and it
diverges from gfortran and the rest of the LLVM toolchain.

Add a small file-local GetSystemTempDir() helper that mirrors the lookup
chain used by llvm::sys::path::system_temp_directory on Unix:

  TMPDIR -> TMP -> TEMP -> TEMPDIR -> P_tmpdir -> /tmp

Windows behavior is unchanged - GetTempPathA already consults the
relevant environment variables.

Document the new lookup chain in flang/docs/RuntimeEnvironment.md
alongside the other variables consulted by the runtime.

Assisted-by: AI
---
 flang-rt/lib/runtime/file.cpp                 |  44 ++++-
 flang-rt/unittests/Runtime/CMakeLists.txt     |   1 +
 flang-rt/unittests/Runtime/ScratchTempDir.cpp | 165 ++++++++++++++++++
 flang/docs/RuntimeEnvironment.md              |  17 ++
 4 files changed, 223 insertions(+), 4 deletions(-)
 create mode 100644 flang-rt/unittests/Runtime/ScratchTempDir.cpp

diff --git a/flang-rt/lib/runtime/file.cpp b/flang-rt/lib/runtime/file.cpp
index 22cd57662b8ce..e050edd98bf9e 100644
--- a/flang-rt/lib/runtime/file.cpp
+++ b/flang-rt/lib/runtime/file.cpp
@@ -12,9 +12,10 @@
 #include "flang/Runtime/magic-numbers.h"
 #include <algorithm>
 #include <cerrno>
+#include <cstdio>
+#include <cstdlib>
 #include <cstring>
 #include <fcntl.h>
-#include <stdlib.h>
 #include <sys/stat.h>
 #ifdef _WIN32
 #include "flang/Common/windows-include.h"
@@ -30,6 +31,29 @@ void OpenFile::set_path(OwningPtr<char> &&path, std::size_t bytes) {
   pathLength_ = bytes;
 }
 
+#ifndef _WIN32
+// Mirrors the lookup chain used by llvm::sys::path::system_temp_directory on
+// Unix: first the conventional environment variables, then P_tmpdir if the
+// platform defines one, then /tmp as the final fallback. The returned pointer
+// is owned by the environment or points to a string literal; the caller must
+// copy before mutating.
+static const char *GetSystemTempDir() {
+  static const char *const envVars[]{"TMPDIR", "TMP", "TEMP", "TEMPDIR"};
+  for (const char *name : envVars) {
+    const char *value{std::getenv(name)};
+    if (value && *value) {
+      return value;
+    }
+  }
+#ifdef P_tmpdir
+  if (P_tmpdir && *P_tmpdir) {
+    return P_tmpdir;
+  }
+#endif
+  return "/tmp";
+}
+#endif
+
 static int openfile_mkstemp(IoErrorHandler &handler) {
 #ifdef _WIN32
   const unsigned int uUnique{0};
@@ -48,14 +72,26 @@ static int openfile_mkstemp(IoErrorHandler &handler) {
   int fd{::_open(tempFileName, _O_CREAT | _O_BINARY | _O_TEMPORARY | _O_RDWR,
       _S_IREAD | _S_IWRITE)};
 #else
-  char path[]{"/tmp/Fortran-Scratch-XXXXXX"};
-  int fd{::mkstemp(path)};
+  static constexpr char suffix[]{"/Fortran-Scratch-XXXXXX"};
+  const char *dir{GetSystemTempDir()};
+  std::size_t dirLen{std::strlen(dir)};
+  // Drop a single trailing slash so we never produce a "//" in the path.
+  if (dirLen > 1 && dir[dirLen - 1] == '/') {
+    --dirLen;
+  }
+  OwningPtr<char> path{reinterpret_cast<char *>(
+      AllocateMemoryOrCrash(handler, dirLen + sizeof(suffix)))};
+  runtime::memcpy(path.get(), dir, dirLen);
+  runtime::memcpy(path.get() + dirLen, suffix, sizeof(suffix));
+  int fd{::mkstemp(path.get())};
 #endif
   if (fd < 0) {
     handler.SignalErrno();
   }
 #ifndef _WIN32
-  ::unlink(path);
+  if (fd >= 0) {
+    ::unlink(path.get());
+  }
 #endif
   return fd;
 }
diff --git a/flang-rt/unittests/Runtime/CMakeLists.txt b/flang-rt/unittests/Runtime/CMakeLists.txt
index ddabe346b54d1..09d6dfcef4771 100644
--- a/flang-rt/unittests/Runtime/CMakeLists.txt
+++ b/flang-rt/unittests/Runtime/CMakeLists.txt
@@ -37,6 +37,7 @@ add_flangrt_unittest(RuntimeTests
   Random.cpp
   Reduction.cpp
   RuntimeCrashTest.cpp
+  ScratchTempDir.cpp
   Stop.cpp
   Support.cpp
   Time.cpp
diff --git a/flang-rt/unittests/Runtime/ScratchTempDir.cpp b/flang-rt/unittests/Runtime/ScratchTempDir.cpp
new file mode 100644
index 0000000000000..fc2a5c4539187
--- /dev/null
+++ b/flang-rt/unittests/Runtime/ScratchTempDir.cpp
@@ -0,0 +1,165 @@
+//===-- unittests/Runtime/ScratchTempDir.cpp --------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Verifies that OPEN(STATUS='SCRATCH') honors the TMPDIR/TMP/TEMP/TEMPDIR
+// environment variables on POSIX, matching llvm::sys::path::system_temp_directory.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef _WIN32
+
+#include "CrashHandlerFixture.h"
+#include "gtest/gtest.h"
+#include "flang/Runtime/io-api.h"
+#include "flang/Runtime/iostat-consts.h"
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <optional>
+#include <string>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <utility>
+#include <vector>
+
+using namespace Fortran::runtime;
+using namespace Fortran::runtime::io;
+
+namespace {
+
+// Saves the values of TMPDIR/TMP/TEMP/TEMPDIR on construction and restores
+// them on destruction so a test that mutates them does not leak state to
+// later tests in the same process.
+class TempDirEnvGuard {
+public:
+  TempDirEnvGuard() {
+    for (const char *name : kVars) {
+      if (const char *value{std::getenv(name)}) {
+        saved_.emplace_back(name, std::string{value});
+      } else {
+        saved_.emplace_back(name, std::nullopt);
+      }
+      ::unsetenv(name);
+    }
+  }
+  ~TempDirEnvGuard() {
+    for (const auto &entry : saved_) {
+      if (entry.second) {
+        ::setenv(entry.first, entry.second->c_str(), /*overwrite=*/1);
+      } else {
+        ::unsetenv(entry.first);
+      }
+    }
+  }
+
+private:
+  static constexpr const char *kVars[]{"TMPDIR", "TMP", "TEMP", "TEMPDIR"};
+  std::vector<std::pair<const char *, std::optional<std::string>>> saved_;
+};
+
+// Opens a SCRATCH unit and returns the iostat value from EndIoStatement
+// (IostatOk on success, non-zero on error). EnableHandlers makes the
+// runtime defer I/O errors to EndIoStatement rather than crashing the
+// process.
+int OpenScratchUnit() {
+  Cookie io{IONAME(BeginOpenNewUnit)(__FILE__, __LINE__)};
+  IONAME(EnableHandlers)(io, /*hasIoStat=*/true);
+  IONAME(SetStatus)(io, "SCRATCH", 7);
+  IONAME(SetAction)(io, "READWRITE", 9);
+  int unit{-1};
+  IONAME(GetNewUnit)(io, unit);
+  int iostat{IONAME(EndIoStatement)(io)};
+  if (iostat == IostatOk && unit >= 0) {
+    Cookie close{IONAME(BeginClose)(unit, __FILE__, __LINE__)};
+    IONAME(EndIoStatement)(close);
+  }
+  return iostat;
+}
+
+struct ScratchTempDirTests : CrashHandlerFixture {};
+
+// TMPDIR pointing at a non-existent directory must cause OPEN to fail —
+// this proves the env var is actually consulted by the runtime rather
+// than the hardcoded /tmp being used.
+TEST_F(ScratchTempDirTests, TmpdirHonored) {
+  TempDirEnvGuard guard;
+  ::setenv("TMPDIR", "/this/path/does/not/exist/flang-rt-test", 1);
+  EXPECT_NE(OpenScratchUnit(), IostatOk)
+      << "OPEN(STATUS='SCRATCH') should fail when TMPDIR points to a "
+         "non-existent directory";
+}
+
+// With no env vars set, OPEN must succeed via the /tmp fallback. This
+// confirms backward compatibility for users who do not set TMPDIR.
+TEST_F(ScratchTempDirTests, FallbackToTmp) {
+  TempDirEnvGuard guard;
+  EXPECT_EQ(OpenScratchUnit(), IostatOk)
+      << "OPEN(STATUS='SCRATCH') should succeed via /tmp fallback when no "
+         "temp env vars are set";
+}
+
+// TMP must be consulted when TMPDIR is unset. Set TMP to a non-existent
+// path and expect failure.
+TEST_F(ScratchTempDirTests, TmpConsulted) {
+  TempDirEnvGuard guard;
+  ::setenv("TMP", "/this/path/does/not/exist/flang-rt-test", 1);
+  EXPECT_NE(OpenScratchUnit(), IostatOk)
+      << "OPEN(STATUS='SCRATCH') should fail when TMP points to a "
+         "non-existent directory and TMPDIR is unset";
+}
+
+// An empty TMPDIR string must be skipped, so the next env var (TMP) wins.
+// Here TMP is a real directory, so OPEN should succeed.
+TEST_F(ScratchTempDirTests, EmptyTmpdirSkipped) {
+  TempDirEnvGuard guard;
+  char tmpDir[]{"/tmp/flang-rt-scratch-test-XXXXXX"};
+  ASSERT_NE(::mkdtemp(tmpDir), nullptr) << "mkdtemp failed: " << strerror(errno);
+  ::setenv("TMPDIR", "", 1);
+  ::setenv("TMP", tmpDir, 1);
+  EXPECT_EQ(OpenScratchUnit(), IostatOk)
+      << "Empty TMPDIR should be skipped in favor of TMP";
+  ::rmdir(tmpDir);
+}
+
+// SCRATCH file should actually be created inside the user-specified temp
+// directory. We can't observe the file by name (it is unlink'd immediately
+// after creation), but we can observe that the inode count of the directory
+// transiently increases while a SCRATCH unit is open.
+TEST_F(ScratchTempDirTests, FileCreatedInTmpdir) {
+  TempDirEnvGuard guard;
+  char tmpDir[]{"/tmp/flang-rt-scratch-test-XXXXXX"};
+  ASSERT_NE(::mkdtemp(tmpDir), nullptr) << "mkdtemp failed: " << strerror(errno);
+  ::setenv("TMPDIR", tmpDir, 1);
+
+  struct stat before{};
+  ASSERT_EQ(::stat(tmpDir, &before), 0);
+
+  Cookie io{IONAME(BeginOpenNewUnit)(__FILE__, __LINE__)};
+  IONAME(EnableHandlers)(io, /*hasIoStat=*/true);
+  ASSERT_TRUE(IONAME(SetStatus)(io, "SCRATCH", 7));
+  ASSERT_TRUE(IONAME(SetAction)(io, "READWRITE", 9));
+  int unit{-1};
+  ASSERT_TRUE(IONAME(GetNewUnit)(io, unit));
+  ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk);
+
+  struct stat during{};
+  ASSERT_EQ(::stat(tmpDir, &during), 0);
+  // mkstemp creates the file then we immediately unlink it, so st_nlink
+  // returns to the original count, but the directory's mtime/ctime should
+  // have been bumped by the create+unlink pair.
+  EXPECT_GE(during.st_mtime, before.st_mtime)
+      << "TMPDIR should have been touched by SCRATCH file creation";
+
+  Cookie close{IONAME(BeginClose)(unit, __FILE__, __LINE__)};
+  EXPECT_EQ(IONAME(EndIoStatement)(close), IostatOk);
+  ::rmdir(tmpDir);
+}
+
+} // namespace
+
+#endif // !_WIN32
diff --git a/flang/docs/RuntimeEnvironment.md b/flang/docs/RuntimeEnvironment.md
index 0414ea1e0d59c..af50e567e132d 100644
--- a/flang/docs/RuntimeEnvironment.md
+++ b/flang/docs/RuntimeEnvironment.md
@@ -84,3 +84,20 @@ the pool can hold, the runtime terminates with a diagnostic message that
 includes the current pool capacity.
 
 Example: `export FLANG_TRAMPOLINE_POOL_SIZE=4096`
+
+## `TMPDIR`, `TMP`, `TEMP`, `TEMPDIR`
+
+These conventional POSIX environment variables select the directory in
+which the runtime creates scratch files for `OPEN(STATUS='SCRATCH')`.
+The first one that is set to a non-empty value wins, with the search
+order being `TMPDIR`, then `TMP`, then `TEMP`, then `TEMPDIR`. If none
+are set, the runtime falls back to `P_tmpdir` (typically `/tmp` on
+glibc) and finally to `/tmp`. This lookup order matches
+`llvm::sys::path::system_temp_directory`, so flang agrees with the rest
+of the LLVM toolchain on where temporary files go.
+
+On Windows these variables are not consulted directly; scratch files
+are placed in the directory returned by `GetTempPathA`, which itself
+consults `TMP`, `TEMP`, and `USERPROFILE`.
+
+Example: `export TMPDIR=/var/local/scratch`

>From b7ac0ed7d10aeaf4b7a0d1d9071ce7faf38db873 Mon Sep 17 00:00:00 2001
From: Eugene Epshteyn <eepshteyn at nvidia.com>
Date: Mon, 25 May 2026 08:03:23 -0400
Subject: [PATCH 2/2] clang-format

---
 flang-rt/unittests/Runtime/ScratchTempDir.cpp | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/flang-rt/unittests/Runtime/ScratchTempDir.cpp b/flang-rt/unittests/Runtime/ScratchTempDir.cpp
index fc2a5c4539187..fbff7c348a58f 100644
--- a/flang-rt/unittests/Runtime/ScratchTempDir.cpp
+++ b/flang-rt/unittests/Runtime/ScratchTempDir.cpp
@@ -7,7 +7,8 @@
 //===----------------------------------------------------------------------===//
 //
 // Verifies that OPEN(STATUS='SCRATCH') honors the TMPDIR/TMP/TEMP/TEMPDIR
-// environment variables on POSIX, matching llvm::sys::path::system_temp_directory.
+// environment variables on POSIX, matching
+// llvm::sys::path::system_temp_directory.
 //
 //===----------------------------------------------------------------------===//
 
@@ -118,7 +119,8 @@ TEST_F(ScratchTempDirTests, TmpConsulted) {
 TEST_F(ScratchTempDirTests, EmptyTmpdirSkipped) {
   TempDirEnvGuard guard;
   char tmpDir[]{"/tmp/flang-rt-scratch-test-XXXXXX"};
-  ASSERT_NE(::mkdtemp(tmpDir), nullptr) << "mkdtemp failed: " << strerror(errno);
+  ASSERT_NE(::mkdtemp(tmpDir), nullptr)
+      << "mkdtemp failed: " << strerror(errno);
   ::setenv("TMPDIR", "", 1);
   ::setenv("TMP", tmpDir, 1);
   EXPECT_EQ(OpenScratchUnit(), IostatOk)
@@ -133,7 +135,8 @@ TEST_F(ScratchTempDirTests, EmptyTmpdirSkipped) {
 TEST_F(ScratchTempDirTests, FileCreatedInTmpdir) {
   TempDirEnvGuard guard;
   char tmpDir[]{"/tmp/flang-rt-scratch-test-XXXXXX"};
-  ASSERT_NE(::mkdtemp(tmpDir), nullptr) << "mkdtemp failed: " << strerror(errno);
+  ASSERT_NE(::mkdtemp(tmpDir), nullptr)
+      << "mkdtemp failed: " << strerror(errno);
   ::setenv("TMPDIR", tmpDir, 1);
 
   struct stat before{};



More information about the flang-commits mailing list