Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

nico
Copy link
Contributor

@nico nico commented Sep 20, 2024

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\",@progbits #"))) myVar;

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

.section    .myVarSection,"a",@progbits #,"aw",@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.)

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 llvm#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\",@progbits #"))) myVar;

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

    .section    .myVarSection,"a",@progbits #,"aw",@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 llvm#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.)
@llvmbot
Copy link
Member

llvmbot commented Sep 20, 2024

@llvm/pr-subscribers-lld-elf

@llvm/pr-subscribers-lld

Author: Nico Weber (nico)

Changes

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\",@<!-- -->progbits #"))) myVar;

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

.section    .myVarSection,"a",@<!-- -->progbits #,"aw",@<!-- -->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.)


Full diff: https://github.com/llvm/llvm-project/pull/109454.diff

5 Files Affected:

  • (modified) lld/ELF/Config.h (+2)
  • (modified) lld/ELF/Driver.cpp (+29)
  • (modified) lld/ELF/InputSection.cpp (+10-2)
  • (modified) lld/ELF/Options.td (+4)
  • (added) lld/test/ELF/override-section-flags.s (+38)
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

@nico
Copy link
Contributor Author

nico commented Sep 20, 2024

Here's the promised example of using this:

% cat prot-elf.cc
#include <sys/mman.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

extern char my_data[];
__attribute__((section(".mydata"))) char my_data[] = "Initial read-only data";

int main() {
  long pagesize = sysconf(_SC_PAGESIZE);
  void* page_start = (void*)((unsigned long)my_data & ~(pagesize - 1));
  if (mprotect(page_start, pagesize, PROT_READ | PROT_WRITE) == 0)
    strcpy(my_data, "New writable data");
  else
     perror("mprotect failed");
}
$ out/gn/bin/clang prot-elf.cc  -fuse-ld=lld -Wl,--override-section-flags=.mydata=a
$ ./a.out
-----------------------------------------------
56387607f000-563876080000 r--p 00000000 fd:01 19553804                   /usr/local/google/home/thakis/src/llvm-project/a.out
563876080000-563876081000 r-xp 00000000 fd:01 19553804                   /usr/local/google/home/thakis/src/llvm-project/a.out
563876081000-563876082000 r--p 00000000 fd:01 19553804                   /usr/local/google/home/thakis/src/llvm-project/a.out
563876082000-563876083000 rw-p 00000000 fd:01 19553804                   /usr/local/google/home/thakis/src/llvm-project/a.out
5638a883b000-5638a885c000 rw-p 00000000 00:00 0                          [heap]
*snip*
-----------------------------------------------
my_data: 0x56387607f800; page start: 0x56387607f000
-----------------------------------------------
56387607f000-563876080000 rw-p 00000000 fd:01 19553804                   /usr/local/google/home/thakis/src/llvm-project/a.out
563876080000-563876081000 r-xp 00000000 fd:01 19553804                   /usr/local/google/home/thakis/src/llvm-project/a.out
563876081000-563876082000 r--p 00000000 fd:01 19553804                   /usr/local/google/home/thakis/src/llvm-project/a.out
563876082000-563876083000 rw-p 00000000 fd:01 19553804                   /usr/local/google/home/thakis/src/llvm-project/a.out
5638a883b000-5638a885c000 rw-p 00000000 00:00 0                          [heap]
*snip*
-----------------------------------------------
$ readelf --segments a.out
There are 11 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x0000000000000268 0x0000000000000268  R      0x8
  INTERP         0x00000000000002a8 0x00000000000002a8 0x00000000000002a8
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000000008c8 0x00000000000008c8  R      0x1000
  LOAD           0x00000000000008d0 0x00000000000018d0 0x00000000000018d0
                 0x00000000000002d0 0x00000000000002d0  R E    0x1000
  LOAD           0x0000000000000ba0 0x0000000000002ba0 0x0000000000002ba0
                 0x00000000000001e0 0x0000000000000460  RW     0x1000
  LOAD           0x0000000000000d80 0x0000000000003d80 0x0000000000003d80
                 0x0000000000000078 0x0000000000000079  RW     0x1000
  DYNAMIC        0x0000000000000bb0 0x0000000000002bb0 0x0000000000002bb0
                 0x00000000000001a0 0x00000000000001a0  RW     0x8
  GNU_RELRO      0x0000000000000ba0 0x0000000000002ba0 0x0000000000002ba0
                 0x00000000000001e0 0x0000000000000460  R      0x1
  GNU_EH_FRAME   0x0000000000000818 0x0000000000000818 0x0000000000000818
                 0x0000000000000024 0x0000000000000024  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x0
  NOTE           0x00000000000002c4 0x00000000000002c4 0x00000000000002c4
                 0x0000000000000020 0x0000000000000020  R      0x4

 Section to Segment mapping:
  Segment Sections...
   00    
   01     .interp
   02     .interp .note.ABI-tag .dynsym .gnu.version .gnu.version_r .gnu.hash .dynstr .rela.dyn .rela.plt .rodata .mydata .eh_frame_hdr .eh_frame
   03     .text .init .fini .plt
   04     .fini_array .init_array .dynamic .got .relro_padding
   05     .data .tm_clone_table .got.plt .bss
   06     .dynamic
   07     .fini_array .init_array .dynamic .got .relro_padding
   08     .eh_frame_hdr
   09    
   10     .note.ABI-tag

(In --sections output, .mydata looks like so:

  [11] .mydata           PROGBITS         0000000000000800  00000800
       0000000000000017  0000000000000000   A       0     0     16

)

One effect of this is that .mydata ends up in a phrd with .rela.dyn .rela.plt .rodata etc, and when temporarily mapping the first page of .mydata writeable, parts of those other readonly sections temporarily become writeable too. This can be worked around by adding an object file first on the link line that has .mydata with page alignment. We can also add a --section-alignment=glob=value flag to make this easier later on.

@nico
Copy link
Contributor Author

nico commented Sep 20, 2024

Here's the promised proof-of-concept with a linker script. The only way I found to set section flags is by using custom PHDRS (…which only sets segment flags, not section flags). prot-elf.cc as above, and then:

% cat prot-elf.ld
PHDRS
{
   headers PT_PHDR PHDRS ;
   interp PT_INTERP ;
   text PT_LOAD FILEHDR PHDRS ;
   data PT_LOAD ;
   readonly PT_LOAD FLAGS (4) ;
   dynamic PT_DYNAMIC ;
}

SECTIONS {
   . = SIZEOF_HEADERS;
   .interp : { *(.interp) } :text :interp
   .text : { *(.text) } :text
   .rodata : { *(.rodata) } /* defaults to :text */

   . = . + 0x1000; /* move to a new page in memory */
   .data : { *(.data) } :data
   .dynamic : { *(.dynamic) } :data :dynamic

   . = . + 0x1000; /* move to a new page in memory */
   .mydata :
   {
       *(.mydata)
   } : readonly
}
% ~/src/chrome/src/third_party/llvm-build/Release+Asserts/bin/clang --target=x86_64-pc-linux prot-elf.cc -fuse-ld=lld --sysroot ~/src/chrome/src/build/linux/debian_bullseye_amd64-sysroot -Wl,-T,prot-elf.ld
Sections look wrong:

% out/gn/bin/llvm-readelf -S a.out            
out/gn/bin/llvm-readelf: warning: 'a.out': invalid PT_DYNAMIC size (0x1d8)
out/gn/bin/llvm-readelf: warning: 'a.out': PT_DYNAMIC dynamic table is invalid: SHT_DYNAMIC will be used
There are 32 section headers, starting at offset 0x12d8:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
*snip*
  [23] .mydata           PROGBITS        0000000000002a50 000a50 000017 00  WA  0   0 16
*snip

But segments look right:

% out/gn/bin/llvm-readelf --segments a.out
out/gn/bin/llvm-readelf: warning: 'a.out': invalid PT_DYNAMIC size (0x1d8)
out/gn/bin/llvm-readelf: warning: 'a.out': PT_DYNAMIC dynamic table is invalid: SHT_DYNAMIC will be used

Elf file type is DYN (Shared object file)
Entry point 0x610
There are 6 program headers, starting at offset 64

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000000040 0x0000000000000040 0x000150 0x000150 R   0x8
  INTERP         0x000190 0x0000000000000190 0x0000000000000190 0x000478 0x000478 R   0x8
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x000870 0x000870 R E 0x1000
  LOAD           0x000870 0x0000000000001870 0x0000000000001870 0x0001e0 0x0001e0 RW  0x1000
  LOAD           0x000a50 0x0000000000002a50 0x0000000000002a50 0x000060 0x000061 R   0x1000
  DYNAMIC        0x000878 0x0000000000001878 0x0000000000001878 0x0001d8 0x0001d8 RW  0x8

 Section to Segment mapping:
  Segment Sections...
   00    
   01     .interp .note.ABI-tag .dynsym .gnu.version .gnu.version_r .gnu.hash .dynstr .rela.dyn .rela.plt .rodata.cst4 .rodata.str1.1 .eh_frame_hdr .eh_frame
   02     .interp .note.ABI-tag .dynsym .gnu.version .gnu.version_r .gnu.hash .dynstr .rela.dyn .rela.plt .rodata.cst4 .rodata.str1.1 .eh_frame_hdr .eh_frame .text .init .fini .plt
   03     .data .dynamic .fini_array .init_array .got
   04     .mydata .tm_clone_table .data.rel.local .got.plt .bss
   05     .dynamic .fini_array .init_array .got
   None   .comment .symtab .shstrtab .strtab

This works, but making it production-quality requires a much longer and more complicated linker script, and per triple.

On the other hand, this PR (plus a follow-up --section-align= PR) is small and makes this much easier to use for applications.

@smithp35
Copy link
Collaborator

My initial reaction is that this seems like a low-level solution that could work, but could interact in unforeseen ways with other features as it could break some assumptions that some sections have particular flags. This may be resolvable with a tighter specification of what the option does and any restriction on what it applies to. For example can the section have dynamic relocations like .data.rel.ro if writeable is removed? My intuition is that this might be worth a RFC to see if there are alternative solutions that are perhaps a bit more specific.

To make sure I understand, the desire is to have an area of the program that is mapped read-only by the dynamic-linker, ideally aligned on page boundaries. The application, presumably using some kind of linker generated _start and _stop will dynamically alter the permission to write to the section.

This is kind of like .data.rel.ro which is writeable at static link time, but the dynamic loader marks read-only post dynamic relocation. This sounds like it is almost exactly what you want, with the exception that you probably don't want to make the genuinely RELRO sections temporarily writeable. Perhaps there's a way of making this work by using .data.rel.ro? For example if there were a page aligned sub-section of .data.rel.ro? The dynamic linker would make the RELRO segment read-only. Could the application temporarily map the sub-section of .data.rel.ro as read-write.

If we want to pursue the option I'd like to know (in the description) what the semantics of the option are. For example:

  • Does it behave like the linker script (NOLOAD) so the linker just give the output section the flags regardless of what the input section flags are.
  • What flags are supported by the option, and what ELF flags do they map to?
  • If you support overwrite, what about modify? For example can I add or subtract individual flags? I'm thinking of someone that has processor/OS specific flags that may not be representable by the option, or at least cumbersome to represent in it.
  • How does it interact with a PHDRS command that changes the flags like your example above?
  • How does it interact with conventions like .data.rel.ro, if the flags are overridden does that section cease to be RELRO?
  • What happens if there are clashes like read-only SHT-NOBITS?
  • If a section is marked read-only should the linker forbid dynamic relocations? I would expect so.

@nico
Copy link
Contributor Author

nico commented Sep 23, 2024

To make sure I understand, the desire is to have an area of the program that is mapped read-only by the dynamic-linker, ideally aligned on page boundaries.

Right.

The application, presumably using some kind of linker generated _start and _stop will dynamically alter the permission to write to the section.

When the code wants to write to a variable in such protected memory, it does something like:

void* p = round_to_page_boundary(&my_variable);
mprotect(p, READWRITE);
my_variable = 42;
mprotect(p, READONLY);

This sounds like it is almost exactly what you want

Kinda? We want to be able to write to the variable at any time during the program's execution.

The idea is for variables in this section to be regular variables, but since pages in this section are readonly most of the time, they're protected from heap spraying attacks (…most of the time).

What flags are supported by the option

Currently 'a', 'w', 'x' as described in the help text :)

If you support overwrite, what about modify? F

Might be nice for a follow-up.

How does it interact with a PHDRS command that changes the flags like your example above?

You mean in a linker script? Or is it possible to change PHDRS without a linker script? (If not, I'd say just error out when seeing both this flag and a linker script, at least for starters.)

If a section is marked read-only should the linker forbid dynamic relocations? I would expect so.

Can you say more about this?


In general, I'm trying to go for "simplest thing that could work" here. If there's desire to make this work in more cases in the future to address additional use cases, we can always tweak it later :) (…and make it restrictive enough in this first version to not paint ourselves in a corner)

@smithp35
Copy link
Collaborator

Apologies for taking so long to reply, I didn't catch the github notification.

To make sure I understand, the desire is to have an area of the program that is mapped read-only by the dynamic-linker, ideally aligned on page boundaries.

Right.

The application, presumably using some kind of linker generated _start and _stop will dynamically alter the permission to write to the section.

When the code wants to write to a variable in such protected memory, it does something like:

void* p = round_to_page_boundary(&my_variable);
mprotect(p, READWRITE);
my_variable = 42;
mprotect(p, READONLY);

This sounds like it is almost exactly what you want

Kinda? We want to be able to write to the variable at any time during the program's execution.

The idea is for variables in this section to be regular variables, but since pages in this section are readonly most of the time, they're protected from heap spraying attacks (…most of the time).

My thought was that extending .data.rel.ro may be a better starting point for you. The compiler and assembler can still use RW. The static linker's section ordering will place the sections with the other RW and not in the RO (if section flags are overridden from RW to RO). The sections can still contain dynamic relocations. What I think we're missing is enough isolation from the other RELRO sections (page boundaries) so that the data can be temporarily mapped as RW without giving access to the other RELRO sections.

Possibly some subset of .data.rel.ro say .data.rel.ro.mostlyro.* that LLD would treat as .relro but would page align and would have the size padded out to a page boundary.

What flags are supported by the option

Currently 'a', 'w', 'x' as described in the help text :)

If you support overwrite, what about modify? F

Might be nice for a follow-up.

How does it interact with a PHDRS command that changes the flags like your example above?

You mean in a linker script? Or is it possible to change PHDRS without a linker script? (If not, I'd say just error out when seeing both this flag and a linker script, at least for starters.)

Yes I mean PHDRS as a linker script.

If a section is marked read-only should the linker forbid dynamic relocations? I would expect so.

Can you say more about this?

The linker will only permit dynamic relocations in writeable segments, at least not without -z text which is frowned upon as it permits dynamic relocations in code-sections. The .data.rel.ro sections can contain dynamic relocations as they are writeable.

If the data in these sections are just constants then they won't need dynamic relocations. However if any of them are initialised to an address, this will need a dynamic relocation if the address is imported from a shared object, or if the program is position independent.

In general, I'm trying to go for "simplest thing that could work" here. If there's desire to make this work in more cases in the future to address additional use cases, we can always tweak it later :) (…and make it restrictive enough in this first version to not paint ourselves in a corner)

I think that the option could work for this use case. My concern is that the option is so general that I can see people finding it, picking it up and thinking that's just what I need, then finding that it either crashes the linker or doesn't do what they want it to do, which then provokes questions/issues etc.

Anyway this is just my opinion. Maybe worth asking MaskRay for his thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants