llvm-project/flang/lib/Optimizer/Passes/CommandLineOpts.cpp
Sairudra More 111bafff9b
[flang] Add runtime trampoline pool for W^X compliance (#183108)
Flang currently lowers internal procedures passed as actual arguments
using LLVM's `llvm.init.trampoline` / `llvm.adjust.trampoline`
intrinsics, which require an executable stack. On modern Linux
toolchains and security-hardened kernels that enforce W^X (Write XOR
Execute), this causes link-time failures (`ld.lld: error: ... requires
an executable stack`) or runtime `SEGV` from NX violations.

This patch introduces a runtime trampoline pool that allocates
trampolines from a dedicated `mmap`'d region instead of the stack. The
pool toggles page permissions between writable (for patching) and
executable (for dispatch), so the stack stays non-executable throughout.
On macOS, MAP_JIT and `pthread_jit_write_protect_np` are used for the
same effect. An i-cache flush (`__builtin___clear_cache` on Linux,
`sys_icache_invalidate` on macOS) is performed after each write→exec
transition.

The feature is gated behind a new driver flag, `-fsafe-trampoline` (off
by default), which threads through the frontend into the
`BoxedProcedurePass`. When enabled, the pass emits calls to
`_FortranATrampolineInit`, `_FortranATrampolineAdjust`, and
`_FortranATrampolineFree` instead of the legacy intrinsics. The legacy
path is completely untouched when the flag is off.

The pool is a singleton with a fixed capacity (default 1024 slots,
overridable via `FLANG_TRAMPOLINE_POOL_SIZE`). Slot size varies by
target (32 bytes on x86-64/AArch64, 48 on PPC64, 64 fallback). Each slot
holds a small architecture-specific stub, currently x86-64 (17 bytes,
using `r10` as the nest/static-chain register) and AArch64 (24 bytes,
using `x15`). The implementation compiles on all architectures but will
crash at runtime with a clear diagnostic if trampoline emission is
actually attempted on an unsupported target. This avoids breaking the
flang-rt build on e.g. RISC-V or PPC64.

Freed slots are poisoned (the callee pointer is overwritten with a
sentinel) and recycled into a freelist, so the pool can sustain
long-running programs that repeatedly create and destroy closures.

A few design choices worth calling out:

The runtime avoids all C++ runtime dependencies, no `std::mutex`, no
`operator new`, no function-local statics with hidden guard variables.
Locking is via flang-rt's own `Lock` / `CriticalSection`, memory is via
`AllocateMemoryOrCrash` / `FreeMemory`, and the singleton uses explicit
double-checked locking with a raw pointer. This was done so the
trampoline pool links cleanly in minimal / freestanding flang-rt
configurations.

`_FortranATrampolineFree` calls are inserted immediately before every
`func.return` in the enclosing host function. This is a conservative but
correct strategy. The trampoline handle cannot outlive the host's stack
frame since the closure captures the host's local variables by
reference.

The GNU_STACK note is verified via a dedicated integration test
(`safe-trampoline-gnustack.f90`) that compiles and links a Fortran
program using the runtime path, then inspects the ELF with
`llvm-readelf` to confirm the stack segment is `RW` (not `RWE`).

**Test coverage:**

- `flang/test/Driver/fsafe-trampoline.f90` — flag forwarding (on, off,
default)
- `flang/test/Fir/boxproc-safe-trampoline.fir` — FIR-level FileCheck for
emitted runtime calls
- `flang/test/Lower/safe-trampoline.f90` — end-to-end lowering
- `flang-rt/test/Driver/safe-trampoline-gnustack.f90` — GNU_STACK ELF
verification

Closes #182813

Co-authored-by: Sairudra More <moresair@pe31.hpc.amslabs.hpecorp.net>
2026-03-10 16:16:05 +05:30

83 lines
3.7 KiB
C++

//===-- CommandLineOpts.cpp -- shared command line options ------*- 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
//
//===----------------------------------------------------------------------===//
/// This file defines some shared command-line options that can be used when
/// debugging the test tools.
#include "flang/Optimizer/Passes/CommandLineOpts.h"
using namespace llvm;
#define DisableOption(DOName, DOOption, DODescription) \
cl::opt<bool> disable##DOName("disable-" DOOption, \
cl::desc("disable " DODescription " pass"), \
cl::init(false), cl::Hidden)
#define EnableOption(EOName, EOOption, EODescription) \
cl::opt<bool> enable##EOName("enable-" EOOption, \
cl::desc("enable " EODescription " pass"), \
cl::init(false), cl::Hidden)
cl::opt<bool> dynamicArrayStackToHeapAllocation(
"fdynamic-heap-array",
cl::desc("place all array allocations of dynamic size on the heap"),
cl::init(false), cl::Hidden);
cl::opt<std::size_t> arrayStackAllocationThreshold(
"fstack-array-size",
cl::desc(
"place all array allocations more than <size> elements on the heap"),
cl::init(~static_cast<std::size_t>(0)), cl::Hidden);
cl::opt<bool> ignoreMissingTypeDescriptors(
"ignore-missing-type-desc",
cl::desc("ignore failures to find derived type descriptors when "
"translating FIR to LLVM"),
cl::init(false), cl::Hidden);
cl::opt<bool> skipExternalRttiDefinition(
"skip-external-rtti-definition", llvm::cl::init(false),
llvm::cl::desc("do not define rtti static objects for types belonging to "
"other compilation units"),
cl::Hidden);
OptimizationLevel defaultOptLevel{OptimizationLevel::O0};
codegenoptions::DebugInfoKind noDebugInfo{codegenoptions::NoDebugInfo};
/// Optimizer Passes
DisableOption(CfgConversion, "cfg-conversion", "disable FIR to CFG pass");
DisableOption(FirAvc, "avc", "array value copy analysis and transformation");
DisableOption(FirMao, "memory-allocation-opt",
"memory allocation optimization");
DisableOption(FirAliasTags, "fir-alias-tags", "fir alias analysis");
cl::opt<bool> useOldAliasTags(
"use-old-alias-tags",
cl::desc("Use a single TBAA tree for all functions and do not use "
"the FIR alias tags pass"),
cl::init(false), cl::Hidden);
EnableOption(FirLICM, "fir-licm", "FIR loop invariant code motion");
/// CodeGen Passes
DisableOption(CodeGenRewrite, "codegen-rewrite", "rewrite FIR for codegen");
DisableOption(TargetRewrite, "target-rewrite", "rewrite FIR for target");
DisableOption(DebugInfo, "debug-info", "Add debug info");
DisableOption(FirToLlvmIr, "fir-to-llvmir", "FIR to LLVM-IR dialect");
DisableOption(LlvmIrToLlvm, "llvm", "conversion to LLVM");
DisableOption(BoxedProcedureRewrite, "boxed-procedure-rewrite",
"rewrite boxed procedures");
EnableOption(SafeTrampoline, "safe-trampoline",
"W^X compliant runtime trampoline pool");
DisableOption(ExternalNameConversion, "external-name-interop",
"convert names with external convention");
EnableOption(ConstantArgumentGlobalisation, "constant-argument-globalisation",
"the local constant argument to global constant conversion");
DisableOption(CompilerGeneratedNamesConversion, "compiler-generated-names",
"replace special symbols in compiler generated names");