RFC: PR 30907: BFD linker option to allow read-only data in code segments

Fangrui Song i@maskray.me
Wed Jun 5 02:21:36 GMT 2024


On 2024-06-04, Jakub Jelinek wrote:
>On Tue, Jun 04, 2024 at 06:02:29PM +0100, Nick Clifton wrote:
>>   Attached is an experimental patch to add new option the bfd linker:
>>   -z rodata-in-code.
>>
>>   If used the option allows read only data to be placed into a code
>>   segment.  This is only effective if the -z separate-code option is in
>>   effect.  When used it has the ability to reduce the number of loadable
>>   segments from 4 to 3, which can have a big effect on the overall size
>>   of an executable.  (See PR 30907 for more discussion on this).
>
>Isn't that a security risk though?  One can then look for the ROP gadgets
>also in .rodata/.eh_frame/.eh_frame_hdr sections.  I don't know if there
>are more ROP gadgets in the sections before .init (the first PT_LOAD) or
>in .rodata/.eh_frame*/.gcc_except_table/.gnu_extab sections.
>
>I was wondering whether for -z separate-code it wouldn't be possible to
>just remove the
>  . = ALIGN(CONSTANT (MAXPAGESIZE));
>  /* Adjust the address for the rodata segment.  We want to adjust up to
>     the same address within the page on the next page up.  */
>  . = SEGMENT_START("rodata-segment", ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)));
>part from the built-in linker script and move the
>  .rodata         : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
>  .rodata1        : { *(.rodata1) }
>  .eh_frame_hdr   : { *(.eh_frame_hdr) *(.eh_frame_entry .eh_frame_entry.*) }
>  .eh_frame       : ONLY_IF_RO { KEEP (*(.eh_frame)) *(.eh_frame.*) }
>  .gcc_except_table   : ONLY_IF_RO { *(.gcc_except_table .gcc_except_table.*) }
>  .gnu_extab   : ONLY_IF_RO { *(.gnu_extab*) }
>  /* These sections are generated by the Sun/Oracle C++ compiler.  */
>  .exception_ranges   : ONLY_IF_RO { *(.exception_ranges*) }
>part from where it is now to before
>  . = ALIGN(CONSTANT (MAXPAGESIZE));
>  .init           :
>  {
>    KEEP (*(SORT_NONE(.init)))
>  }
>to achieve the same effect (just 3 PT_LOAD segments instead of 4) without
>sacrificing security.
>On x86_64/i?86, I really don't see anything that make it a problem, if the
>writable data sections refer to .rodata, they do it with full pointers,
>and if .text refers to .rodata, it is still usually signed 32-bit immediate
>which doesn't care if it is before or after .text, in the latter case there
>is the limit of the first .text reference to latest .rodata symbol distance,
>in the first case the limit is between first .rodata symbol and last .text
>reference to it distance.
>Maybe it could be a problem in -fsection-anchors defaulting arches if the
>section anchor refers to both .rodata and .data symbols and a big gap in
>between (the .text section) might be a problem.
>Another problem might be arches which do use .sdata2/.sbss2.
>
>Anyway, moving the
>  ${WRITABLE_RODATA-${RODATA}}
>  .${RODATA_NAME}1      ${RELOCATING-0} : { *(.${RODATA_NAME}1) }
>  ${CREATE_SHLIB-${SDATA2}}
>  ${CREATE_SHLIB-${SBSS2}}
>  ${OTHER_READONLY_SECTIONS}
>  .eh_frame_hdr ${RELOCATING-0} : { *(.eh_frame_hdr)${RELOCATING+ *(.eh_frame_entry .eh_frame_entry.*)} }
>  .eh_frame     ${RELOCATING-0} : ONLY_IF_RO { KEEP (*(.eh_frame))${RELOCATING+ *(.eh_frame.*)} }
>  .sframe       ${RELOCATING-0} : ONLY_IF_RO { *(.sframe)${RELOCATING+ *(.sframe.*)} }
>  .gcc_except_table ${RELOCATING-0} : ONLY_IF_RO { *(.gcc_except_table${RELOCATING+ .gcc_except_table.*}) }
>  .gnu_extab ${RELOCATING-0} : ONLY_IF_RO { *(.gnu_extab*) }
>  /* These sections are generated by the Sun/Oracle C++ compiler.  */
>  .exception_ranges ${RELOCATING-0} : ONLY_IF_RO { *(.exception_ranges${RELOCATING+*}) }
>  ${TEXT_PLT+${PLT_NEXT_DATA+${PLT} ${OTHER_PLT_SECTIONS}}}
>part into a separate emit_rodata function and emitting it before .init for
>if test -n "${SEPARATE_CODE}${SEPARATE_TEXT}"; then
>and maybe
>  if test -z "${RODATA_ADDR}"; then
>(dunno about SHLIB_RODATA_ADDR) and only emit the
>SEGMENT_START("rodata-segment"
>etc. related stuff if it isn't early, then trying to build a lot of code on
>multiple arches (e.g. Fedora mass-prebuild) and see where it works and where
>it doesn't.
>
>	Jakub
>

(I am not familiar with the code. Hopefully I do not misunderstand
Jakub's description.)
I agree that placing .rodata/.eh_frame after .dynsym/.rela.dyn is a more
optimal layout. This would indeed involve adjusting the internal linker script.

Additionally, ld/ldlang.c:lang_output_section_find_by_flags might need a change,
as it places .rodata after .text .

My description of lld's section layout can be found at
https://maskray.me/blog/2020-11-15-explain-gnu-linker-options#no-rosegment
and
https://maskray.me/blog/2023-12-17-exploring-the-section-layout-in-linker-output

* R PT_LOAD
* RX PT_LOAD
* RW PT_LOAD (overlaps with PT_GNU_RELRO)
* RW PT_LOAD

> Specify this option to combine the R PT_LOAD and the RX PT_LOAD. The RX
> PT_LOAD segment is traditionally called the text segment and is the
> first segment.
>
> ld.lld places rodata and data on both sides of text. This layout has
> the advantage that the distance between text and data is shorter,
> decreasing the relocation overflow pressure.

Since GNU ld has placed .text before .rodata for a very long time and some
programs may rely on this, I agree that an option to place .rodata before .text
should initially be opt-in.
This could be revisited in the future as lld's layout (also adopted by mold) has
been thoroughly tested by many distributions.

---

**Proposed Changes for GNU ld:**

* Split -z separate-code into --rosegment and -z separate-code (as suggested in https://sourceware.org/bugzilla/show_bug.cgi?id=30907#c4)
* Restore the pre-2.31 -z noseparate-code default for Linux x86, prioritizing file size concerns.
* Add an option to place read-only sections entirely before .text, eliminating the R segment after the RX segment.

Regarding security concerns:

>Isn't that a security risk though?  One can then look for the ROP gadgets
>also in .rodata/.eh_frame/.eh_frame_hdr sections.  I don't know if there
>are more ROP gadgets in the sections before .init (the first PT_LOAD) or
>in .rodata/.eh_frame*/.gcc_except_table/.gnu_extab sections.

While -z separate-code might be seen as a security measure, executable memory
inherently contains many ROP gadgets. I believe using -z separate-code for
security purposes is really a security theatre. (See also
https://isopenbsdsecu.re/mitigations/rop_removal/)

A potentially more useful feature for security is execute-only code supported
by AArch64 (ld.lld --execute-only), which makes the PF_R|PF_X PT_LOAD segment
PF_X only.

The possibly useful feature is ld.lld --execute-only, which makes the
PF_R|PF_X PT_LOAD segment PF_X only. lld --execute-only is incompatible with --no-rosegment:

% myld.lld -Ttext=0xcafe0000 a.o -o a.so -shared --execute-only --no-rosegment
ld.lld: error: --execute-only and --no-rosegment cannot be used together


More information about the Binutils mailing list