[lld] [lld/ELF] Add --override-section-flags flag (PR #109454)

Nico Weber via llvm-commits llvm-commits at lists.llvm.org
Fri Sep 20 11:14:52 PDT 2024


https://github.com/nico created https://github.com/llvm/llvm-project/pull/109454

The motivation is protection against heap-spraying attacks: Put security-critical variables (think IPC-related) into a special section that's usually mapped read-only, but which application code temporarily manually remaps as writeable while the variables in there are written to. That way, these variables can't be written to during heap spraying.

The section these variables are in should be mapped read-only at program start.

Since the variables in the section aren't constant, source-level techniques like making the variables `const` don't work: That way, LLVM assumes they are constant for optimization purposes.

On Windows, this can be achieved, using both link.exe and lld-link.exe by putting the variables in a special section and passing `/SECTION:mysect,R` to the linker to tell it to mark that section as read-only. It can then be remapped as writeable at runtime.

On Mac, ld64 and ld64.lld have a `-segprot MYSEG max init` flag that can pick different max and initial segment protections. After #107269, it's possible to use this flag with at least ld64.lld as `-segprot MYSEG rw r` to achieve the desired effect.

For ELF, with gcc it's possible to do a bobby tables attack on gcc's section attribute like so:

    unsigned int __attribute__((section(".myVarSection,\"a\", at progbits #"))) myVar;

This gets translated into assembly without any escaping and results in:

    .section    .myVarSection,"a", at progbits #,"aw", at progbits

This allows us to smuggle through custom section flags. But while it's funny and it does work, this:

* is very hacky
* probably needs an arch-dependent comment character at the end
* doesn't work with clang.

With clang, it used to be possible to instead do

    __asm__(".section protected_memory, \"a\"\n\t");
    constinit __attribute__((section("protected_memory")))

and the merging used to work -- but #50551 "broke" this and this no works.

So this patch adds an --override-section-flags flag to LLD that allows changing the flags of a section at link time, just as you can in lld/COFF (and link.exe) and in lld/MachO. See the PR adding this commit for a complete example.

It's technically also possible to achieve this by using a linker script, but while linker scripts are common for embedded and kernel work, they are rare for userland programs. People don't know them well, and you'd have to write a > 100 LoC linker script per arch. Also, LLD has historically added targeted flags instead of requiring users to reach for linker scripts, and this follows that tradition. (The PR adding this commit also has a proof-of-concept for this approach.)

>From 81ec4b24152e07448a2e449d0cb40880c7c322c6 Mon Sep 17 00:00:00 2001
From: Nico Weber <thakis at chromium.org>
Date: Tue, 17 Sep 2024 11:36:13 -0400
Subject: [PATCH] [lld/ELF] Add --override-section-flags flag

The motivation is protection against heap-spraying attacks: Put
security-critical variables (think IPC-related) into a special section
that's usually mapped read-only, but which application code temporarily
manually remaps as writeable while the variables in there are written
to. That way, these variables can't be written to during heap spraying.

The section these variables are in should be mapped read-only at program
start.

Since the variables in the section aren't constant, source-level
techniques like making the variables `const` don't work: That way,
LLVM assumes they are constant for optimization purposes.

On Windows, this can be achieved, using both link.exe and lld-link.exe
by putting the variables in a special section and passing
`/SECTION:mysect,R` to the linker to tell it to mark that section
as read-only. It can then be remapped as writeable at runtime.

On Mac, ld64 and ld64.lld have a `-segprot MYSEG max init` flag that
can pick different max and initial segment protections. After #107269,
it's possible to use this flag with at least ld64.lld as
`-segprot MYSEG rw r` to achieve the desired effect.

For ELF, with gcc it's possible to do a bobby tables attack on
gcc's section attribute like so:

    unsigned int __attribute__((section(".myVarSection,\"a\", at progbits #"))) myVar;

This gets translated into assembly without any escaping and results in:

    .section    .myVarSection,"a", at progbits #,"aw", at progbits

This allows us to smuggle through custom section flags. But while it's
funny and it does work, this:

* is very hacky
* probably needs an arch-dependent comment character at the end
* doesn't work with clang.

With clang, it used to be possible to instead do

    __asm__(".section protected_memory, \"a\"\n\t");
    constinit __attribute__((section("protected_memory")))

and the merging used to work -- but #50551 "broke" this and this no
works.

So this patch adds an --override-section-flags flag to LLD that allows
changing the flags of a section at link time, just as you can in
lld/COFF (and link.exe) and in lld/MachO. See the PR adding this commit
for a complete example.

It's technically also possible to achieve this by using a linker script,
but while linker scripts are common for embedded and kernel work,
they are rare for userland programs. People don't know them well,
and you'd have to write a > 100 LoC linker script per arch.
Also, LLD has historically added targeted flags instead of requiring
users to reach for linker scripts, and this follows that tradition.
(The PR adding this commit also has a proof-of-concept for this approach.)
---
 lld/ELF/Config.h                      |  2 ++
 lld/ELF/Driver.cpp                    | 29 ++++++++++++++++++++
 lld/ELF/InputSection.cpp              | 12 +++++++--
 lld/ELF/Options.td                    |  4 +++
 lld/test/ELF/override-section-flags.s | 38 +++++++++++++++++++++++++++
 5 files changed, 83 insertions(+), 2 deletions(-)
 create mode 100644 lld/test/ELF/override-section-flags.s

diff --git a/lld/ELF/Config.h b/lld/ELF/Config.h
index 7cae8677ef5ce1..c123b5c23afcf7 100644
--- a/lld/ELF/Config.h
+++ b/lld/ELF/Config.h
@@ -309,6 +309,8 @@ struct Config {
   bool optEL = false;
   bool optimizeBBJumps;
   bool optRemarksWithHotness;
+  llvm::SmallVector<std::tuple<llvm::GlobPattern, uint32_t>, 0>
+      overrideSectionFlags;
   bool picThunk;
   bool pie;
   bool printGcSections;
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index e25db0e4951275..c7de9eb0f90875 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -1611,6 +1611,35 @@ static void readConfigs(Ctx &ctx, opt::InputArgList &args) {
     }
   }
 
+  for (opt::Arg *arg : args.filtered(OPT_override_section_flags)) {
+    SmallVector<StringRef, 0> fields;
+    StringRef(arg->getValue()).split(fields, '=');
+    if (fields.size() != 2) {
+      error(arg->getSpelling() +
+            ": parse error, no '=' found in --override-section-flags arg");
+      continue;
+    }
+
+    uint32_t flags = 0;
+    for (char c : fields[1]) {
+      if (c == 'a')
+        flags |= SHF_ALLOC;
+      else if (c == 'w')
+        flags |= SHF_WRITE;
+      else if (c == 'x')
+        flags |= SHF_EXECINSTR;
+      else
+        error(arg->getSpelling() + ": flags do not match [awx]+");
+    }
+
+    if (Expected<GlobPattern> pat = GlobPattern::create(fields[0])) {
+      config->overrideSectionFlags.emplace_back(std::move(*pat), flags);
+    } else {
+      error(arg->getSpelling() + ": " + toString(pat.takeError()));
+      continue;
+    }
+  }
+
   for (opt::Arg *arg : args.filtered(OPT_z)) {
     std::pair<StringRef, StringRef> option =
         StringRef(arg->getValue()).split('=');
diff --git a/lld/ELF/InputSection.cpp b/lld/ELF/InputSection.cpp
index 9601e6b3250cc0..085221ac5dcf43 100644
--- a/lld/ELF/InputSection.cpp
+++ b/lld/ELF/InputSection.cpp
@@ -47,13 +47,21 @@ static ArrayRef<uint8_t> getSectionContents(ObjFile<ELFT> &file,
   return check(file.getObj().getSectionContents(hdr));
 }
 
+uint32_t effectiveSectionFlags(uint32_t flags, StringRef name) {
+  for (auto &[glob, overriddenFlags] : config->overrideSectionFlags) {
+    if (glob.match(name))
+      return overriddenFlags;
+  }
+  return flags;
+}
+
 InputSectionBase::InputSectionBase(InputFile *file, uint64_t flags,
                                    uint32_t type, uint64_t entsize,
                                    uint32_t link, uint32_t info,
                                    uint32_t addralign, ArrayRef<uint8_t> data,
                                    StringRef name, Kind sectionKind)
-    : SectionBase(sectionKind, name, flags, entsize, addralign, type, info,
-                  link),
+    : SectionBase(sectionKind, name, effectiveSectionFlags(flags, name),
+                  entsize, addralign, type, info, link),
       file(file), content_(data.data()), size(data.size()) {
   // In order to reduce memory allocation, we assume that mergeable
   // sections are smaller than 4 GiB, which is not an unreasonable
diff --git a/lld/ELF/Options.td b/lld/ELF/Options.td
index c80c4017d3512c..cb89fece4d9560 100644
--- a/lld/ELF/Options.td
+++ b/lld/ELF/Options.td
@@ -353,6 +353,10 @@ def omagic: FF<"omagic">, MetaVarName<"<magic>">,
 defm orphan_handling:
   Eq<"orphan-handling", "Control how orphan sections are handled when linker script used">;
 
+defm override_section_flags:
+  EEq<"override-section-flags", "Override section flags">,
+  MetaVarName<"<section-glob>=[awx]+">;
+
 defm pack_dyn_relocs:
   EEq<"pack-dyn-relocs", "Pack dynamic relocations in the given format">,
   MetaVarName<"[none,android,relr,android+relr]">;
diff --git a/lld/test/ELF/override-section-flags.s b/lld/test/ELF/override-section-flags.s
new file mode 100644
index 00000000000000..937bb788c63d86
--- /dev/null
+++ b/lld/test/ELF/override-section-flags.s
@@ -0,0 +1,38 @@
+# REQUIRES: x86
+
+# RUN: rm -rf %t && mkdir %t
+# RUN: llvm-mc -filetype=obj -triple=x86_64 %s -o %t/a.o
+
+# RUN: ld.lld -pie %t/a.o -o %t/out \
+# RUN:     --override-section-flags 'foo0=' \
+# RUN:     --override-section-flags 'foo1=a' \
+# RUN:     --override-section-flags 'foo2=ax' \
+# RUN:     --override-section-flags 'foo3=aw' \
+# RUN:     --override-section-flags 'foo4=awx'
+
+# RUN: llvm-readelf --sections --segments %t/out | FileCheck %s
+
+# CHECK-DAG: foo0 PROGBITS {{[0-9a-f]+}} {{[0-9a-f]+}} {{[0-9a-f]+}} {{[0-9a-f]+}}     {{[0-9a-f]+}} {{[0-9a-f]+}} {{[0-9a-f]+}}
+# CHECK-DAG: foo1 PROGBITS {{[0-9a-f]+}} {{[0-9a-f]+}} {{[0-9a-f]+}} {{[0-9a-f]+}} A   {{[0-9a-f]+}} {{[0-9a-f]+}} {{[0-9a-f]+}}
+# CHECK-DAG: foo2 PROGBITS {{[0-9a-f]+}} {{[0-9a-f]+}} {{[0-9a-f]+}} {{[0-9a-f]+}} AX  {{[0-9a-f]+}} {{[0-9a-f]+}} {{[0-9a-f]+}}
+# CHECK-DAG: foo3 PROGBITS {{[0-9a-f]+}} {{[0-9a-f]+}} {{[0-9a-f]+}} {{[0-9a-f]+}} WA  {{[0-9a-f]+}} {{[0-9a-f]+}} {{[0-9a-f]+}}
+# CHECK-DAG: foo4 PROGBITS {{[0-9a-f]+}} {{[0-9a-f]+}} {{[0-9a-f]+}} {{[0-9a-f]+}} WAX {{[0-9a-f]+}} {{[0-9a-f]+}} {{[0-9a-f]+}}
+
+
+.globl _start
+_start:
+
+.section foo0,"aw"
+.space 8
+
+.section foo1,"aw"
+.space 8
+
+.section foo2,"aw"
+.space 8
+
+.section foo3,"ax"
+.space 8
+
+.section foo4,"a"
+.space 8



More information about the llvm-commits mailing list