[flang-commits] [flang] [llvm] [flang-rt] Prototype: skip copy-out into read-only memory via a process memory-map snapshot (PR #223002)

Eugene Epshteyn via flang-commits flang-commits at lists.llvm.org
Fri Sep 11 10:52:08 PDT 2026


https://github.com/eugeneepshteyn created https://github.com/llvm/llvm-project/pull/223002

> [!WARNING]
> **This is a PROTOTYPE, posted for design discussion — not intended to land as-is.**
> It is stacked on top of #222101 (copy-out only the modified suffix); only the
> **last commit** is new here. Review that commit; the rest is #222101 plus its
> base.

## What this prototype does

An optional, environment-gated compatibility mode: the runtime consults the
process memory map to recognize copy-out destinations that live in read-only
memory and **skips the write-back**. A compiler-generated copy-out into
genuinely read-only storage could only ever rewrite identical bytes (the
temporary is an unmodified bitwise copy) or fault (an invalid program modified
a temporary whose original is not definable, e.g. backed by a named constant
in `.rodata`), so skipping converts the fault into a no-op while remaining
unobservable for conforming programs.

`FLANG_RT_COPYOUT_READONLY_MODE` (system environment variable):

- `0` (default): off — behavior identical to #222101.
- `1`: trust a one-time lazy snapshot of the memory map (Linux
  `/proc/self/maps`; Windows `VirtualQuery` walk), restricted to file-backed
  private read-only mappings. **No system calls on the copy-out path** — the
  check is one binary search over an immutable table.
- `2`: additionally re-confirm each (rare) snapshot hit against the current
  map before skipping (`PROCMAP_QUERY` ioctl on Linux ≥ 6.11 when available,
  else a re-parse; `VirtualQuery` on Windows).

`FLANG_RT_COPYOUT_READONLY_DIAG=1` reports the first few skipped copy-outs
with source location, plus a counter — the triage switch for the silent-skip
semantics.

## Design points

- **Fail-closed everywhere**: unsupported platform, parse anomaly, allocation
  failure, degenerate descriptor, partial containment, failed confirm — all
  answer "not read-only" and the regular copy-out runs. Initialization is a
  non-blocking atomic state machine (Uninitialized/Building/Ready/Inert);
  allocation failure makes the feature permanently inert rather than
  terminating (no `AllocateMemoryOrCrash` in the module).
- **Host-only**: compiled out of every device path (CUDA/OpenMP offload and
  native GPU); device copy-out behavior is bit-identical to #222101.
- **Documented staleness trade-off** (why this is a *compatibility mode*, not
  a proven-safe optimization): in mode 1 a mapping whose protection changes
  after the snapshot is not seen — a region that *became* read-only is simply
  not recognized (regular copy-out, today's behavior), and a formerly
  read-only region that became writable is still skipped (the copy-out is
  lost). Mode 2 narrows that window with per-hit confirmation but is not
  atomic across VMAs. Programs that `mprotect`/remap regions used as
  copy-out destinations mid-run should not enable this.
- The span check is stride-sign-aware and overflow-checked; the whole
  destination span must be contained in a read-only region.

## Testing

- `check-flang-rt` green on x86-64 and aarch64 Linux (the new unit tests add
  parser fault-injection — no trusted prefix is ever published on any parse
  anomaly — span/containment/classification units, and six
  subprocess-isolated behavioral arms, including both staleness directions
  and a death test proving the feature-off behavior still faults).
- The Windows enumeration/classification is written to the documented API
  contract and unit-tested through a host-portable classifier
  (write-copy protections and guard pages are never treated as read-only),
  but has **not** been run on Windows.

## Measurements (Linux; medians of 9 interleaved reps, pinned core)

- Writable destinations (the common path; the table always misses): **no
  regression** across a size × modified-percentage matrix on either
  architecture (worst cell within noise, ≤ +1.5%).
- Read-only destinations (non-contiguous section of a read-only mapping
  passed to an implicit-interface external in a hot loop, callee never
  modifies): mode 1 replaces the O(n) equal-scan with an O(log R) lookup —
  **19–47% faster** than #222101 alone on x86-64, **44–54%** on aarch64
  (Neoverse-N1). Mode 2 measures at parity with #222101: the confirm system
  call is reached only by executions that would otherwise crash, so
  conforming programs pay zero system calls in every mode.

## Known limitations (deliberate, for discussion)

- The silent skip masks invalid writes into read-only-backed destinations
  (that is its purpose — matching the tolerance of compilers that place
  named-constant arrays in writable static storage). The propagate-and-catch
  behavior of #222101 remains the default and is restored by mode 0.
- Array repacking's copy-back (`fir.unpack_array` →
  `ShallowCopyDirect`) is **not** covered by this prototype; with
  `-frepack-arrays`, a read-only-backed actual associated with an INTENT-less
  assumed-shape dummy still faults on the write-back even in modes 1/2. If
  this design moves forward, that entry point can reuse the same helper.
- Snapshot timing is nondeterministic with respect to `dlopen`: read-only
  segments mapped after the first copy-out are not in the trust table
  (fallback = regular copy-out).

🤖 Generated with [Claude Code](https://claude.com/claude-code)


>From 5abbfcd8f41dc264f5497dd4afb7b20f1f0e8626 Mon Sep 17 00:00:00 2001
From: Eugene Epshteyn <eepshteyn at nvidia.com>
Date: Tue, 8 Sep 2026 11:22:12 -0700
Subject: [PATCH 1/6] [flang-rt] Copy back only modified elements in
 CopyOutAssign

CopyOutAssign() used to copy the whole temporary back into the original
variable unconditionally. Since CopyInAssign() creates the temporary as a
bitwise copy of the variable, an element the callee never modified is
still bitwise-identical to the original, and storing it back is
unnecessary.

Compare each element bitwise and store only the elements that differ.
This avoids compiler-generated stores into storage that must not be
written -- e.g. an actual argument that is not definable and was,
conformingly, never modified by the callee -- which is a prerequisite for
passing non-definable actual arguments (such as named constants) without
defensive temporaries, as discussed in #187348.

The comparison is bitwise (memcmp), not a value comparison, so unmodified
NaN elements and derived-type padding do not produce spurious stores. The
helpers mirror the ShallowCopy* type/rank specialization structure so the
per-element comparison and copy inline to fixed-size operations.
---
 .../include/flang-rt/runtime/environment.h    |   4 +
 flang-rt/include/flang-rt/runtime/tools.h     |  11 ++
 flang-rt/lib/runtime/assign.cpp               |  15 ++-
 flang-rt/lib/runtime/environment.cpp          |  13 ++
 flang-rt/lib/runtime/tools.cpp                | 122 ++++++++++++++++++
 flang-rt/unittests/Runtime/Assign.cpp         | 122 ++++++++++++++++++
 flang/docs/RuntimeEnvironment.md              |  11 ++
 7 files changed, 297 insertions(+), 1 deletion(-)

diff --git a/flang-rt/include/flang-rt/runtime/environment.h b/flang-rt/include/flang-rt/runtime/environment.h
index 8ae441c12b3f8..a4dabb2ca20fb 100644
--- a/flang-rt/include/flang-rt/runtime/environment.h
+++ b/flang-rt/include/flang-rt/runtime/environment.h
@@ -74,6 +74,10 @@ struct ExecutionEnvironment {
   bool checkPointerDeallocation{true}; // FORT_CHECK_POINTER_DEALLOCATION
   bool truncateStream{true}; // FORT_TRUNCATE_STREAM
   bool noEmptyAllocation{false}; // FORT_NO_EMPTY_ALLOCATION
+  // FLANG_RT_COPYOUT_MODIFIED_ONLY=0 restores the unconditional copy-out
+  // of argument temporaries (CopyOutAssign copies every element back
+  // instead of only the elements the callee modified).
+  bool copyOutModifiedOnly{true}; // FLANG_RT_COPYOUT_MODIFIED_ONLY
 
   enum InternalDebugging { WorkQueue = 1 };
   int internalDebugging{0}; // FLANG_RT_DEBUG
diff --git a/flang-rt/include/flang-rt/runtime/tools.h b/flang-rt/include/flang-rt/runtime/tools.h
index a45c2ac98f2fa..411e5b2221702 100644
--- a/flang-rt/include/flang-rt/runtime/tools.h
+++ b/flang-rt/include/flang-rt/runtime/tools.h
@@ -525,6 +525,17 @@ RT_API_ATTRS void ShallowCopy(const Descriptor &to, const Descriptor &from,
     bool toIsContiguous, bool fromIsContiguous);
 RT_API_ATTRS void ShallowCopy(const Descriptor &to, const Descriptor &from);
 
+// Copies payload data like ShallowCopy(), but stores only to the elements of
+// 'to' whose bit patterns differ from the corresponding elements of 'from'.
+// Used by copy-out to avoid stores when the callee never modified the data,
+// so that an original that lives in read-only memory (e.g. a named constant)
+// is not written to by an unmodifying copy-out. The comparison is bitwise, so
+// it is exact when 'from' was originally produced from 'to' by ShallowCopy()
+// (as CopyInAssign() does): unmodified elements compare equal even for NaNs
+// and padding bytes, which a value comparison would misjudge.
+RT_API_ATTRS void ShallowCopyModifiedElements(
+    const Descriptor &to, const Descriptor &from);
+
 // Ensures that a character string is null-terminated, allocating a /p length +1
 // size memory for null-terminator if necessary. Returns the original or a newly
 // allocated null-terminated string (responsibility for deallocation is on the
diff --git a/flang-rt/lib/runtime/assign.cpp b/flang-rt/lib/runtime/assign.cpp
index 2fc584d2569e7..1f7e7c688875f 100644
--- a/flang-rt/lib/runtime/assign.cpp
+++ b/flang-rt/lib/runtime/assign.cpp
@@ -10,6 +10,7 @@
 #include "flang-rt/runtime/assign-impl.h"
 #include "flang-rt/runtime/derived.h"
 #include "flang-rt/runtime/descriptor.h"
+#include "flang-rt/runtime/environment.h"
 #include "flang-rt/runtime/memory.h"
 #include "flang-rt/runtime/stat.h"
 #include "flang-rt/runtime/terminator.h"
@@ -838,8 +839,20 @@ void RTDEF(CopyOutAssign)(
   Terminator terminator{sourceFile, sourceLine};
   // Copyout from the temporary must not cause any finalizations
   // for LHS. The variable must be properly initialized already.
+  // Copy back only the elements that were modified through the temporary:
+  // the temporary was created as a bitwise copy of the variable (see
+  // CopyInAssign above), so an element the callee never assigned is still
+  // bit-identical to the original and must not be stored to. This keeps a
+  // compiler-generated copy-out from writing into read-only storage when the
+  // effective argument is not definable (e.g., a named constant) and the
+  // callee, conformingly, never modified it.
+  // FLANG_RT_COPYOUT_MODIFIED_ONLY=0 restores the unconditional copy-out.
   if (var) {
-    ShallowCopy(*var, temp);
+    if (executionEnvironment.copyOutModifiedOnly) {
+      ShallowCopyModifiedElements(*var, temp);
+    } else {
+      ShallowCopy(*var, temp);
+    }
   }
   temp.Deallocate();
 }
diff --git a/flang-rt/lib/runtime/environment.cpp b/flang-rt/lib/runtime/environment.cpp
index ea98eb4634293..fc19c8c3973e7 100644
--- a/flang-rt/lib/runtime/environment.cpp
+++ b/flang-rt/lib/runtime/environment.cpp
@@ -205,6 +205,19 @@ void ExecutionEnvironment::Configure(int ac, const char *av[],
     }
   }
 
+  if (auto *x{std::getenv("FLANG_RT_COPYOUT_MODIFIED_ONLY")}) {
+    char *end;
+    auto n{std::strtol(x, &end, 10)};
+    if (n >= 0 && n <= 1 && *end == '\0') {
+      copyOutModifiedOnly = n != 0;
+    } else {
+      std::fprintf(stderr,
+          "Fortran runtime: FLANG_RT_COPYOUT_MODIFIED_ONLY=%s is invalid; "
+          "ignored\n",
+          x);
+    }
+  }
+
   if (auto *x{std::getenv("FLANG_RT_DEBUG")}) {
     internalDebugging = std::strtol(x, nullptr, 10);
   }
diff --git a/flang-rt/lib/runtime/tools.cpp b/flang-rt/lib/runtime/tools.cpp
index b7408c81f83f4..56890770c253f 100644
--- a/flang-rt/lib/runtime/tools.cpp
+++ b/flang-rt/lib/runtime/tools.cpp
@@ -271,6 +271,128 @@ RT_API_ATTRS void ShallowCopy(const Descriptor &to, const Descriptor &from) {
   ShallowCopy(to, from, to.IsContiguous(), from.IsContiguous());
 }
 
+// Compares one element bitwise and copies it only when it differs. As in the
+// ShallowCopy* helpers above, the compile-time element size lets the compiler
+// inline both the comparison and the copy.
+template <typename P>
+static inline RT_API_ATTRS void CopyElementIfModified(
+    char *toAt, const char *fromAt, std::size_t elementBytes) {
+  constexpr std::size_t typeElementBytes{sizeof(P)};
+  if constexpr (typeElementBytes != 1) {
+    if (runtime::memcmp(toAt, fromAt, typeElementBytes) != 0) {
+      runtime::memcpy(toAt, fromAt, typeElementBytes);
+    }
+  } else {
+    if (runtime::memcmp(toAt, fromAt, elementBytes) != 0) {
+      runtime::memcpy(toAt, fromAt, elementBytes);
+    }
+  }
+}
+
+template <typename P, int RANK = -1>
+static RT_API_ATTRS void ShallowCopyModifiedInner(const Descriptor &to,
+    const Descriptor &from, bool toIsContiguous, bool fromIsContiguous) {
+  std::size_t elementBytes{to.ElementBytes()};
+  if (toIsContiguous) {
+    char *toAt{to.OffsetElement()};
+    if (fromIsContiguous) {
+      const char *fromAt{from.OffsetElement()};
+      for (std::size_t n{to.Elements()}; n-- > 0;
+          toAt += elementBytes, fromAt += elementBytes) {
+        CopyElementIfModified<P>(toAt, fromAt, elementBytes);
+      }
+    } else {
+      DescriptorIterator<RANK> fromIt{from};
+      for (std::size_t n{to.Elements()}; n-- > 0;
+          toAt += elementBytes, fromIt.Advance()) {
+        CopyElementIfModified<P>(
+            toAt, fromIt.template Get<char>(), elementBytes);
+      }
+    }
+  } else {
+    DescriptorIterator<RANK> toIt{to};
+    if (fromIsContiguous) {
+      const char *fromAt{from.OffsetElement()};
+      for (std::size_t n{to.Elements()}; n-- > 0;
+          toIt.Advance(), fromAt += elementBytes) {
+        CopyElementIfModified<P>(
+            toIt.template Get<char>(), fromAt, elementBytes);
+      }
+    } else {
+      DescriptorIterator<RANK> fromIt{from};
+      for (std::size_t n{to.Elements()}; n-- > 0;
+          toIt.Advance(), fromIt.Advance()) {
+        CopyElementIfModified<P>(toIt.template Get<char>(),
+            fromIt.template Get<char>(), elementBytes);
+      }
+    }
+  }
+}
+
+template <typename P>
+static RT_API_ATTRS void ShallowCopyModifiedRank(const Descriptor &to,
+    const Descriptor &from, bool toIsContiguous, bool fromIsContiguous) {
+  INTERNAL_CHECK(to.rank() == from.rank());
+  // Mirror ShallowCopyRank's rank specialization policy.
+  switch (to.rank()) {
+  case 1:
+    ShallowCopyModifiedInner<P, 1>(to, from, toIsContiguous, fromIsContiguous);
+    return;
+  case 2:
+    ShallowCopyModifiedInner<P, 2>(to, from, toIsContiguous, fromIsContiguous);
+    return;
+  case 3:
+    ShallowCopyModifiedInner<P, 3>(to, from, toIsContiguous, fromIsContiguous);
+    return;
+  case 4:
+    ShallowCopyModifiedInner<P, 4>(to, from, toIsContiguous, fromIsContiguous);
+    return;
+  default:
+    ShallowCopyModifiedInner<P>(to, from, toIsContiguous, fromIsContiguous);
+    return;
+  }
+}
+
+RT_API_ATTRS void ShallowCopyModifiedElements(
+    const Descriptor &to, const Descriptor &from) {
+  bool toIsContiguous{to.IsContiguous()};
+  bool fromIsContiguous{from.IsContiguous()};
+  std::size_t elementBytes{to.ElementBytes()};
+  // Same type-based dispatch as ShallowCopy() above, so the per-element
+  // comparison and copy inline to fixed-size operations.
+  if (to.type().IsInteger()) {
+    if (elementBytes == sizeof(int64_t)) {
+      ShallowCopyModifiedRank<int64_t>(
+          to, from, toIsContiguous, fromIsContiguous);
+    } else if (elementBytes == sizeof(int32_t)) {
+      ShallowCopyModifiedRank<int32_t>(
+          to, from, toIsContiguous, fromIsContiguous);
+    } else if (elementBytes == sizeof(int16_t)) {
+      ShallowCopyModifiedRank<int16_t>(
+          to, from, toIsContiguous, fromIsContiguous);
+#if defined USING_NATIVE_INT128_T
+    } else if (elementBytes == sizeof(__int128_t)) {
+      ShallowCopyModifiedRank<__int128_t>(
+          to, from, toIsContiguous, fromIsContiguous);
+#endif
+    } else {
+      ShallowCopyModifiedRank<char>(to, from, toIsContiguous, fromIsContiguous);
+    }
+  } else if (to.type().IsReal()) {
+    if (elementBytes == sizeof(double)) {
+      ShallowCopyModifiedRank<double>(
+          to, from, toIsContiguous, fromIsContiguous);
+    } else if (elementBytes == sizeof(float)) {
+      ShallowCopyModifiedRank<float>(
+          to, from, toIsContiguous, fromIsContiguous);
+    } else {
+      ShallowCopyModifiedRank<char>(to, from, toIsContiguous, fromIsContiguous);
+    }
+  } else {
+    ShallowCopyModifiedRank<char>(to, from, toIsContiguous, fromIsContiguous);
+  }
+}
+
 RT_API_ATTRS char *EnsureNullTerminated(
     char *str, std::size_t length, Terminator &terminator) {
   if (runtime::memchr(str, '\0', length) == nullptr) {
diff --git a/flang-rt/unittests/Runtime/Assign.cpp b/flang-rt/unittests/Runtime/Assign.cpp
index 8e9b89d7f661f..e9b8de52ffde2 100644
--- a/flang-rt/unittests/Runtime/Assign.cpp
+++ b/flang-rt/unittests/Runtime/Assign.cpp
@@ -10,7 +10,14 @@
 #include "CrashHandlerFixture.h"
 #include "tools.h"
 #include "gtest/gtest.h"
+#include "flang-rt/runtime/environment.h"
+#include <cstdint>
+#include <cstring>
 #include <vector>
+#if defined(__unix__) || defined(__APPLE__)
+#include <sys/mman.h>
+#include <unistd.h>
+#endif
 
 using namespace Fortran::runtime;
 using Fortran::common::TypeCategory;
@@ -446,3 +453,118 @@ TEST(AssignSimpleCrash, NonAllocatableElementCountMismatch) {
   ASSERT_DEATH(RTNAME(AssignSimple)(dest, source, __FILE__, __LINE__),
       "AssignSimple: mismatching element counts");
 }
+
+TEST(Assign, RTNAME(CopyOutAssign)) {
+  // Copy-out writes back the elements the callee modified through the
+  // temporary and performs no stores for the elements it never touched.
+  // Discontiguous var: stride-2 view (elements 1,3,5,7) of an 8-element
+  // backing array, as copy-in/copy-out creates for a non-contiguous actual.
+  int data[8] = {1, 2, 3, 4, 5, 6, 7, 8};
+  TypeCode intType{TypeCategory::Integer, 4};
+  StaticDescriptor<1> staticVar;
+  Descriptor &var{staticVar.descriptor()};
+  SubscriptValue extent[1]{4};
+  var.Establish(intType, sizeof(int), data, 1, extent);
+  var.GetDimension(0).SetLowerBound(1);
+  var.GetDimension(0).SetByteStride(sizeof(int) * 2);
+
+  StaticDescriptor<1> staticTemp;
+  Descriptor &temp{staticTemp.descriptor()};
+  RTNAME(CopyInAssign)(temp, var, __FILE__, __LINE__);
+  ASSERT_TRUE(temp.IsAllocated());
+  ASSERT_TRUE(temp.IsContiguous());
+
+  // The "callee" modifies the first and third elements of the temporary.
+  *temp.OffsetElement<int>(0 * sizeof(int)) = 100;
+  *temp.OffsetElement<int>(2 * sizeof(int)) = 300;
+
+  RTNAME(CopyOutAssign)(&var, temp, __FILE__, __LINE__);
+
+  int expected[8] = {100, 2, 3, 4, 300, 6, 7, 8};
+  EXPECT_EQ(std::memcmp(data, expected, 8 * sizeof(int)), 0);
+}
+
+#if defined(__unix__) || defined(__APPLE__)
+TEST(Assign, RTNAME(CopyOutAssignReadOnlyUnmodified)) {
+  // An unmodified copy-out must perform no stores at all: the original may
+  // live in read-only memory (e.g. a named constant's storage). The array
+  // includes a NaN element to verify that the comparison is bitwise — a
+  // value comparison would consider the unmodified NaN element "changed"
+  // and store to it, faulting on the read-only page.
+  std::size_t pageSize{static_cast<std::size_t>(sysconf(_SC_PAGESIZE))};
+  void *page{mmap(nullptr, pageSize, PROT_READ | PROT_WRITE,
+      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)};
+  ASSERT_NE(page, MAP_FAILED);
+  double *data{static_cast<double *>(page)};
+  for (int j{0}; j < 8; ++j) {
+    data[j] = j + 1;
+  }
+  std::uint64_t quietNaN{0x7FF8000000000000ULL};
+  std::memcpy(&data[2], &quietNaN, sizeof(double));
+  ASSERT_EQ(mprotect(page, pageSize, PROT_READ), 0);
+
+  // Discontiguous read-only var: stride-2 view (elements 1,NaN,5,7).
+  StaticDescriptor<1> staticVar;
+  Descriptor &var{staticVar.descriptor()};
+  SubscriptValue extent[1]{4};
+  var.Establish(
+      TypeCode{TypeCategory::Real, 8}, sizeof(double), data, 1, extent);
+  var.GetDimension(0).SetLowerBound(1);
+  var.GetDimension(0).SetByteStride(sizeof(double) * 2);
+
+  StaticDescriptor<1> staticTemp;
+  Descriptor &temp{staticTemp.descriptor()};
+  RTNAME(CopyInAssign)(temp, var, __FILE__, __LINE__);
+  ASSERT_TRUE(temp.IsAllocated());
+
+  // The "callee" only reads the temporary; copying out into the read-only
+  // original must not fault.
+  RTNAME(CopyOutAssign)(&var, temp, __FILE__, __LINE__);
+
+  std::uint64_t elem2Bits;
+  std::memcpy(&elem2Bits, &data[2], sizeof(double));
+  EXPECT_EQ(elem2Bits, quietNaN);
+  EXPECT_EQ(data[0], 1.0);
+  EXPECT_EQ(data[4], 5.0);
+  ASSERT_EQ(munmap(page, pageSize), 0);
+}
+#endif
+
+#if defined(__unix__) || defined(__APPLE__)
+TEST(Assign, RTNAME(CopyOutAssignUnconditionalEnvVar)) {
+  // With FLANG_RT_COPYOUT_MODIFIED_ONLY=0 semantics (unconditional copy-out),
+  // even an unmodified copy-out stores every element, so a read-only original
+  // faults. This proves the environment control selects the legacy path.
+  std::size_t pageSize{static_cast<std::size_t>(sysconf(_SC_PAGESIZE))};
+  void *page{mmap(nullptr, pageSize, PROT_READ | PROT_WRITE,
+      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)};
+  ASSERT_NE(page, MAP_FAILED);
+  double *data{static_cast<double *>(page)};
+  for (int j{0}; j < 8; ++j) {
+    data[j] = j + 1;
+  }
+  ASSERT_EQ(mprotect(page, pageSize, PROT_READ), 0);
+
+  StaticDescriptor<1> staticVar;
+  Descriptor &var{staticVar.descriptor()};
+  SubscriptValue extent[1]{4};
+  var.Establish(
+      TypeCode{TypeCategory::Real, 8}, sizeof(double), data, 1, extent);
+  var.GetDimension(0).SetLowerBound(1);
+  var.GetDimension(0).SetByteStride(sizeof(double) * 2);
+
+  StaticDescriptor<1> staticTemp;
+  Descriptor &temp{staticTemp.descriptor()};
+  RTNAME(CopyInAssign)(temp, var, __FILE__, __LINE__);
+  ASSERT_TRUE(temp.IsAllocated());
+
+  executionEnvironment.copyOutModifiedOnly = false;
+  EXPECT_DEATH(RTNAME(CopyOutAssign)(&var, temp, __FILE__, __LINE__), "");
+  executionEnvironment.copyOutModifiedOnly = true;
+
+  // The parent's temp is still allocated (the death happened in the child);
+  // clean it up through the default path.
+  RTNAME(CopyOutAssign)(&var, temp, __FILE__, __LINE__);
+  ASSERT_EQ(munmap(page, pageSize), 0);
+}
+#endif
diff --git a/flang/docs/RuntimeEnvironment.md b/flang/docs/RuntimeEnvironment.md
index 0414ea1e0d59c..a24da8e6a6a62 100644
--- a/flang/docs/RuntimeEnvironment.md
+++ b/flang/docs/RuntimeEnvironment.md
@@ -34,6 +34,17 @@ Determines data conversions applied to unformatted I/O.
 * `BIG_ENDIAN`: assume input is big-endian; emit big-endian output
 * `SWAP`: reverse endianness (always convert)
 
+## `FLANG_RT_COPYOUT_MODIFIED_ONLY`
+
+When the compiler passes a copy of an actual argument to a procedure
+(copy-in/copy-out), the runtime copies back only the elements whose bit
+patterns were changed through the temporary copy. This avoids stores to
+the original argument when the callee never modified the data -- in
+particular, stores into read-only storage backing a non-definable actual
+argument.
+Set `FLANG_RT_COPYOUT_MODIFIED_ONLY=0` to restore the unconditional
+copy-out of every element.
+
 ## `FORT_CHECK_POINTER_DEALLOCATION`
 
 Fortran requires that a pointer that appears in a `DEALLOCATE` statement

>From 56179cfbdec339a9f27d60471502e82dbb8d6931 Mon Sep 17 00:00:00 2001
From: Eugene Epshteyn <eepshteyn at nvidia.com>
Date: Wed, 9 Sep 2026 08:21:34 -0700
Subject: [PATCH 2/6] [flang-rt] Fall back to whole-object copy-out when the
 temporary was modified

Benchmarking the per-element compare-and-store across modification
percentages showed a large penalty on partially modified data: with 50%
of the elements modified at random positions, the per-element conditional
store is an unpredictable branch, measuring up to 3.6x the cost of the
unconditional copy on x86-64 and 2.7x on AArch64, while 0% and 100% were
near parity.

Replace it with an all-or-nothing scheme: scan for the first bitwise
difference between the temporary and the variable; when there is none,
store nothing (the no-store guarantee for unmodified copy-out is
unchanged); when any element was modified, perform the plain whole-object
ShallowCopy. A modified temporary implies the variable is legally
definable, and storing unmodified elements alongside modified ones is
exactly the pre-existing behavior, so this changes nothing observable for
correct programs. With the fallback, copy-out measures at parity with the
unconditional copy at every modification percentage, and the unmodified
case keeps its no-store property.

This also replaces the ShallowCopyModifiedElements template family with a
smaller ElementsBitwiseEqual scan (early exit at the first difference),
and adds a death test documenting that a modified temporary copied out
into read-only storage still faults.
---
 flang-rt/include/flang-rt/runtime/tools.h | 18 ++---
 flang-rt/lib/runtime/assign.cpp           | 19 +++--
 flang-rt/lib/runtime/tools.cpp            | 91 ++++++++++++-----------
 flang-rt/unittests/Runtime/Assign.cpp     | 40 +++++++++-
 flang/docs/RuntimeEnvironment.md          |  9 ++-
 5 files changed, 113 insertions(+), 64 deletions(-)

diff --git a/flang-rt/include/flang-rt/runtime/tools.h b/flang-rt/include/flang-rt/runtime/tools.h
index 411e5b2221702..6038157369f3c 100644
--- a/flang-rt/include/flang-rt/runtime/tools.h
+++ b/flang-rt/include/flang-rt/runtime/tools.h
@@ -525,15 +525,15 @@ RT_API_ATTRS void ShallowCopy(const Descriptor &to, const Descriptor &from,
     bool toIsContiguous, bool fromIsContiguous);
 RT_API_ATTRS void ShallowCopy(const Descriptor &to, const Descriptor &from);
 
-// Copies payload data like ShallowCopy(), but stores only to the elements of
-// 'to' whose bit patterns differ from the corresponding elements of 'from'.
-// Used by copy-out to avoid stores when the callee never modified the data,
-// so that an original that lives in read-only memory (e.g. a named constant)
-// is not written to by an unmodifying copy-out. The comparison is bitwise, so
-// it is exact when 'from' was originally produced from 'to' by ShallowCopy()
-// (as CopyInAssign() does): unmodified elements compare equal even for NaNs
-// and padding bytes, which a value comparison would misjudge.
-RT_API_ATTRS void ShallowCopyModifiedElements(
+// Returns true when every element of 'from' is bitwise-identical to the
+// corresponding element of 'to' (returns false at the first difference).
+// Used by copy-out to skip all stores when the callee never modified the
+// data, so that an original that lives in read-only memory (e.g. a named
+// constant) is not written to by an unmodifying copy-out. The comparison is
+// bitwise, so it is exact when 'from' was originally produced from 'to' by
+// ShallowCopy() (as CopyInAssign() does): unmodified elements compare equal
+// even for NaNs and padding bytes, which a value comparison would misjudge.
+RT_API_ATTRS bool ElementsBitwiseEqual(
     const Descriptor &to, const Descriptor &from);
 
 // Ensures that a character string is null-terminated, allocating a /p length +1
diff --git a/flang-rt/lib/runtime/assign.cpp b/flang-rt/lib/runtime/assign.cpp
index 1f7e7c688875f..0377c00c24f32 100644
--- a/flang-rt/lib/runtime/assign.cpp
+++ b/flang-rt/lib/runtime/assign.cpp
@@ -839,18 +839,21 @@ void RTDEF(CopyOutAssign)(
   Terminator terminator{sourceFile, sourceLine};
   // Copyout from the temporary must not cause any finalizations
   // for LHS. The variable must be properly initialized already.
-  // Copy back only the elements that were modified through the temporary:
-  // the temporary was created as a bitwise copy of the variable (see
-  // CopyInAssign above), so an element the callee never assigned is still
-  // bit-identical to the original and must not be stored to. This keeps a
+  // Skip the copy-out entirely when the temporary is still bitwise-identical
+  // to the variable: the temporary was created as a bitwise copy (see
+  // CopyInAssign above), so it can only differ if the callee modified it, and
+  // an unmodifying copy-out must not store at all. This keeps a
   // compiler-generated copy-out from writing into read-only storage when the
   // effective argument is not definable (e.g., a named constant) and the
-  // callee, conformingly, never modified it.
+  // callee, conformingly, never modified it. When any element was modified,
+  // fall back to the plain whole-object copy: a per-element conditional
+  // store measures far slower on partially-modified data (branch
+  // misprediction), and a modified temporary means the variable is legally
+  // writable anyway.
   // FLANG_RT_COPYOUT_MODIFIED_ONLY=0 restores the unconditional copy-out.
   if (var) {
-    if (executionEnvironment.copyOutModifiedOnly) {
-      ShallowCopyModifiedElements(*var, temp);
-    } else {
+    if (!executionEnvironment.copyOutModifiedOnly ||
+        !ElementsBitwiseEqual(*var, temp)) {
       ShallowCopy(*var, temp);
     }
   }
diff --git a/flang-rt/lib/runtime/tools.cpp b/flang-rt/lib/runtime/tools.cpp
index 56890770c253f..2cc4a9da9d5bb 100644
--- a/flang-rt/lib/runtime/tools.cpp
+++ b/flang-rt/lib/runtime/tools.cpp
@@ -271,42 +271,41 @@ RT_API_ATTRS void ShallowCopy(const Descriptor &to, const Descriptor &from) {
   ShallowCopy(to, from, to.IsContiguous(), from.IsContiguous());
 }
 
-// Compares one element bitwise and copies it only when it differs. As in the
-// ShallowCopy* helpers above, the compile-time element size lets the compiler
-// inline both the comparison and the copy.
+// Compares one element bitwise. As in the ShallowCopy* helpers above, the
+// compile-time element size lets the compiler inline the comparison.
 template <typename P>
-static inline RT_API_ATTRS void CopyElementIfModified(
-    char *toAt, const char *fromAt, std::size_t elementBytes) {
+static inline RT_API_ATTRS bool ElementIsModified(
+    const char *toAt, const char *fromAt, std::size_t elementBytes) {
   constexpr std::size_t typeElementBytes{sizeof(P)};
   if constexpr (typeElementBytes != 1) {
-    if (runtime::memcmp(toAt, fromAt, typeElementBytes) != 0) {
-      runtime::memcpy(toAt, fromAt, typeElementBytes);
-    }
+    return runtime::memcmp(toAt, fromAt, typeElementBytes) != 0;
   } else {
-    if (runtime::memcmp(toAt, fromAt, elementBytes) != 0) {
-      runtime::memcpy(toAt, fromAt, elementBytes);
-    }
+    return runtime::memcmp(toAt, fromAt, elementBytes) != 0;
   }
 }
 
 template <typename P, int RANK = -1>
-static RT_API_ATTRS void ShallowCopyModifiedInner(const Descriptor &to,
+static RT_API_ATTRS bool ElementsBitwiseEqualInner(const Descriptor &to,
     const Descriptor &from, bool toIsContiguous, bool fromIsContiguous) {
   std::size_t elementBytes{to.ElementBytes()};
   if (toIsContiguous) {
-    char *toAt{to.OffsetElement()};
+    const char *toAt{to.OffsetElement()};
     if (fromIsContiguous) {
       const char *fromAt{from.OffsetElement()};
       for (std::size_t n{to.Elements()}; n-- > 0;
           toAt += elementBytes, fromAt += elementBytes) {
-        CopyElementIfModified<P>(toAt, fromAt, elementBytes);
+        if (ElementIsModified<P>(toAt, fromAt, elementBytes)) {
+          return false;
+        }
       }
     } else {
       DescriptorIterator<RANK> fromIt{from};
       for (std::size_t n{to.Elements()}; n-- > 0;
           toAt += elementBytes, fromIt.Advance()) {
-        CopyElementIfModified<P>(
-            toAt, fromIt.template Get<char>(), elementBytes);
+        if (ElementIsModified<P>(
+                toAt, fromIt.template Get<char>(), elementBytes)) {
+          return false;
+        }
       }
     }
   } else {
@@ -315,81 +314,89 @@ static RT_API_ATTRS void ShallowCopyModifiedInner(const Descriptor &to,
       const char *fromAt{from.OffsetElement()};
       for (std::size_t n{to.Elements()}; n-- > 0;
           toIt.Advance(), fromAt += elementBytes) {
-        CopyElementIfModified<P>(
-            toIt.template Get<char>(), fromAt, elementBytes);
+        if (ElementIsModified<P>(
+                toIt.template Get<char>(), fromAt, elementBytes)) {
+          return false;
+        }
       }
     } else {
       DescriptorIterator<RANK> fromIt{from};
       for (std::size_t n{to.Elements()}; n-- > 0;
           toIt.Advance(), fromIt.Advance()) {
-        CopyElementIfModified<P>(toIt.template Get<char>(),
-            fromIt.template Get<char>(), elementBytes);
+        if (ElementIsModified<P>(toIt.template Get<char>(),
+                fromIt.template Get<char>(), elementBytes)) {
+          return false;
+        }
       }
     }
   }
+  return true;
 }
 
 template <typename P>
-static RT_API_ATTRS void ShallowCopyModifiedRank(const Descriptor &to,
+static RT_API_ATTRS bool ElementsBitwiseEqualRank(const Descriptor &to,
     const Descriptor &from, bool toIsContiguous, bool fromIsContiguous) {
   INTERNAL_CHECK(to.rank() == from.rank());
   // Mirror ShallowCopyRank's rank specialization policy.
   switch (to.rank()) {
   case 1:
-    ShallowCopyModifiedInner<P, 1>(to, from, toIsContiguous, fromIsContiguous);
-    return;
+    return ElementsBitwiseEqualInner<P, 1>(
+        to, from, toIsContiguous, fromIsContiguous);
   case 2:
-    ShallowCopyModifiedInner<P, 2>(to, from, toIsContiguous, fromIsContiguous);
-    return;
+    return ElementsBitwiseEqualInner<P, 2>(
+        to, from, toIsContiguous, fromIsContiguous);
   case 3:
-    ShallowCopyModifiedInner<P, 3>(to, from, toIsContiguous, fromIsContiguous);
-    return;
+    return ElementsBitwiseEqualInner<P, 3>(
+        to, from, toIsContiguous, fromIsContiguous);
   case 4:
-    ShallowCopyModifiedInner<P, 4>(to, from, toIsContiguous, fromIsContiguous);
-    return;
+    return ElementsBitwiseEqualInner<P, 4>(
+        to, from, toIsContiguous, fromIsContiguous);
   default:
-    ShallowCopyModifiedInner<P>(to, from, toIsContiguous, fromIsContiguous);
-    return;
+    return ElementsBitwiseEqualInner<P>(
+        to, from, toIsContiguous, fromIsContiguous);
   }
 }
 
-RT_API_ATTRS void ShallowCopyModifiedElements(
+RT_API_ATTRS bool ElementsBitwiseEqual(
     const Descriptor &to, const Descriptor &from) {
   bool toIsContiguous{to.IsContiguous()};
   bool fromIsContiguous{from.IsContiguous()};
   std::size_t elementBytes{to.ElementBytes()};
   // Same type-based dispatch as ShallowCopy() above, so the per-element
-  // comparison and copy inline to fixed-size operations.
+  // comparison inlines to fixed-size operations.
   if (to.type().IsInteger()) {
     if (elementBytes == sizeof(int64_t)) {
-      ShallowCopyModifiedRank<int64_t>(
+      return ElementsBitwiseEqualRank<int64_t>(
           to, from, toIsContiguous, fromIsContiguous);
     } else if (elementBytes == sizeof(int32_t)) {
-      ShallowCopyModifiedRank<int32_t>(
+      return ElementsBitwiseEqualRank<int32_t>(
           to, from, toIsContiguous, fromIsContiguous);
     } else if (elementBytes == sizeof(int16_t)) {
-      ShallowCopyModifiedRank<int16_t>(
+      return ElementsBitwiseEqualRank<int16_t>(
           to, from, toIsContiguous, fromIsContiguous);
 #if defined USING_NATIVE_INT128_T
     } else if (elementBytes == sizeof(__int128_t)) {
-      ShallowCopyModifiedRank<__int128_t>(
+      return ElementsBitwiseEqualRank<__int128_t>(
           to, from, toIsContiguous, fromIsContiguous);
 #endif
     } else {
-      ShallowCopyModifiedRank<char>(to, from, toIsContiguous, fromIsContiguous);
+      return ElementsBitwiseEqualRank<char>(
+          to, from, toIsContiguous, fromIsContiguous);
     }
   } else if (to.type().IsReal()) {
     if (elementBytes == sizeof(double)) {
-      ShallowCopyModifiedRank<double>(
+      return ElementsBitwiseEqualRank<double>(
           to, from, toIsContiguous, fromIsContiguous);
     } else if (elementBytes == sizeof(float)) {
-      ShallowCopyModifiedRank<float>(
+      return ElementsBitwiseEqualRank<float>(
           to, from, toIsContiguous, fromIsContiguous);
     } else {
-      ShallowCopyModifiedRank<char>(to, from, toIsContiguous, fromIsContiguous);
+      return ElementsBitwiseEqualRank<char>(
+          to, from, toIsContiguous, fromIsContiguous);
     }
   } else {
-    ShallowCopyModifiedRank<char>(to, from, toIsContiguous, fromIsContiguous);
+    return ElementsBitwiseEqualRank<char>(
+        to, from, toIsContiguous, fromIsContiguous);
   }
 }
 
diff --git a/flang-rt/unittests/Runtime/Assign.cpp b/flang-rt/unittests/Runtime/Assign.cpp
index e9b8de52ffde2..cf2dba619766e 100644
--- a/flang-rt/unittests/Runtime/Assign.cpp
+++ b/flang-rt/unittests/Runtime/Assign.cpp
@@ -456,7 +456,8 @@ TEST(AssignSimpleCrash, NonAllocatableElementCountMismatch) {
 
 TEST(Assign, RTNAME(CopyOutAssign)) {
   // Copy-out writes back the elements the callee modified through the
-  // temporary and performs no stores for the elements it never touched.
+  // temporary (when nothing was modified, it performs no stores at all;
+  // see the read-only test below).
   // Discontiguous var: stride-2 view (elements 1,3,5,7) of an 8-element
   // backing array, as copy-in/copy-out creates for a non-contiguous actual.
   int data[8] = {1, 2, 3, 4, 5, 6, 7, 8};
@@ -567,4 +568,41 @@ TEST(Assign, RTNAME(CopyOutAssignUnconditionalEnvVar)) {
   RTNAME(CopyOutAssign)(&var, temp, __FILE__, __LINE__);
   ASSERT_EQ(munmap(page, pageSize), 0);
 }
+
+TEST(Assign, RTNAME(CopyOutAssignReadOnlyModifiedDies)) {
+  // When the callee DID modify the temporary, copy-out falls back to the
+  // whole-object copy; storing into a read-only original then faults, which
+  // is the intended behavior for a program that modifies a non-definable
+  // actual argument.
+  std::size_t pageSize{static_cast<std::size_t>(sysconf(_SC_PAGESIZE))};
+  void *page{mmap(nullptr, pageSize, PROT_READ | PROT_WRITE,
+      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)};
+  ASSERT_NE(page, MAP_FAILED);
+  double *data{static_cast<double *>(page)};
+  for (int j{0}; j < 8; ++j) {
+    data[j] = j + 1;
+  }
+  ASSERT_EQ(mprotect(page, pageSize, PROT_READ), 0);
+
+  StaticDescriptor<1> staticVar;
+  Descriptor &var{staticVar.descriptor()};
+  SubscriptValue extent[1]{4};
+  var.Establish(
+      TypeCode{TypeCategory::Real, 8}, sizeof(double), data, 1, extent);
+  var.GetDimension(0).SetLowerBound(1);
+  var.GetDimension(0).SetByteStride(sizeof(double) * 2);
+
+  StaticDescriptor<1> staticTemp;
+  Descriptor &temp{staticTemp.descriptor()};
+  RTNAME(CopyInAssign)(temp, var, __FILE__, __LINE__);
+  ASSERT_TRUE(temp.IsAllocated());
+  *temp.OffsetElement<double>(1 * sizeof(double)) = -1.0;
+
+  EXPECT_DEATH(RTNAME(CopyOutAssign)(&var, temp, __FILE__, __LINE__), "");
+
+  // Clean up the parent's still-allocated temp against writable storage.
+  ASSERT_EQ(mprotect(page, pageSize, PROT_READ | PROT_WRITE), 0);
+  RTNAME(CopyOutAssign)(&var, temp, __FILE__, __LINE__);
+  ASSERT_EQ(munmap(page, pageSize), 0);
+}
 #endif
diff --git a/flang/docs/RuntimeEnvironment.md b/flang/docs/RuntimeEnvironment.md
index a24da8e6a6a62..02128f4430de0 100644
--- a/flang/docs/RuntimeEnvironment.md
+++ b/flang/docs/RuntimeEnvironment.md
@@ -37,13 +37,14 @@ Determines data conversions applied to unformatted I/O.
 ## `FLANG_RT_COPYOUT_MODIFIED_ONLY`
 
 When the compiler passes a copy of an actual argument to a procedure
-(copy-in/copy-out), the runtime copies back only the elements whose bit
-patterns were changed through the temporary copy. This avoids stores to
-the original argument when the callee never modified the data -- in
+(copy-in/copy-out), the runtime skips the copy-out entirely when the
+temporary copy is still bitwise-identical to the original, and performs
+the normal whole-object copy-out otherwise. This avoids stores to the
+original argument when the callee never modified the data -- in
 particular, stores into read-only storage backing a non-definable actual
 argument.
 Set `FLANG_RT_COPYOUT_MODIFIED_ONLY=0` to restore the unconditional
-copy-out of every element.
+copy-out.
 
 ## `FORT_CHECK_POINTER_DEALLOCATION`
 

>From 31bf02f94c880403f181622cb31808756cbd9423 Mon Sep 17 00:00:00 2001
From: Eugene Epshteyn <eepshteyn at nvidia.com>
Date: Wed, 9 Sep 2026 10:14:52 -0700
Subject: [PATCH 3/6] [flang-rt] Copy only the modified suffix in CopyOutAssign

The scan-then-copy-whole-object fallback re-traverses the entire object
when the first modified element sits late in the array: with the
modifications clustered at the tail, the scan reads everything before
finding a difference and the copy then traverses everything again,
measuring up to 2.3x the unconditional copy.

Fuse the two phases: scan to the first bitwise difference and copy from
that position to the end, reusing the scan's position (one pass, no
repositioning; the copy phase remains unconditional, so no data-dependent
branching is reintroduced). Elements before the first difference are
bitwise-identical, so skipping them changes nothing observable; the
unmodified case still performs no stores; and copy-out now never
traverses the data more than once nor stores more elements than the
unconditional copy would, for any modification layout.

Adds a unit test pinning the prefix skip: the variable spans a read-only
page followed by a writable page, the callee modifies only an element on
the writable page, and copy-out must not fault while delivering the
modification.
---
 flang-rt/include/flang-rt/runtime/tools.h | 22 +++---
 flang-rt/lib/runtime/assign.cpp           | 22 +++---
 flang-rt/lib/runtime/tools.cpp            | 94 +++++++++++++++--------
 flang-rt/unittests/Runtime/Assign.cpp     | 43 +++++++++++
 4 files changed, 129 insertions(+), 52 deletions(-)

diff --git a/flang-rt/include/flang-rt/runtime/tools.h b/flang-rt/include/flang-rt/runtime/tools.h
index 6038157369f3c..01977ebe11912 100644
--- a/flang-rt/include/flang-rt/runtime/tools.h
+++ b/flang-rt/include/flang-rt/runtime/tools.h
@@ -525,15 +525,19 @@ RT_API_ATTRS void ShallowCopy(const Descriptor &to, const Descriptor &from,
     bool toIsContiguous, bool fromIsContiguous);
 RT_API_ATTRS void ShallowCopy(const Descriptor &to, const Descriptor &from);
 
-// Returns true when every element of 'from' is bitwise-identical to the
-// corresponding element of 'to' (returns false at the first difference).
-// Used by copy-out to skip all stores when the callee never modified the
-// data, so that an original that lives in read-only memory (e.g. a named
-// constant) is not written to by an unmodifying copy-out. The comparison is
-// bitwise, so it is exact when 'from' was originally produced from 'to' by
-// ShallowCopy() (as CopyInAssign() does): unmodified elements compare equal
-// even for NaNs and padding bytes, which a value comparison would misjudge.
-RT_API_ATTRS bool ElementsBitwiseEqual(
+// Scans for the first element of 'from' whose bit pattern differs from the
+// corresponding element of 'to', then copies that element and every element
+// after it (one fused pass). Elements before the first difference are
+// bitwise-identical and are not stored to. Used by copy-out so that an
+// unmodifying copy-out performs no stores at all — an original that lives in
+// read-only memory (e.g. a named constant) is never written to unless it was
+// actually modified — while a modifying copy-out never traverses the data
+// more than once nor stores more than the unconditional copy would. The
+// comparison is bitwise, so it is exact when 'from' was originally produced
+// from 'to' by ShallowCopy() (as CopyInAssign() does): unmodified elements
+// compare equal even for NaNs and padding bytes, which a value comparison
+// would misjudge.
+RT_API_ATTRS void ShallowCopyModifiedSuffix(
     const Descriptor &to, const Descriptor &from);
 
 // Ensures that a character string is null-terminated, allocating a /p length +1
diff --git a/flang-rt/lib/runtime/assign.cpp b/flang-rt/lib/runtime/assign.cpp
index 0377c00c24f32..e044d672c6e05 100644
--- a/flang-rt/lib/runtime/assign.cpp
+++ b/flang-rt/lib/runtime/assign.cpp
@@ -839,21 +839,21 @@ void RTDEF(CopyOutAssign)(
   Terminator terminator{sourceFile, sourceLine};
   // Copyout from the temporary must not cause any finalizations
   // for LHS. The variable must be properly initialized already.
-  // Skip the copy-out entirely when the temporary is still bitwise-identical
-  // to the variable: the temporary was created as a bitwise copy (see
-  // CopyInAssign above), so it can only differ if the callee modified it, and
-  // an unmodifying copy-out must not store at all. This keeps a
+  // Scan for the first bitwise difference and copy from there to the end
+  // (fused, one pass): the temporary was created as a bitwise copy (see
+  // CopyInAssign above), so it can only differ where the callee modified it,
+  // and an unmodifying copy-out must not store at all. This keeps a
   // compiler-generated copy-out from writing into read-only storage when the
   // effective argument is not definable (e.g., a named constant) and the
-  // callee, conformingly, never modified it. When any element was modified,
-  // fall back to the plain whole-object copy: a per-element conditional
-  // store measures far slower on partially-modified data (branch
-  // misprediction), and a modified temporary means the variable is legally
-  // writable anyway.
+  // callee, conformingly, never modified it. From the first difference
+  // onward the copy is unconditional: a per-element conditional store
+  // measures far slower on partially-modified data (branch misprediction),
+  // and a modified temporary means the variable is legally writable anyway.
   // FLANG_RT_COPYOUT_MODIFIED_ONLY=0 restores the unconditional copy-out.
   if (var) {
-    if (!executionEnvironment.copyOutModifiedOnly ||
-        !ElementsBitwiseEqual(*var, temp)) {
+    if (executionEnvironment.copyOutModifiedOnly) {
+      ShallowCopyModifiedSuffix(*var, temp);
+    } else {
       ShallowCopy(*var, temp);
     }
   }
diff --git a/flang-rt/lib/runtime/tools.cpp b/flang-rt/lib/runtime/tools.cpp
index 2cc4a9da9d5bb..cb3a36b4d74fc 100644
--- a/flang-rt/lib/runtime/tools.cpp
+++ b/flang-rt/lib/runtime/tools.cpp
@@ -284,118 +284,148 @@ static inline RT_API_ATTRS bool ElementIsModified(
   }
 }
 
+template <typename P>
+static inline RT_API_ATTRS void CopyElement(
+    char *toAt, const char *fromAt, std::size_t elementBytes) {
+  constexpr std::size_t typeElementBytes{sizeof(P)};
+  if constexpr (typeElementBytes != 1) {
+    runtime::memcpy(toAt, fromAt, typeElementBytes);
+  } else {
+    runtime::memcpy(toAt, fromAt, elementBytes);
+  }
+}
+
+// Scans for the first bitwise difference; when one is found, copies that
+// element and everything after it, reusing the scan's position (fused, one
+// pass). Elements before the first difference are bitwise-identical and are
+// not stored to, so an unmodified copy-out performs no stores at all, and a
+// copy-out never traverses the data more than once nor stores more elements
+// than the unconditional copy would.
 template <typename P, int RANK = -1>
-static RT_API_ATTRS bool ElementsBitwiseEqualInner(const Descriptor &to,
+static RT_API_ATTRS void ShallowCopyModifiedSuffixInner(const Descriptor &to,
     const Descriptor &from, bool toIsContiguous, bool fromIsContiguous) {
   std::size_t elementBytes{to.ElementBytes()};
+  std::size_t n{to.Elements()};
   if (toIsContiguous) {
-    const char *toAt{to.OffsetElement()};
+    char *toAt{to.OffsetElement()};
     if (fromIsContiguous) {
       const char *fromAt{from.OffsetElement()};
-      for (std::size_t n{to.Elements()}; n-- > 0;
-          toAt += elementBytes, fromAt += elementBytes) {
+      for (; n > 0; --n, toAt += elementBytes, fromAt += elementBytes) {
         if (ElementIsModified<P>(toAt, fromAt, elementBytes)) {
-          return false;
+          // Copy the remaining elements, including this one, in one block.
+          runtime::memcpy(toAt, fromAt, n * elementBytes);
+          return;
         }
       }
     } else {
       DescriptorIterator<RANK> fromIt{from};
-      for (std::size_t n{to.Elements()}; n-- > 0;
-          toAt += elementBytes, fromIt.Advance()) {
+      for (; n > 0; --n, toAt += elementBytes, fromIt.Advance()) {
         if (ElementIsModified<P>(
                 toAt, fromIt.template Get<char>(), elementBytes)) {
-          return false;
+          break;
         }
       }
+      for (; n > 0; --n, toAt += elementBytes, fromIt.Advance()) {
+        CopyElement<P>(toAt, fromIt.template Get<char>(), elementBytes);
+      }
     }
   } else {
     DescriptorIterator<RANK> toIt{to};
     if (fromIsContiguous) {
       const char *fromAt{from.OffsetElement()};
-      for (std::size_t n{to.Elements()}; n-- > 0;
-          toIt.Advance(), fromAt += elementBytes) {
+      for (; n > 0; --n, toIt.Advance(), fromAt += elementBytes) {
         if (ElementIsModified<P>(
                 toIt.template Get<char>(), fromAt, elementBytes)) {
-          return false;
+          break;
         }
       }
+      for (; n > 0; --n, toIt.Advance(), fromAt += elementBytes) {
+        CopyElement<P>(toIt.template Get<char>(), fromAt, elementBytes);
+      }
     } else {
       DescriptorIterator<RANK> fromIt{from};
-      for (std::size_t n{to.Elements()}; n-- > 0;
-          toIt.Advance(), fromIt.Advance()) {
+      for (; n > 0; --n, toIt.Advance(), fromIt.Advance()) {
         if (ElementIsModified<P>(toIt.template Get<char>(),
                 fromIt.template Get<char>(), elementBytes)) {
-          return false;
+          break;
         }
       }
+      for (; n > 0; --n, toIt.Advance(), fromIt.Advance()) {
+        CopyElement<P>(toIt.template Get<char>(), fromIt.template Get<char>(),
+            elementBytes);
+      }
     }
   }
-  return true;
 }
 
 template <typename P>
-static RT_API_ATTRS bool ElementsBitwiseEqualRank(const Descriptor &to,
+static RT_API_ATTRS void ShallowCopyModifiedSuffixRank(const Descriptor &to,
     const Descriptor &from, bool toIsContiguous, bool fromIsContiguous) {
   INTERNAL_CHECK(to.rank() == from.rank());
   // Mirror ShallowCopyRank's rank specialization policy.
   switch (to.rank()) {
   case 1:
-    return ElementsBitwiseEqualInner<P, 1>(
+    ShallowCopyModifiedSuffixInner<P, 1>(
         to, from, toIsContiguous, fromIsContiguous);
+    return;
   case 2:
-    return ElementsBitwiseEqualInner<P, 2>(
+    ShallowCopyModifiedSuffixInner<P, 2>(
         to, from, toIsContiguous, fromIsContiguous);
+    return;
   case 3:
-    return ElementsBitwiseEqualInner<P, 3>(
+    ShallowCopyModifiedSuffixInner<P, 3>(
         to, from, toIsContiguous, fromIsContiguous);
+    return;
   case 4:
-    return ElementsBitwiseEqualInner<P, 4>(
+    ShallowCopyModifiedSuffixInner<P, 4>(
         to, from, toIsContiguous, fromIsContiguous);
+    return;
   default:
-    return ElementsBitwiseEqualInner<P>(
+    ShallowCopyModifiedSuffixInner<P>(
         to, from, toIsContiguous, fromIsContiguous);
+    return;
   }
 }
 
-RT_API_ATTRS bool ElementsBitwiseEqual(
+RT_API_ATTRS void ShallowCopyModifiedSuffix(
     const Descriptor &to, const Descriptor &from) {
   bool toIsContiguous{to.IsContiguous()};
   bool fromIsContiguous{from.IsContiguous()};
   std::size_t elementBytes{to.ElementBytes()};
   // Same type-based dispatch as ShallowCopy() above, so the per-element
-  // comparison inlines to fixed-size operations.
+  // comparison and copy inline to fixed-size operations.
   if (to.type().IsInteger()) {
     if (elementBytes == sizeof(int64_t)) {
-      return ElementsBitwiseEqualRank<int64_t>(
+      ShallowCopyModifiedSuffixRank<int64_t>(
           to, from, toIsContiguous, fromIsContiguous);
     } else if (elementBytes == sizeof(int32_t)) {
-      return ElementsBitwiseEqualRank<int32_t>(
+      ShallowCopyModifiedSuffixRank<int32_t>(
           to, from, toIsContiguous, fromIsContiguous);
     } else if (elementBytes == sizeof(int16_t)) {
-      return ElementsBitwiseEqualRank<int16_t>(
+      ShallowCopyModifiedSuffixRank<int16_t>(
           to, from, toIsContiguous, fromIsContiguous);
 #if defined USING_NATIVE_INT128_T
     } else if (elementBytes == sizeof(__int128_t)) {
-      return ElementsBitwiseEqualRank<__int128_t>(
+      ShallowCopyModifiedSuffixRank<__int128_t>(
           to, from, toIsContiguous, fromIsContiguous);
 #endif
     } else {
-      return ElementsBitwiseEqualRank<char>(
+      ShallowCopyModifiedSuffixRank<char>(
           to, from, toIsContiguous, fromIsContiguous);
     }
   } else if (to.type().IsReal()) {
     if (elementBytes == sizeof(double)) {
-      return ElementsBitwiseEqualRank<double>(
+      ShallowCopyModifiedSuffixRank<double>(
           to, from, toIsContiguous, fromIsContiguous);
     } else if (elementBytes == sizeof(float)) {
-      return ElementsBitwiseEqualRank<float>(
+      ShallowCopyModifiedSuffixRank<float>(
           to, from, toIsContiguous, fromIsContiguous);
     } else {
-      return ElementsBitwiseEqualRank<char>(
+      ShallowCopyModifiedSuffixRank<char>(
           to, from, toIsContiguous, fromIsContiguous);
     }
   } else {
-    return ElementsBitwiseEqualRank<char>(
+    ShallowCopyModifiedSuffixRank<char>(
         to, from, toIsContiguous, fromIsContiguous);
   }
 }
diff --git a/flang-rt/unittests/Runtime/Assign.cpp b/flang-rt/unittests/Runtime/Assign.cpp
index cf2dba619766e..0449fd4f652ad 100644
--- a/flang-rt/unittests/Runtime/Assign.cpp
+++ b/flang-rt/unittests/Runtime/Assign.cpp
@@ -569,6 +569,49 @@ TEST(Assign, RTNAME(CopyOutAssignUnconditionalEnvVar)) {
   ASSERT_EQ(munmap(page, pageSize), 0);
 }
 
+TEST(Assign, RTNAME(CopyOutAssignSkipsUnmodifiedPrefix)) {
+  // When the first modification lies beyond a read-only prefix, copy-out
+  // must not store into the unmodified prefix: two adjacent pages, the first
+  // read-only, the second writable; the variable spans both; the callee
+  // modifies only an element on the second page. Copy-out must not fault and
+  // must deliver the modification.
+  std::size_t pageSize{static_cast<std::size_t>(sysconf(_SC_PAGESIZE))};
+  void *pages{mmap(nullptr, 2 * pageSize, PROT_READ | PROT_WRITE,
+      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)};
+  ASSERT_NE(pages, MAP_FAILED);
+  // Element stride 2*sizeof(double); place 'count' elements so that the
+  // first ones sit on page 1 and the last ones on page 2.
+  std::size_t perPage{pageSize / (2 * sizeof(double))};
+  std::size_t count{perPage + 4};
+  double *data{static_cast<double *>(pages)};
+  for (std::size_t j{0}; j < 2 * count; ++j) {
+    data[j] = static_cast<double>(j);
+  }
+  ASSERT_EQ(mprotect(pages, pageSize, PROT_READ), 0); // page 1 read-only
+
+  StaticDescriptor<1> staticVar;
+  Descriptor &var{staticVar.descriptor()};
+  SubscriptValue extent[1]{static_cast<SubscriptValue>(count)};
+  var.Establish(
+      TypeCode{TypeCategory::Real, 8}, sizeof(double), data, 1, extent);
+  var.GetDimension(0).SetLowerBound(1);
+  var.GetDimension(0).SetByteStride(sizeof(double) * 2);
+
+  StaticDescriptor<1> staticTemp;
+  Descriptor &temp{staticTemp.descriptor()};
+  RTNAME(CopyInAssign)(temp, var, __FILE__, __LINE__);
+  ASSERT_TRUE(temp.IsAllocated());
+  // Modify only the last element; its storage is on the writable page 2.
+  *temp.OffsetElement<double>((count - 1) * sizeof(double)) = -123.0;
+
+  RTNAME(CopyOutAssign)(&var, temp, __FILE__, __LINE__); // must not fault
+  EXPECT_EQ(data[2 * (count - 1)], -123.0);
+  EXPECT_EQ(data[0], 0.0);
+
+  ASSERT_EQ(mprotect(pages, pageSize, PROT_READ | PROT_WRITE), 0);
+  ASSERT_EQ(munmap(pages, 2 * pageSize), 0);
+}
+
 TEST(Assign, RTNAME(CopyOutAssignReadOnlyModifiedDies)) {
   // When the callee DID modify the temporary, copy-out falls back to the
   // whole-object copy; storing into a read-only original then faults, which

>From 9ac27f9664daa6e1431666832702739295eb649d Mon Sep 17 00:00:00 2001
From: Eugene Epshteyn <eepshteyn at nvidia.com>
Date: Wed, 9 Sep 2026 11:25:09 -0700
Subject: [PATCH 4/6] [flang-rt] Test FLANG_RT_COPYOUT_MODIFIED_ONLY parsing

Add a unit test that exercises the environment-variable parsing path in
ExecutionEnvironment::Configure() for FLANG_RT_COPYOUT_MODIFIED_ONLY:
"0" disables, "1" enables, an invalid value warns and leaves the default
enabled, and an absent variable leaves the default. The existing control
test sets the parsed field directly and does not cover the parsing itself.
---
 flang-rt/unittests/Runtime/Assign.cpp | 30 +++++++++++++++++++++++++++
 1 file changed, 30 insertions(+)

diff --git a/flang-rt/unittests/Runtime/Assign.cpp b/flang-rt/unittests/Runtime/Assign.cpp
index 0449fd4f652ad..a53d6c7e07f07 100644
--- a/flang-rt/unittests/Runtime/Assign.cpp
+++ b/flang-rt/unittests/Runtime/Assign.cpp
@@ -532,6 +532,36 @@ TEST(Assign, RTNAME(CopyOutAssignReadOnlyUnmodified)) {
 #endif
 
 #if defined(__unix__) || defined(__APPLE__)
+#if defined(__unix__) || defined(__APPLE__)
+TEST(Assign, RTNAME(CopyOutAssignEnvVarParsing)) {
+  // Exercise the FLANG_RT_COPYOUT_MODIFIED_ONLY parsing path in
+  // ExecutionEnvironment::Configure(), rather than setting the field
+  // directly: "0" disables, "1" enables, an invalid value warns and leaves
+  // the default (enabled), and an absent variable leaves the default.
+  bool saved{executionEnvironment.copyOutModifiedOnly};
+
+  ASSERT_EQ(setenv("FLANG_RT_COPYOUT_MODIFIED_ONLY", "0", 1), 0);
+  executionEnvironment.Configure(0, nullptr, nullptr, nullptr);
+  EXPECT_FALSE(executionEnvironment.copyOutModifiedOnly);
+
+  ASSERT_EQ(setenv("FLANG_RT_COPYOUT_MODIFIED_ONLY", "1", 1), 0);
+  executionEnvironment.Configure(0, nullptr, nullptr, nullptr);
+  EXPECT_TRUE(executionEnvironment.copyOutModifiedOnly);
+
+  // Invalid value: warns, leaves the default (enabled).
+  ASSERT_EQ(setenv("FLANG_RT_COPYOUT_MODIFIED_ONLY", "2", 1), 0);
+  executionEnvironment.Configure(0, nullptr, nullptr, nullptr);
+  EXPECT_TRUE(executionEnvironment.copyOutModifiedOnly);
+
+  // Absent: default (enabled).
+  ASSERT_EQ(unsetenv("FLANG_RT_COPYOUT_MODIFIED_ONLY"), 0);
+  executionEnvironment.Configure(0, nullptr, nullptr, nullptr);
+  EXPECT_TRUE(executionEnvironment.copyOutModifiedOnly);
+
+  executionEnvironment.copyOutModifiedOnly = saved;
+}
+#endif
+
 TEST(Assign, RTNAME(CopyOutAssignUnconditionalEnvVar)) {
   // With FLANG_RT_COPYOUT_MODIFIED_ONLY=0 semantics (unconditional copy-out),
   // even an unmodified copy-out stores every element, so a read-only original

>From 17bb79f715eecda0e7326156a7a5585370392fd7 Mon Sep 17 00:00:00 2001
From: Eugene Epshteyn <eepshteyn at nvidia.com>
Date: Thu, 10 Sep 2026 13:49:01 -0700
Subject: [PATCH 5/6] [flang-rt] Name FLANG_RT_COPYOUT_MODIFIED_ONLY as a
 system environment variable

Refer to FLANG_RT_COPYOUT_MODIFIED_ONLY explicitly as a system environment
variable in the documentation and in the comments that mention it, so a
reader can tell what it is without consulting the runtime sources.

Documentation and comments only; no functional change.
---
 flang-rt/include/flang-rt/runtime/environment.h | 3 ++-
 flang-rt/lib/runtime/assign.cpp                 | 3 ++-
 flang/docs/RuntimeEnvironment.md                | 7 +++++--
 3 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/flang-rt/include/flang-rt/runtime/environment.h b/flang-rt/include/flang-rt/runtime/environment.h
index a4dabb2ca20fb..31478bcf3e4f1 100644
--- a/flang-rt/include/flang-rt/runtime/environment.h
+++ b/flang-rt/include/flang-rt/runtime/environment.h
@@ -74,7 +74,8 @@ struct ExecutionEnvironment {
   bool checkPointerDeallocation{true}; // FORT_CHECK_POINTER_DEALLOCATION
   bool truncateStream{true}; // FORT_TRUNCATE_STREAM
   bool noEmptyAllocation{false}; // FORT_NO_EMPTY_ALLOCATION
-  // FLANG_RT_COPYOUT_MODIFIED_ONLY=0 restores the unconditional copy-out
+  // The system environment variable FLANG_RT_COPYOUT_MODIFIED_ONLY=0
+  // restores the unconditional copy-out
   // of argument temporaries (CopyOutAssign copies every element back
   // instead of only the elements the callee modified).
   bool copyOutModifiedOnly{true}; // FLANG_RT_COPYOUT_MODIFIED_ONLY
diff --git a/flang-rt/lib/runtime/assign.cpp b/flang-rt/lib/runtime/assign.cpp
index e044d672c6e05..6f8f246d8e3c1 100644
--- a/flang-rt/lib/runtime/assign.cpp
+++ b/flang-rt/lib/runtime/assign.cpp
@@ -849,7 +849,8 @@ void RTDEF(CopyOutAssign)(
   // onward the copy is unconditional: a per-element conditional store
   // measures far slower on partially-modified data (branch misprediction),
   // and a modified temporary means the variable is legally writable anyway.
-  // FLANG_RT_COPYOUT_MODIFIED_ONLY=0 restores the unconditional copy-out.
+  // Setting the system environment variable FLANG_RT_COPYOUT_MODIFIED_ONLY=0
+  // restores the unconditional copy-out.
   if (var) {
     if (executionEnvironment.copyOutModifiedOnly) {
       ShallowCopyModifiedSuffix(*var, temp);
diff --git a/flang/docs/RuntimeEnvironment.md b/flang/docs/RuntimeEnvironment.md
index 02128f4430de0..0d3e274328a8a 100644
--- a/flang/docs/RuntimeEnvironment.md
+++ b/flang/docs/RuntimeEnvironment.md
@@ -36,6 +36,9 @@ Determines data conversions applied to unformatted I/O.
 
 ## `FLANG_RT_COPYOUT_MODIFIED_ONLY`
 
+The system environment variable `FLANG_RT_COPYOUT_MODIFIED_ONLY` selects how
+the runtime performs copy-out.
+
 When the compiler passes a copy of an actual argument to a procedure
 (copy-in/copy-out), the runtime skips the copy-out entirely when the
 temporary copy is still bitwise-identical to the original, and performs
@@ -43,8 +46,8 @@ the normal whole-object copy-out otherwise. This avoids stores to the
 original argument when the callee never modified the data -- in
 particular, stores into read-only storage backing a non-definable actual
 argument.
-Set `FLANG_RT_COPYOUT_MODIFIED_ONLY=0` to restore the unconditional
-copy-out.
+Set the system environment variable `FLANG_RT_COPYOUT_MODIFIED_ONLY=0` to
+restore the unconditional copy-out.
 
 ## `FORT_CHECK_POINTER_DEALLOCATION`
 

>From cf1a3dd703a08b7cc0d70c84dccf5794f0b6793b Mon Sep 17 00:00:00 2001
From: Eugene Epshteyn <eepshteyn at nvidia.com>
Date: Wed, 9 Sep 2026 13:02:37 -0700
Subject: [PATCH 6/6] [flang-rt] Optionally skip copy-out into read-only memory
 (memory-map probe)

Add an optional, environment-gated compatibility mode that consults the
process memory map to recognize copy-out destinations living in read-only
memory and skips the write-back: such a store could only rewrite identical
bytes or fault, so skipping converts the fault into a no-op for programs
that (invalidly) modified a temporary whose original is not definable.

FLANG_RT_COPYOUT_READONLY_MODE=0 (default) leaves behavior unchanged;
=1 trusts a one-time lazy snapshot of the map (Linux /proc/self/maps,
Windows VirtualQuery walk; file-backed private read-only mappings only)
with no system calls on the copy-out path; =2 additionally re-confirms
each hit against the current map (PROCMAP_QUERY ioctl when available,
else a re-parse) before skipping. All uncertainty falls back to the
regular copy-out; initialization is fail-closed and non-blocking, and
allocation failure makes the feature inert rather than terminating.
Host-only: compiled out of all device paths.

FLANG_RT_COPYOUT_READONLY_DIAG=1 reports the first few skipped copy-outs.

The Windows enumeration/classification is written to the documented API
contract and unit-tested via a host-portable classifier, but has not been
run on Windows.
---
 .../include/flang-rt/runtime/memory-map.h     |  95 +++
 flang-rt/lib/runtime/CMakeLists.txt           |   1 +
 flang-rt/lib/runtime/assign.cpp               |  30 +-
 flang-rt/lib/runtime/memory-map.cpp           | 601 ++++++++++++++++++
 flang-rt/unittests/Runtime/CMakeLists.txt     |   1 +
 flang-rt/unittests/Runtime/MemoryMap.cpp      | 421 ++++++++++++
 flang/docs/RuntimeEnvironment.md              |  22 +
 7 files changed, 1167 insertions(+), 4 deletions(-)
 create mode 100644 flang-rt/include/flang-rt/runtime/memory-map.h
 create mode 100644 flang-rt/lib/runtime/memory-map.cpp
 create mode 100644 flang-rt/unittests/Runtime/MemoryMap.cpp

diff --git a/flang-rt/include/flang-rt/runtime/memory-map.h b/flang-rt/include/flang-rt/runtime/memory-map.h
new file mode 100644
index 0000000000000..7bf9ffe98321c
--- /dev/null
+++ b/flang-rt/include/flang-rt/runtime/memory-map.h
@@ -0,0 +1,95 @@
+//===-- include/flang-rt/runtime/memory-map.h -------------------*- 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
+//
+//===----------------------------------------------------------------------===//
+
+// Optional copy-out compatibility feature: consult the process memory map to
+// recognize copy-out destinations that live in read-only memory and skip the
+// write-back (see RTDEF(CopyOutAssign)). A compiler-generated copy-out into
+// genuinely read-only storage could only ever rewrite identical bytes or
+// fault, so skipping converts the fault into a no-op for programs that
+// (invalidly) modified a temporary whose original is not definable.
+//
+// Modes (FLANG_RT_COPYOUT_READONLY_MODE): 0 = off (default), 1 = trust the
+// one-time lazy snapshot (no system calls on the copy-out path), 2 = re-confirm
+// each snapshot hit against the current OS state before skipping. Every
+// uncertainty - unsupported platform, parse anomaly, allocation failure,
+// degenerate descriptor, partial containment - answers "not read-only", i.e.
+// the regular copy-out runs. Host-only: all of this is compiled out of device
+// paths.
+
+#ifndef FLANG_RT_RUNTIME_MEMORY_MAP_H_
+#define FLANG_RT_RUNTIME_MEMORY_MAP_H_
+
+#include "flang/Common/api-attrs.h"
+#include <cstddef>
+#include <cstdint>
+
+namespace Fortran::runtime {
+class Descriptor;
+
+#if !defined(RT_DEVICE_COMPILATION) && !defined(RT_GPU_TARGET)
+
+enum class CopyOutReadOnlyMode : int {
+  Off = 0, // feature disabled
+  Trust = 1, // snapshot table only; no system calls at copy-out
+  Confirm = 2, // snapshot hit re-confirmed against current OS state
+};
+
+// Parsed lazily from FLANG_RT_COPYOUT_READONLY_MODE; invalid values are Off.
+CopyOutReadOnlyMode GetCopyOutReadOnlyMode();
+
+// True iff the whole data span of 'var' lies within the snapshot's read-only
+// regions. Performs no system calls after the one-time lazy snapshot.
+bool CopyOutReadOnlyCandidate(const Descriptor &var);
+
+// True iff the whole data span of 'var' is mapped read-only in the *current*
+// OS memory map. Performs system calls; used by mode 2 on candidate hits.
+bool CopyOutReadOnlyConfirm(const Descriptor &var);
+
+// Diagnostics (FLANG_RT_COPYOUT_READONLY_DIAG=1): first-N notes on stderr and
+// a process-lifetime counter. Never allocates and never blocks.
+void NoteSkippedCopyOut(const char *sourceFile, int sourceLine);
+
+// Internal pieces exposed for unit testing only.
+namespace memmap {
+struct Region {
+  std::uintptr_t start;
+  std::uintptr_t end; // exclusive
+};
+
+// Parses a complete /proc/self/maps-format buffer into a malloc'd, coalesced,
+// ascending Region array of the read-only entries. fileBackedOnly selects the
+// trust-table filter (r, no w, private, inode != 0, no [pseudo] or (deleted)
+// paths); otherwise any readable non-writable mapping qualifies. Returns false
+// on ANY anomaly (malformed line, out-of-order or overlapping entries,
+// overflow, allocation failure) without publishing a partial result.
+bool ParseProcMaps(const char *buf, std::size_t len, bool fileBackedOnly,
+    Region **out, std::size_t *outCount);
+
+// Windows MEMORY_BASIC_INFORMATION classification, compiled everywhere so it
+// is unit-testable on any host. imageOnly selects the trust-table filter
+// (MEM_IMAGE regions only). Rejects guard pages, write-copy, and every
+// unknown protection combination.
+bool ProtectionIsReadOnly(std::uint32_t state, std::uint32_t protect,
+    std::uint32_t type, bool imageOnly);
+
+// True iff [lo, hi) is fully contained in one entry of the ascending,
+// coalesced region array.
+bool SpanIsContained(std::uintptr_t lo, std::uintptr_t hi,
+    const Region *regions, std::size_t count);
+
+// Computes the byte span touched through 'var' (stride-sign-aware, overflow
+// checked). False for degenerate descriptors (unallocated, zero extent,
+// zero-length elements) and on arithmetic overflow.
+bool ComputeDataSpan(
+    const Descriptor &var, std::uintptr_t &lo, std::uintptr_t &hi);
+} // namespace memmap
+
+#endif // !RT_DEVICE_COMPILATION && !RT_GPU_TARGET
+
+} // namespace Fortran::runtime
+#endif // FLANG_RT_RUNTIME_MEMORY_MAP_H_
diff --git a/flang-rt/lib/runtime/CMakeLists.txt b/flang-rt/lib/runtime/CMakeLists.txt
index ae9a6e7fb7188..825bb68e62614 100644
--- a/flang-rt/lib/runtime/CMakeLists.txt
+++ b/flang-rt/lib/runtime/CMakeLists.txt
@@ -120,6 +120,7 @@ set(host_sources
   execute.cpp
   extensions.cpp
   main.cpp
+  memory-map.cpp
   random.cpp
   reduce.cpp
   reduction.cpp
diff --git a/flang-rt/lib/runtime/assign.cpp b/flang-rt/lib/runtime/assign.cpp
index 6f8f246d8e3c1..e648253b938c4 100644
--- a/flang-rt/lib/runtime/assign.cpp
+++ b/flang-rt/lib/runtime/assign.cpp
@@ -11,6 +11,7 @@
 #include "flang-rt/runtime/derived.h"
 #include "flang-rt/runtime/descriptor.h"
 #include "flang-rt/runtime/environment.h"
+#include "flang-rt/runtime/memory-map.h"
 #include "flang-rt/runtime/memory.h"
 #include "flang-rt/runtime/stat.h"
 #include "flang-rt/runtime/terminator.h"
@@ -851,11 +852,32 @@ void RTDEF(CopyOutAssign)(
   // and a modified temporary means the variable is legally writable anyway.
   // Setting the system environment variable FLANG_RT_COPYOUT_MODIFIED_ONLY=0
   // restores the unconditional copy-out.
+  //
+  // Additionally, FLANG_RT_COPYOUT_READONLY_MODE (host only, default off)
+  // consults the process memory map and skips the copy-out entirely when the
+  // destination lives in read-only memory - a compatibility mode: any store
+  // into such a destination could only rewrite identical bytes or fault, so
+  // skipping converts the fault into a no-op. Mode 1 trusts a one-time
+  // snapshot of the map (no system calls here); mode 2 re-confirms each
+  // rare snapshot hit against the current map, and a stale entry (the
+  // destination is writable now) falls through to the regular copy-out.
+  // See memory-map.h.
   if (var) {
-    if (executionEnvironment.copyOutModifiedOnly) {
-      ShallowCopyModifiedSuffix(*var, temp);
-    } else {
-      ShallowCopy(*var, temp);
+    bool skipStores{false};
+#if !defined(RT_DEVICE_COMPILATION) && !defined(RT_GPU_TARGET)
+    if (CopyOutReadOnlyMode mode{GetCopyOutReadOnlyMode()};
+        mode != CopyOutReadOnlyMode::Off && CopyOutReadOnlyCandidate(*var) &&
+        (mode == CopyOutReadOnlyMode::Trust || CopyOutReadOnlyConfirm(*var))) {
+      skipStores = true;
+      NoteSkippedCopyOut(sourceFile, sourceLine);
+    }
+#endif
+    if (!skipStores) {
+      if (executionEnvironment.copyOutModifiedOnly) {
+        ShallowCopyModifiedSuffix(*var, temp);
+      } else {
+        ShallowCopy(*var, temp);
+      }
     }
   }
   temp.Deallocate();
diff --git a/flang-rt/lib/runtime/memory-map.cpp b/flang-rt/lib/runtime/memory-map.cpp
new file mode 100644
index 0000000000000..722e36d36835a
--- /dev/null
+++ b/flang-rt/lib/runtime/memory-map.cpp
@@ -0,0 +1,601 @@
+//===-- lib/runtime/memory-map.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
+//
+//===----------------------------------------------------------------------===//
+
+#include "flang-rt/runtime/memory-map.h"
+#include "flang-rt/runtime/descriptor.h"
+#include <atomic>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+
+#if !defined(RT_DEVICE_COMPILATION) && !defined(RT_GPU_TARGET)
+
+#if defined(_WIN32)
+#include "flang/Common/windows-include.h"
+#elif defined(__linux__) || defined(__unix__) || defined(__APPLE__)
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#if defined(__linux__) && __has_include(<linux/fs.h>)
+#include <linux/fs.h> // may define PROCMAP_QUERY (kernel >= 6.11 headers)
+#include <sys/ioctl.h>
+#endif
+#endif
+
+namespace Fortran::runtime {
+namespace memmap {
+
+// All memory in this module is plain malloc/realloc/free: an optional feature
+// must not crash the program on allocation failure (no AllocateMemoryOrCrash);
+// any failure makes the feature permanently inert instead.
+
+//===----------------------------------------------------------------------===//
+// Span computation
+//===----------------------------------------------------------------------===//
+
+bool ComputeDataSpan(
+    const Descriptor &var, std::uintptr_t &lo, std::uintptr_t &hi) {
+  const char *base{var.OffsetElement<char>()};
+  std::size_t elementBytes{var.ElementBytes()};
+  if (!base || elementBytes == 0) {
+    return false; // unallocated, or zero-length elements (e.g. CHARACTER(0))
+  }
+  // Sum negative-stride and positive-stride reaches separately so that the
+  // span is correct for any stride signs. All arithmetic is overflow-checked;
+  // no out-of-object C++ pointer arithmetic is performed.
+  std::int64_t negReach{0}; // <= 0
+  std::int64_t posReach{0}; // >= 0
+  for (int j{0}; j < var.rank(); ++j) {
+    const auto &dim{var.GetDimension(j)};
+    std::int64_t extent{dim.Extent()};
+    if (extent <= 0) {
+      return false; // zero-sized array: nothing will be stored anyway
+    }
+    std::int64_t stride{dim.ByteStride()};
+    std::int64_t reach;
+    if (__builtin_mul_overflow(extent - 1, stride, &reach)) {
+      return false;
+    }
+    if (reach < 0) {
+      if (__builtin_add_overflow(negReach, reach, &negReach)) {
+        return false;
+      }
+    } else if (__builtin_add_overflow(posReach, reach, &posReach)) {
+      return false;
+    }
+  }
+  auto baseAddr{reinterpret_cast<std::uintptr_t>(base)};
+  std::uintptr_t loAddr, hiAddr;
+  if (negReach < 0) {
+    std::uintptr_t down{static_cast<std::uintptr_t>(-negReach)};
+    if (down > baseAddr) {
+      return false;
+    }
+    loAddr = baseAddr - down;
+  } else {
+    loAddr = baseAddr;
+  }
+  if (__builtin_add_overflow(
+          baseAddr, static_cast<std::uintptr_t>(posReach), &hiAddr) ||
+      __builtin_add_overflow(hiAddr, elementBytes, &hiAddr)) {
+    return false;
+  }
+  if (hiAddr <= loAddr) {
+    return false;
+  }
+  lo = loAddr;
+  hi = hiAddr;
+  return true;
+}
+
+//===----------------------------------------------------------------------===//
+// Region table and containment
+//===----------------------------------------------------------------------===//
+
+bool SpanIsContained(std::uintptr_t lo, std::uintptr_t hi,
+    const Region *regions, std::size_t count) {
+  if (!regions || count == 0 || hi <= lo) {
+    return false;
+  }
+  // Binary search: last region with start <= lo. Regions are ascending and
+  // coalesced, so containment must be within that single region.
+  std::size_t first{0}, n{count};
+  while (n > 1) {
+    std::size_t half{n / 2};
+    if (regions[first + half].start <= lo) {
+      first += half;
+      n -= half;
+    } else {
+      n = half;
+    }
+  }
+  return regions[first].start <= lo && hi <= regions[first].end;
+}
+
+// Fixed-capacity guard against a pathological number of mappings; a process
+// with more read-only regions than this simply gets an inert feature.
+static constexpr std::size_t maxRegions{65536};
+
+namespace {
+struct RegionBuilder {
+  Region *data{nullptr};
+  std::size_t size{0};
+  std::size_t capacity{0};
+  std::uintptr_t lastEnd{0}; // monotonicity watermark over ALL parsed entries
+
+  ~RegionBuilder() { std::free(data); }
+
+  // Appends a kept region, coalescing with the previous kept one when
+  // contiguous. Returns false on allocation failure or capacity exhaustion.
+  bool Append(std::uintptr_t start, std::uintptr_t end) {
+    if (size > 0 && data[size - 1].end == start) {
+      data[size - 1].end = end;
+      return true;
+    }
+    if (size == capacity) {
+      if (capacity >= maxRegions) {
+        return false;
+      }
+      std::size_t newCap{capacity ? capacity * 2 : 64};
+      void *p{std::realloc(data, newCap * sizeof(Region))};
+      if (!p) {
+        return false;
+      }
+      data = static_cast<Region *>(p);
+      capacity = newCap;
+    }
+    data[size++] = Region{start, end};
+    return true;
+  }
+
+  Region *Release(std::size_t &countOut) {
+    Region *result{data};
+    countOut = size;
+    data = nullptr;
+    size = capacity = 0;
+    return result;
+  }
+};
+} // namespace
+
+//===----------------------------------------------------------------------===//
+// Linux /proc/self/maps parsing (fail-closed)
+//===----------------------------------------------------------------------===//
+
+static bool ParseHex(const char *&p, const char *end, std::uintptr_t &value) {
+  std::uintptr_t v{0};
+  const char *start{p};
+  while (p < end) {
+    char c{*p};
+    unsigned digit;
+    if (c >= '0' && c <= '9') {
+      digit = c - '0';
+    } else if (c >= 'a' && c <= 'f') {
+      digit = c - 'a' + 10;
+    } else {
+      break;
+    }
+    if (v > (~static_cast<std::uintptr_t>(0)) >> 4) {
+      return false; // overflow
+    }
+    v = (v << 4) | digit;
+    ++p;
+  }
+  if (p == start) {
+    return false;
+  }
+  value = v;
+  return true;
+}
+
+static bool ParseDec(const char *&p, const char *end, std::uint64_t &value) {
+  std::uint64_t v{0};
+  const char *start{p};
+  while (p < end && *p >= '0' && *p <= '9') {
+    if (v > (UINT64_MAX - 9) / 10) {
+      return false;
+    }
+    v = v * 10 + (*p - '0');
+    ++p;
+  }
+  if (p == start) {
+    return false;
+  }
+  value = v;
+  return true;
+}
+
+bool ParseProcMaps(const char *buf, std::size_t len, bool fileBackedOnly,
+    Region **out, std::size_t *outCount) {
+  *out = nullptr;
+  *outCount = 0;
+  RegionBuilder builder;
+  const char *p{buf};
+  const char *end{buf + len};
+  while (p < end) {
+    const char *lineEnd{static_cast<const char *>(
+        std::memchr(p, '\n', static_cast<std::size_t>(end - p)))};
+    if (!lineEnd) {
+      return false; // truncated final line: fail closed, publish nothing
+    }
+    // <start>-<end> <perms> <offset> <dev> <inode> [path]
+    std::uintptr_t start, stop;
+    if (!ParseHex(p, lineEnd, start) || p >= lineEnd || *p++ != '-' ||
+        !ParseHex(p, lineEnd, stop) || p >= lineEnd || *p++ != ' ') {
+      return false;
+    }
+    if (stop <= start || start < builder.lastEnd) {
+      return false; // empty, out-of-order, or overlapping entry
+    }
+    builder.lastEnd = stop;
+    if (lineEnd - p < 5) {
+      return false;
+    }
+    char permR{p[0]}, permW{p[1]}, permX{p[2]}, permP{p[3]};
+    if ((permR != 'r' && permR != '-') || (permW != 'w' && permW != '-') ||
+        (permX != 'x' && permX != '-') || (permP != 'p' && permP != 's')) {
+      return false;
+    }
+    p += 4;
+    if (*p++ != ' ') {
+      return false;
+    }
+    std::uintptr_t offset;
+    if (!ParseHex(p, lineEnd, offset) || p >= lineEnd || *p++ != ' ') {
+      return false;
+    }
+    std::uintptr_t devMajor, devMinor;
+    if (!ParseHex(p, lineEnd, devMajor) || p >= lineEnd || *p++ != ':' ||
+        !ParseHex(p, lineEnd, devMinor) || p >= lineEnd || *p++ != ' ') {
+      return false;
+    }
+    std::uint64_t inode;
+    if (!ParseDec(p, lineEnd, inode)) {
+      return false;
+    }
+    while (p < lineEnd && *p == ' ') {
+      ++p;
+    }
+    const char *path{p};
+    std::size_t pathLen{static_cast<std::size_t>(lineEnd - p)};
+    // Keep only readable, non-writable mappings. Both modes require 'r':
+    // a destination that was copied in from cannot have been PROT_NONE or
+    // execute-only, so its absence signals a bug, not a constant.
+    bool keep{permR == 'r' && permW == '-'};
+    if (keep && fileBackedOnly) {
+      // Trust-table filter: file-backed private mappings only. Anonymous
+      // read-only pages are the most recycling-prone (mprotect'd arenas,
+      // JIT) and contribute nothing to the PARAMETER/.rodata target.
+      keep = permP == 'p' && inode != 0 && pathLen > 0 && path[0] == '/' &&
+          !(pathLen >= 9 &&
+              std::memcmp(path + pathLen - 9, "(deleted)", 9) == 0);
+    }
+    if (keep && !builder.Append(start, stop)) {
+      return false;
+    }
+    p = lineEnd + 1;
+  }
+  *out = builder.Release(*outCount);
+  return true;
+}
+
+//===----------------------------------------------------------------------===//
+// Windows protection classification (compiled everywhere for testability)
+//===----------------------------------------------------------------------===//
+
+// Local mirrors of the Windows constants so this classifier can be unit-tested
+// on any host. Values are fixed ABI constants.
+static constexpr std::uint32_t kMemCommit{0x1000};
+static constexpr std::uint32_t kMemImage{0x1000000};
+static constexpr std::uint32_t kPageReadonly{0x02};
+static constexpr std::uint32_t kPageExecuteRead{0x20};
+
+bool ProtectionIsReadOnly(std::uint32_t state, std::uint32_t protect,
+    std::uint32_t type, bool imageOnly) {
+  if (state != kMemCommit) {
+    return false;
+  }
+  if (imageOnly && type != kMemImage) {
+    return false;
+  }
+  // Exactly PAGE_READONLY or PAGE_EXECUTE_READ, with no modifier bits at all:
+  // this rejects PAGE_GUARD, PAGE_NOCACHE, PAGE_WRITECOMBINE, the write-copy
+  // protections (writable on fault), and every unknown combination.
+  return protect == kPageReadonly || protect == kPageExecuteRead;
+}
+
+//===----------------------------------------------------------------------===//
+// Snapshot enumeration
+//===----------------------------------------------------------------------===//
+
+#if defined(_WIN32)
+
+static bool EnumerateReadOnlyRegions(
+    bool imageOnly, Region **out, std::size_t *outCount) {
+  *out = nullptr;
+  *outCount = 0;
+  RegionBuilder builder;
+  SYSTEM_INFO si;
+  GetNativeSystemInfo(&si);
+  std::uintptr_t address{0};
+  std::uintptr_t maxAddress{
+      reinterpret_cast<std::uintptr_t>(si.lpMaximumApplicationAddress)};
+  while (address < maxAddress) {
+    MEMORY_BASIC_INFORMATION mbi;
+    if (VirtualQuery(reinterpret_cast<LPCVOID>(address), &mbi, sizeof mbi) ==
+        0) {
+      break;
+    }
+    std::uintptr_t regionBase{
+        reinterpret_cast<std::uintptr_t>(mbi.BaseAddress)};
+    std::uintptr_t regionEnd;
+    if (__builtin_add_overflow(regionBase, mbi.RegionSize, &regionEnd) ||
+        regionEnd <= address) {
+      return false; // wrap or no forward progress
+    }
+    if (ProtectionIsReadOnly(mbi.State, mbi.Protect, mbi.Type, imageOnly) &&
+        !builder.Append(regionBase, regionEnd)) {
+      return false;
+    }
+    address = regionEnd;
+  }
+  *out = builder.Release(*outCount);
+  return true;
+}
+
+// Confirms [lo, hi) is currently committed read-only. VirtualQuery is the
+// native per-range primitive; no table is involved.
+static bool CurrentlyReadOnly(std::uintptr_t lo, std::uintptr_t hi) {
+  std::uintptr_t address{lo};
+  while (address < hi) {
+    MEMORY_BASIC_INFORMATION mbi;
+    if (VirtualQuery(reinterpret_cast<LPCVOID>(address), &mbi, sizeof mbi) ==
+        0) {
+      return false;
+    }
+    if (!ProtectionIsReadOnly(
+            mbi.State, mbi.Protect, mbi.Type, /*imageOnly=*/false)) {
+      return false;
+    }
+    std::uintptr_t regionEnd;
+    if (__builtin_add_overflow(
+            reinterpret_cast<std::uintptr_t>(mbi.BaseAddress), mbi.RegionSize,
+            &regionEnd) ||
+        regionEnd <= address) {
+      return false;
+    }
+    address = regionEnd;
+  }
+  return true;
+}
+
+#elif defined(__linux__)
+
+// Reads all of /proc/self/maps into a malloc'd buffer. procfs files are not
+// seekable or stat-able for size, so read in a doubling loop.
+static char *ReadWholeProcMaps(std::size_t *lenOut) {
+  int fd{-1};
+  do {
+    fd = ::open("/proc/self/maps", O_RDONLY | O_CLOEXEC);
+  } while (fd < 0 && errno == EINTR);
+  if (fd < 0) {
+    return nullptr;
+  }
+  std::size_t capacity{1u << 16};
+  std::size_t length{0};
+  char *buffer{static_cast<char *>(std::malloc(capacity))};
+  while (buffer) {
+    if (length == capacity) {
+      if (capacity >= (1u << 26)) { // 64 MiB cap: fail closed
+        break;
+      }
+      capacity *= 2;
+      void *p{std::realloc(buffer, capacity)};
+      if (!p) {
+        break;
+      }
+      buffer = static_cast<char *>(p);
+    }
+    ::ssize_t n{::read(fd, buffer + length, capacity - length)};
+    if (n < 0) {
+      if (errno == EINTR) {
+        continue;
+      }
+      break;
+    }
+    if (n == 0) {
+      ::close(fd);
+      *lenOut = length;
+      return buffer;
+    }
+    length += static_cast<std::size_t>(n);
+  }
+  ::close(fd);
+  std::free(buffer);
+  return nullptr;
+}
+
+static bool EnumerateReadOnlyRegions(
+    bool fileBackedOnly, Region **out, std::size_t *outCount) {
+  std::size_t length;
+  char *buffer{ReadWholeProcMaps(&length)};
+  if (!buffer) {
+    return false;
+  }
+  bool ok{ParseProcMaps(buffer, length, fileBackedOnly, out, outCount)};
+  std::free(buffer);
+  return ok;
+}
+
+#if defined(PROCMAP_QUERY)
+// Linux >= 6.11: per-VMA query ioctl on /proc/self/maps - no full traversal.
+// Returns 1 = read-only over the span, 0 = not, -1 = unsupported (fall back).
+static int QuerySpanReadOnlyIoctl(std::uintptr_t lo, std::uintptr_t hi) {
+  int fd{-1};
+  do {
+    fd = ::open("/proc/self/maps", O_RDONLY | O_CLOEXEC);
+  } while (fd < 0 && errno == EINTR);
+  if (fd < 0) {
+    return -1;
+  }
+  std::uintptr_t address{lo};
+  while (address < hi) {
+    struct procmap_query q;
+    std::memset(&q, 0, sizeof q);
+    q.size = sizeof q;
+    q.query_flags = 0; // covering VMA only
+    q.query_addr = address;
+    if (::ioctl(fd, PROCMAP_QUERY, &q) < 0) {
+      int e{errno};
+      ::close(fd);
+      return (e == ENOTTY || e == EINVAL || e == EOPNOTSUPP) ? -1 : 0;
+    }
+    if (!(q.vma_flags & PROCMAP_QUERY_VMA_READABLE) ||
+        (q.vma_flags & PROCMAP_QUERY_VMA_WRITABLE) || q.vma_end <= address) {
+      ::close(fd);
+      return 0;
+    }
+    address = q.vma_end;
+  }
+  ::close(fd);
+  return 1;
+}
+#endif // PROCMAP_QUERY
+
+static bool CurrentlyReadOnly(std::uintptr_t lo, std::uintptr_t hi) {
+#if defined(PROCMAP_QUERY)
+  if (int r{QuerySpanReadOnlyIoctl(lo, hi)}; r >= 0) {
+    return r == 1;
+  }
+#endif
+  // Fallback: re-read and re-parse the current map (any mapping type; the
+  // question here is current permissions, not provenance).
+  Region *regions{nullptr};
+  std::size_t count{0};
+  if (!EnumerateReadOnlyRegions(/*fileBackedOnly=*/false, &regions, &count)) {
+    return false;
+  }
+  bool result{SpanIsContained(lo, hi, regions, count)};
+  std::free(regions);
+  return result;
+}
+
+#else // neither _WIN32 nor __linux__: feature inert
+
+static bool EnumerateReadOnlyRegions(bool, Region **out, std::size_t *count) {
+  *out = nullptr;
+  *count = 0;
+  return false;
+}
+static bool CurrentlyReadOnly(std::uintptr_t, std::uintptr_t) { return false; }
+
+#endif
+
+//===----------------------------------------------------------------------===//
+// Lazy, fail-closed, non-blocking snapshot state machine
+//===----------------------------------------------------------------------===//
+
+enum : int { kUninitialized = 0, kBuilding = 1, kReady = 2, kInert = 3 };
+
+static std::atomic<int> tableState{kUninitialized};
+// One immutable table per process lifetime, intentionally leaked, never
+// replaced or reclaimed. Published only by the release store to tableState.
+static Region *roTable{nullptr};
+static std::size_t roTableCount{0};
+
+static bool EnsureSnapshot() {
+  int s{tableState.load(std::memory_order_acquire)};
+  if (s == kReady) {
+    return true;
+  }
+  if (s != kUninitialized) {
+    return false; // Building (never block; a fork child inheriting Building
+                  // observes exactly this) or Inert
+  }
+  int expected{kUninitialized};
+  if (!tableState.compare_exchange_strong(expected, kBuilding,
+          std::memory_order_acq_rel, std::memory_order_acquire)) {
+    return tableState.load(std::memory_order_acquire) == kReady;
+  }
+  Region *regions{nullptr};
+  std::size_t count{0};
+  if (EnumerateReadOnlyRegions(/*fileBackedOnly=*/true, &regions, &count)) {
+    roTable = regions;
+    roTableCount = count;
+    tableState.store(kReady, std::memory_order_release);
+    return true;
+  }
+  tableState.store(kInert, std::memory_order_release);
+  return false;
+}
+
+} // namespace memmap
+
+//===----------------------------------------------------------------------===//
+// Public entry points
+//===----------------------------------------------------------------------===//
+
+CopyOutReadOnlyMode GetCopyOutReadOnlyMode() {
+  // Read lazily and independently of ExecutionEnvironment::Configure, which
+  // never runs under a non-Fortran main program.
+  static std::atomic<int> cached{-1};
+  int mode{cached.load(std::memory_order_relaxed)};
+  if (mode < 0) {
+    mode = 0;
+    if (const char *x{std::getenv("FLANG_RT_COPYOUT_READONLY_MODE")}) {
+      char *end;
+      long n{std::strtol(x, &end, 10)};
+      if (n >= 0 && n <= 2 && *end == '\0' && end != x) {
+        mode = static_cast<int>(n);
+      } // anything else fails closed to Off
+    }
+    cached.store(mode, std::memory_order_relaxed);
+  }
+  return static_cast<CopyOutReadOnlyMode>(mode);
+}
+
+bool CopyOutReadOnlyCandidate(const Descriptor &var) {
+  std::uintptr_t lo, hi;
+  if (!memmap::ComputeDataSpan(var, lo, hi)) {
+    return false;
+  }
+  if (!memmap::EnsureSnapshot()) {
+    return false;
+  }
+  return memmap::SpanIsContained(lo, hi, memmap::roTable, memmap::roTableCount);
+}
+
+bool CopyOutReadOnlyConfirm(const Descriptor &var) {
+  std::uintptr_t lo, hi;
+  if (!memmap::ComputeDataSpan(var, lo, hi)) {
+    return false;
+  }
+  return memmap::CurrentlyReadOnly(lo, hi);
+}
+
+void NoteSkippedCopyOut(const char *sourceFile, int sourceLine) {
+  static std::atomic<std::uint64_t> skipCount{0};
+  static std::atomic<int> diagEnabled{-1};
+  std::uint64_t n{skipCount.fetch_add(1, std::memory_order_relaxed) + 1};
+  int enabled{diagEnabled.load(std::memory_order_relaxed)};
+  if (enabled < 0) {
+    const char *x{std::getenv("FLANG_RT_COPYOUT_READONLY_DIAG")};
+    enabled = x && x[0] == '1' && x[1] == '\0';
+    diagEnabled.store(enabled, std::memory_order_relaxed);
+  }
+  if (enabled && n <= 10) {
+    std::fprintf(stderr,
+        "flang-rt: skipped copy-out to read-only memory (%s:%d) [%llu]\n",
+        sourceFile ? sourceFile : "<unknown>", sourceLine,
+        static_cast<unsigned long long>(n));
+  }
+}
+
+} // namespace Fortran::runtime
+
+#endif // !RT_DEVICE_COMPILATION && !RT_GPU_TARGET
diff --git a/flang-rt/unittests/Runtime/CMakeLists.txt b/flang-rt/unittests/Runtime/CMakeLists.txt
index 112d864e26a53..244ea95306670 100644
--- a/flang-rt/unittests/Runtime/CMakeLists.txt
+++ b/flang-rt/unittests/Runtime/CMakeLists.txt
@@ -11,6 +11,7 @@ add_flangrt_unittest(RuntimeTests
   Allocatable.cpp
   ArrayConstructor.cpp
   Assign.cpp
+  MemoryMap.cpp
   BufferTest.cpp
   CharacterTest.cpp
   ChildIO.cpp
diff --git a/flang-rt/unittests/Runtime/MemoryMap.cpp b/flang-rt/unittests/Runtime/MemoryMap.cpp
new file mode 100644
index 0000000000000..ba4e2d7bc90b5
--- /dev/null
+++ b/flang-rt/unittests/Runtime/MemoryMap.cpp
@@ -0,0 +1,421 @@
+//===-- unittests/Runtime/MemoryMap.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
+//
+//===----------------------------------------------------------------------===//
+
+// Tests for the optional read-only-destination copy-out feature
+// (FLANG_RT_COPYOUT_READONLY_MODE). The parser, span, and protection
+// classifiers are tested directly; the behavioral arms run in death-test
+// subprocesses because the mode and the memory-map snapshot are latched once
+// per process.
+
+#include "CrashHandlerFixture.h"
+#include "tools.h"
+#include "gtest/gtest.h"
+#include "flang-rt/runtime/descriptor.h"
+#include "flang-rt/runtime/memory-map.h"
+#include "flang/Runtime/assign.h"
+#include <cstdlib>
+#include <cstring>
+#include <string>
+#include <vector>
+
+#if defined(__linux__)
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <unistd.h>
+#endif
+
+using namespace Fortran::runtime;
+using namespace Fortran::runtime::memmap;
+using Fortran::common::TypeCategory;
+
+//===----------------------------------------------------------------------===//
+// ParseProcMaps: filtering and coalescing
+//===----------------------------------------------------------------------===//
+
+static bool Parse(const std::string &s, bool fileBackedOnly, Region **out,
+    std::size_t *count) {
+  return ParseProcMaps(s.data(), s.size(), fileBackedOnly, out, count);
+}
+
+TEST(MemoryMapParse, FiltersAndCoalesces) {
+  std::string maps{// kept (file-backed private RO)
+      "1000-2000 r--p 00000000 08:01 41 /lib/a.so\n"
+      // kept and coalesced with the previous entry (contiguous)
+      "2000-3000 r-xp 00001000 08:01 41 /lib/a.so\n"
+      // rejected: writable
+      "3000-4000 rw-p 00002000 08:01 41 /lib/a.so\n"
+      // rejected in trust mode: anonymous (inode 0, no path)
+      "5000-6000 r--p 00000000 00:00 0 \n"
+      // rejected in trust mode: shared mapping
+      "6000-7000 r--s 00000000 08:01 42 /lib/b.so\n"
+      // rejected in trust mode: deleted file
+      "7000-8000 r--p 00000000 08:01 43 /lib/c.so (deleted)\n"
+      // rejected in trust mode: pseudo-mapping
+      "8000-9000 r--p 00000000 00:00 0 [vvar]\n"
+      // kept (second disjoint entry)
+      "a000-b000 r--p 00000000 08:01 44 /lib/d.so\n"};
+  Region *regions{nullptr};
+  std::size_t count{0};
+  ASSERT_TRUE(Parse(maps, /*fileBackedOnly=*/true, &regions, &count));
+  ASSERT_EQ(count, 2u);
+  EXPECT_EQ(regions[0].start, 0x1000u);
+  EXPECT_EQ(regions[0].end, 0x3000u); // coalesced r--p + r-xp
+  EXPECT_EQ(regions[1].start, 0xa000u);
+  EXPECT_EQ(regions[1].end, 0xb000u);
+  std::free(regions);
+
+  // Permissive filter (confirm mode) also keeps anonymous/shared/deleted
+  // read-only entries.
+  ASSERT_TRUE(Parse(maps, /*fileBackedOnly=*/false, &regions, &count));
+  ASSERT_EQ(count, 3u);
+  EXPECT_EQ(regions[1].start, 0x5000u);
+  EXPECT_EQ(regions[1].end, 0x9000u); // anon+shared+deleted+vvar coalesced
+  EXPECT_EQ(regions[2].start, 0xa000u);
+  std::free(regions);
+}
+
+TEST(MemoryMapParse, FailClosedOnAnomalies) {
+  Region *regions{nullptr};
+  std::size_t count{0};
+  const char *bad[]{
+      "1000-2000 r--p 00000000 08:01 41 /lib/a.so", // no trailing newline
+      "2000-1000 r--p 00000000 08:01 41 /lib/a.so\n", // end <= start
+      "1000-2000 q--p 00000000 08:01 41 /lib/a.so\n", // bad perm char
+      "1000-2000 r--p 00000000 0801 41 /lib/a.so\n", // malformed dev field
+      "1000-2000 r--p 00000000 08:01 x /lib/a.so\n", // non-numeric inode
+      "zzzz-2000 r--p 00000000 08:01 41 /lib/a.so\n", // bad hex
+      "1000-2000\n", // truncated fields
+      // out of order
+      "2000-3000 r--p 00000000 08:01 41 /a\n"
+      "1000-1800 r--p 00000000 08:01 41 /a\n",
+      // overlapping
+      "1000-3000 r--p 00000000 08:01 41 /a\n"
+      "2000-4000 r--p 00000000 08:01 41 /a\n",
+  };
+  for (const char *entry : bad) {
+    std::string s{entry};
+    EXPECT_FALSE(Parse(s, true, &regions, &count)) << "input: " << entry;
+    EXPECT_EQ(regions, nullptr) << "input: " << entry;
+  }
+  // Anomalies after an acceptable prefix must not publish the prefix.
+  std::string prefixThenBad{"1000-2000 r--p 00000000 08:01 41 /lib/a.so\n"
+                            "3000-2800 r--p 00000000 08:01 41 /lib/a.so\n"};
+  EXPECT_FALSE(Parse(prefixThenBad, true, &regions, &count));
+  EXPECT_EQ(regions, nullptr);
+}
+
+//===----------------------------------------------------------------------===//
+// SpanIsContained
+//===----------------------------------------------------------------------===//
+
+TEST(MemoryMapSpan, Containment) {
+  Region regions[]{{0x1000, 0x3000}, {0x5000, 0x6000}};
+  EXPECT_TRUE(SpanIsContained(0x1000, 0x3000, regions, 2));
+  EXPECT_TRUE(SpanIsContained(0x1800, 0x2800, regions, 2));
+  EXPECT_TRUE(SpanIsContained(0x5fff, 0x6000, regions, 2));
+  EXPECT_FALSE(SpanIsContained(0x0fff, 0x2000, regions, 2)); // starts before
+  EXPECT_FALSE(SpanIsContained(0x2000, 0x3001, regions, 2)); // ends after
+  EXPECT_FALSE(SpanIsContained(0x3000, 0x5000, regions, 2)); // gap
+  EXPECT_FALSE(SpanIsContained(0x4000, 0x4800, regions, 2)); // hole
+  EXPECT_FALSE(SpanIsContained(0x2000, 0x2000, regions, 2)); // empty span
+  EXPECT_FALSE(SpanIsContained(0x1000, 0x2000, nullptr, 0)); // empty table
+}
+
+//===----------------------------------------------------------------------===//
+// ComputeDataSpan
+//===----------------------------------------------------------------------===//
+
+TEST(MemoryMapSpan, DescriptorSpans) {
+  auto array{MakeArray<TypeCategory::Integer, 4>(std::vector<int>{8},
+      std::vector<std::int32_t>{1, 2, 3, 4, 5, 6, 7, 8}, sizeof(std::int32_t))};
+  std::uintptr_t lo{0}, hi{0};
+  ASSERT_TRUE(ComputeDataSpan(*array, lo, hi));
+  auto base{reinterpret_cast<std::uintptr_t>(array->OffsetElement<char>())};
+  EXPECT_EQ(lo, base);
+  EXPECT_EQ(hi, base + 8 * sizeof(std::int32_t));
+
+  // Strided view: elements 1,3,5,7 - span still covers first..last touched.
+  array->GetDimension(0).SetByteStride(2 * sizeof(std::int32_t));
+  array->GetDimension(0).SetExtent(4);
+  ASSERT_TRUE(ComputeDataSpan(*array, lo, hi));
+  EXPECT_EQ(lo, base);
+  EXPECT_EQ(hi, base + 6 * sizeof(std::int32_t) + sizeof(std::int32_t));
+
+  // Negative stride: base points at the LAST touched element.
+  array->set_base_addr(array->OffsetElement<char>(6 * sizeof(std::int32_t)));
+  array->GetDimension(0).SetByteStride(
+      -2 * static_cast<std::int64_t>(sizeof(std::int32_t)));
+  ASSERT_TRUE(ComputeDataSpan(*array, lo, hi));
+  EXPECT_EQ(lo, base);
+  EXPECT_EQ(hi, base + 7 * sizeof(std::int32_t));
+
+  // Zero extent => false.
+  array->GetDimension(0).SetExtent(0);
+  EXPECT_FALSE(ComputeDataSpan(*array, lo, hi));
+}
+
+//===----------------------------------------------------------------------===//
+// Windows protection classification (pure function; runs on any host)
+//===----------------------------------------------------------------------===//
+
+TEST(MemoryMapWindows, ProtectionClassification) {
+  constexpr std::uint32_t commit{0x1000}, reserve{0x2000}, image{0x1000000},
+      priv{0x20000};
+  constexpr std::uint32_t ro{0x02}, rw{0x04}, wc{0x08}, xr{0x20}, xwc{0x80},
+      guard{0x100}, nocache{0x200};
+  // Accepted: committed image PAGE_READONLY / PAGE_EXECUTE_READ.
+  EXPECT_TRUE(ProtectionIsReadOnly(commit, ro, image, true));
+  EXPECT_TRUE(ProtectionIsReadOnly(commit, xr, image, true));
+  // Write-copy protections are writable-on-fault: never read-only.
+  EXPECT_FALSE(ProtectionIsReadOnly(commit, wc, image, true));
+  EXPECT_FALSE(ProtectionIsReadOnly(commit, xwc, image, true));
+  // Modifier bits (guard, nocache) reject the region outright.
+  EXPECT_FALSE(ProtectionIsReadOnly(commit, ro | guard, image, true));
+  EXPECT_FALSE(ProtectionIsReadOnly(commit, ro | nocache, image, true));
+  // Writable / no-access / reserved states.
+  EXPECT_FALSE(ProtectionIsReadOnly(commit, rw, image, true));
+  EXPECT_FALSE(ProtectionIsReadOnly(reserve, ro, image, true));
+  // imageOnly (trust table) rejects private RO; permissive mode keeps it.
+  EXPECT_FALSE(ProtectionIsReadOnly(commit, ro, priv, true));
+  EXPECT_TRUE(ProtectionIsReadOnly(commit, ro, priv, false));
+}
+
+//===----------------------------------------------------------------------===//
+// Behavioral arms (Linux; each runs in a fresh subprocess because the mode
+// and the snapshot latch once per process)
+//===----------------------------------------------------------------------===//
+
+#if defined(__linux__)
+
+namespace {
+// A file-backed private read-only mapping of one page holding 'count' int32
+// values - the shape the trust-mode table keeps (inode != 0, private, r--).
+struct RoFileMapping {
+  static constexpr std::size_t count{16};
+  std::int32_t *data{nullptr};
+  std::size_t pageSize{0};
+
+  bool Map(bool readOnlyNow) {
+    pageSize = static_cast<std::size_t>(::sysconf(_SC_PAGESIZE));
+    // The file must stay linked while mapped: an unlinked-but-mapped file
+    // shows as "(deleted)" in /proc/self/maps, which the trust-mode filter
+    // rejects. Unlink happens in the destructor.
+    std::strcpy(path_, "copyout-romap-XXXXXX");
+    int fd{::mkstemp(path_)};
+    if (fd < 0) {
+      return false;
+    }
+    std::vector<std::int32_t> initial(pageSize / sizeof(std::int32_t));
+    for (std::size_t i{0}; i < initial.size(); ++i) {
+      initial[i] = static_cast<std::int32_t>(i + 1);
+    }
+    if (::write(fd, initial.data(), pageSize) !=
+        static_cast<::ssize_t>(pageSize)) {
+      ::close(fd);
+      return false;
+    }
+    void *p{
+        ::mmap(nullptr, pageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0)};
+    ::close(fd);
+    if (p == MAP_FAILED) {
+      return false;
+    }
+    if (readOnlyNow && ::mprotect(p, pageSize, PROT_READ) != 0) {
+      return false;
+    }
+    data = static_cast<std::int32_t *>(p);
+    return true;
+  }
+  ~RoFileMapping() {
+    if (data) {
+      ::munmap(data, pageSize);
+    }
+    if (path_[0]) {
+      ::unlink(path_);
+    }
+  }
+  char path_[32]{};
+};
+
+} // namespace
+
+// Headline: a MODIFIED temporary copied out into read-only storage does not
+// fault and stores nothing in mode 1 - the exact case that faults with the
+// feature off.
+TEST(MemoryMapCopyOut, Mode1SkipsModifiedTempIntoReadOnly) {
+  testing::FLAGS_gtest_death_test_style = "threadsafe";
+  EXPECT_EXIT(
+      {
+        ::setenv("FLANG_RT_COPYOUT_READONLY_MODE", "1", 1);
+        RoFileMapping m;
+        if (!m.Map(/*readOnlyNow=*/true)) {
+          _exit(2);
+        }
+        SubscriptValue extent[1]{RoFileMapping::count};
+        StaticDescriptor<1> staticVar;
+        Descriptor &var{staticVar.descriptor()};
+        var.Establish(
+            TypeCategory::Integer, sizeof(std::int32_t), m.data, 1, extent);
+        StaticDescriptor<1> staticTemp;
+        Descriptor &temp{staticTemp.descriptor()};
+        RTNAME(CopyInAssign)(temp, var);
+        *temp.OffsetElement<std::int32_t>(0) = 999; // invalid-program write
+        RTNAME(CopyOutAssign)(&var, temp, __FILE__, __LINE__);
+        // Reaching here means no fault; verify nothing was stored.
+        _exit(m.data[0] == 1 ? 0 : 3);
+      },
+      testing::ExitedWithCode(0), "");
+}
+
+// Staleness, mode 1: after mprotect RO->RW (post-snapshot), the stale trust
+// table still skips - the DOCUMENTED accepted lost write of trust mode.
+TEST(MemoryMapCopyOut, Mode1StalenessSkipsAfterReprotect) {
+  testing::FLAGS_gtest_death_test_style = "threadsafe";
+  EXPECT_EXIT(
+      {
+        ::setenv("FLANG_RT_COPYOUT_READONLY_MODE", "1", 1);
+        RoFileMapping m;
+        if (!m.Map(/*readOnlyNow=*/true)) {
+          _exit(2);
+        }
+        SubscriptValue extent[1]{RoFileMapping::count};
+        StaticDescriptor<1> staticVar;
+        Descriptor &var{staticVar.descriptor()};
+        var.Establish(
+            TypeCategory::Integer, sizeof(std::int32_t), m.data, 1, extent);
+        // Force the snapshot while the page is read-only.
+        StaticDescriptor<1> staticTemp;
+        Descriptor &temp{staticTemp.descriptor()};
+        RTNAME(CopyInAssign)(temp, var);
+        RTNAME(CopyOutAssign)(&var, temp, __FILE__, __LINE__);
+        // Now make it writable and try a real copy-out.
+        if (::mprotect(m.data, m.pageSize, PROT_READ | PROT_WRITE) != 0) {
+          _exit(2);
+        }
+        StaticDescriptor<1> staticTemp2;
+        Descriptor &temp2{staticTemp2.descriptor()};
+        RTNAME(CopyInAssign)(temp2, var);
+        *temp2.OffsetElement<std::int32_t>(0) = 999;
+        RTNAME(CopyOutAssign)(&var, temp2, __FILE__, __LINE__);
+        // Trust mode consults the stale table: the write is (documentedly)
+        // lost.
+        _exit(m.data[0] == 1 ? 0 : 3);
+      },
+      testing::ExitedWithCode(0), "");
+}
+
+// Staleness, mode 2: same scenario, but confirm-on-hit sees the current RW
+// protection and the write goes through.
+TEST(MemoryMapCopyOut, Mode2ConfirmWritesAfterReprotect) {
+  testing::FLAGS_gtest_death_test_style = "threadsafe";
+  EXPECT_EXIT(
+      {
+        ::setenv("FLANG_RT_COPYOUT_READONLY_MODE", "2", 1);
+        RoFileMapping m;
+        if (!m.Map(/*readOnlyNow=*/true)) {
+          _exit(2);
+        }
+        SubscriptValue extent[1]{RoFileMapping::count};
+        StaticDescriptor<1> staticVar;
+        Descriptor &var{staticVar.descriptor()};
+        var.Establish(
+            TypeCategory::Integer, sizeof(std::int32_t), m.data, 1, extent);
+        StaticDescriptor<1> staticTemp;
+        Descriptor &temp{staticTemp.descriptor()};
+        RTNAME(CopyInAssign)(temp, var);
+        RTNAME(CopyOutAssign)(&var, temp, __FILE__, __LINE__); // snapshot
+        if (::mprotect(m.data, m.pageSize, PROT_READ | PROT_WRITE) != 0) {
+          _exit(2);
+        }
+        StaticDescriptor<1> staticTemp2;
+        Descriptor &temp2{staticTemp2.descriptor()};
+        RTNAME(CopyInAssign)(temp2, var);
+        *temp2.OffsetElement<std::int32_t>(0) = 999;
+        RTNAME(CopyOutAssign)(&var, temp2, __FILE__, __LINE__);
+        _exit(m.data[0] == 999 ? 0 : 3);
+      },
+      testing::ExitedWithCode(0), "");
+}
+
+// Mode 2 with the page still read-only: candidate confirmed, skip, no fault.
+TEST(MemoryMapCopyOut, Mode2SkipsModifiedTempIntoReadOnly) {
+  testing::FLAGS_gtest_death_test_style = "threadsafe";
+  EXPECT_EXIT(
+      {
+        ::setenv("FLANG_RT_COPYOUT_READONLY_MODE", "2", 1);
+        RoFileMapping m;
+        if (!m.Map(/*readOnlyNow=*/true)) {
+          _exit(2);
+        }
+        SubscriptValue extent[1]{RoFileMapping::count};
+        StaticDescriptor<1> staticVar;
+        Descriptor &var{staticVar.descriptor()};
+        var.Establish(
+            TypeCategory::Integer, sizeof(std::int32_t), m.data, 1, extent);
+        StaticDescriptor<1> staticTemp;
+        Descriptor &temp{staticTemp.descriptor()};
+        RTNAME(CopyInAssign)(temp, var);
+        *temp.OffsetElement<std::int32_t>(0) = 999;
+        RTNAME(CopyOutAssign)(&var, temp, __FILE__, __LINE__);
+        _exit(m.data[0] == 1 ? 0 : 3);
+      },
+      testing::ExitedWithCode(0), "");
+}
+
+// Feature off (default): the same modified-temp-into-RO copy-out faults - the
+// upstream propagate-and-catch behavior is preserved.
+TEST(MemoryMapCopyOut, ModeOffStillFaults) {
+  testing::FLAGS_gtest_death_test_style = "threadsafe";
+  EXPECT_EXIT(
+      {
+        ::unsetenv("FLANG_RT_COPYOUT_READONLY_MODE");
+        RoFileMapping m;
+        if (!m.Map(/*readOnlyNow=*/true)) {
+          _exit(2);
+        }
+        SubscriptValue extent[1]{RoFileMapping::count};
+        StaticDescriptor<1> staticVar;
+        Descriptor &var{staticVar.descriptor()};
+        var.Establish(
+            TypeCategory::Integer, sizeof(std::int32_t), m.data, 1, extent);
+        StaticDescriptor<1> staticTemp;
+        Descriptor &temp{staticTemp.descriptor()};
+        RTNAME(CopyInAssign)(temp, var);
+        *temp.OffsetElement<std::int32_t>(0) = 999;
+        RTNAME(CopyOutAssign)(&var, temp, __FILE__, __LINE__);
+        _exit(0); // not reached
+      },
+      testing::KilledBySignal(SIGSEGV), "");
+}
+
+// Writable destinations round-trip unchanged with the feature on.
+TEST(MemoryMapCopyOut, Mode1WritableDestUnaffected) {
+  testing::FLAGS_gtest_death_test_style = "threadsafe";
+  EXPECT_EXIT(
+      {
+        ::setenv("FLANG_RT_COPYOUT_READONLY_MODE", "1", 1);
+        RoFileMapping m;
+        if (!m.Map(/*readOnlyNow=*/false)) { // stays RW
+          _exit(2);
+        }
+        SubscriptValue extent[1]{RoFileMapping::count};
+        StaticDescriptor<1> staticVar;
+        Descriptor &var{staticVar.descriptor()};
+        var.Establish(
+            TypeCategory::Integer, sizeof(std::int32_t), m.data, 1, extent);
+        StaticDescriptor<1> staticTemp;
+        Descriptor &temp{staticTemp.descriptor()};
+        RTNAME(CopyInAssign)(temp, var);
+        *temp.OffsetElement<std::int32_t>(0) = 999;
+        RTNAME(CopyOutAssign)(&var, temp, __FILE__, __LINE__);
+        _exit(m.data[0] == 999 ? 0 : 3);
+      },
+      testing::ExitedWithCode(0), "");
+}
+
+#endif // __linux__
diff --git a/flang/docs/RuntimeEnvironment.md b/flang/docs/RuntimeEnvironment.md
index 0d3e274328a8a..453bb99aca66e 100644
--- a/flang/docs/RuntimeEnvironment.md
+++ b/flang/docs/RuntimeEnvironment.md
@@ -49,6 +49,28 @@ argument.
 Set the system environment variable `FLANG_RT_COPYOUT_MODIFIED_ONLY=0` to
 restore the unconditional copy-out.
 
+## `FLANG_RT_COPYOUT_READONLY_MODE`
+
+An optional compatibility mode (host only; default `0` = off). When enabled,
+the runtime consults the process memory map and skips a copy-out whose
+destination lies in read-only memory: such a store could only rewrite
+identical bytes or crash, so skipping converts the crash into a no-op for
+programs that (invalidly) modified a temporary whose original is not
+definable.
+
+* `1`: trust a one-time lazy snapshot of the memory map (restricted to
+  file-backed private read-only mappings); no system calls on the copy-out
+  path. A mapping whose protection changes after the snapshot is not seen:
+  a region that became read-only is simply not recognized (the regular
+  copy-out runs, as without this feature), and a formerly read-only region
+  that became writable is still skipped (the copy-out is lost). Both are
+  accepted, documented behaviors of this mode.
+* `2`: additionally re-confirm each snapshot hit against the current memory
+  map before skipping (system calls on hits only).
+
+Set `FLANG_RT_COPYOUT_READONLY_DIAG=1` to report the first few skipped
+copy-outs on standard error.
+
 ## `FORT_CHECK_POINTER_DEALLOCATION`
 
 Fortran requires that a pointer that appears in a `DEALLOCATE` statement



More information about the flang-commits mailing list