patchelf should never generate LOAD segments with p_vaddr less than the lowest such address in the original file
Describe the bug
Here's the program header table for a NixOS build of busybox for x86-64-linux:
$ readelf -l /nix/store/3v58nb3cwghbi986nia32i4vrksn6ipl-busybox-1.36.1/bin/busybox
Elf file type is EXEC (Executable file)
Entry point 0x408840
There are 14 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x0000000000000310 0x0000000000000310 R 0x8
INTERP 0x00000000000003b4 0x00000000004003b4 0x00000000004003b4
0x0000000000000053 0x0000000000000053 R 0x1
[Requesting program interpreter: /nix/store/776irwlgfb65a782cxmyk61pck460fs9-glibc-2.40-66/lib/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000006fd0 0x0000000000006fd0 R 0x1000
LOAD 0x0000000000007000 0x0000000000407000 0x0000000000407000
0x00000000000cb9e1 0x00000000000cb9e1 R E 0x1000
LOAD 0x00000000000d3000 0x00000000004d3000 0x00000000004d3000
0x0000000000028738 0x0000000000028738 R 0x1000
LOAD 0x00000000000fc160 0x00000000004fc160 0x00000000004fc160
0x00000000000040fb 0x00000000000047a8 RW 0x1000
DYNAMIC 0x00000000000ff108 0x00000000004ff108 0x00000000004ff108
0x0000000000000230 0x0000000000000230 RW 0x8
NOTE 0x0000000000000350 0x0000000000400350 0x0000000000400350
0x0000000000000040 0x0000000000000040 R 0x8
NOTE 0x0000000000000390 0x0000000000400390 0x0000000000400390
0x0000000000000024 0x0000000000000024 R 0x4
NOTE 0x00000000000fb718 0x00000000004fb718 0x00000000004fb718
0x0000000000000020 0x0000000000000020 R 0x4
GNU_PROPERTY 0x0000000000000350 0x0000000000400350 0x0000000000400350
0x0000000000000040 0x0000000000000040 R 0x8
GNU_EH_FRAME 0x00000000000fb668 0x00000000004fb668 0x00000000004fb668
0x000000000000002c 0x000000000000002c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x00000000000fc160 0x00000000004fc160 0x00000000004fc160
0x0000000000003ea0 0x0000000000003ea0 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .note.gnu.property .note.gnu.build-id .interp .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .text .fini
04 .rodata .eh_frame_hdr .eh_frame .note.ABI-tag
05 .init_array .fini_array .data.rel.ro .dynamic .got .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id
09 .note.ABI-tag
10 .note.gnu.property
11 .eh_frame_hdr
12
13 .init_array .fini_array .data.rel.ro .dynamic .got
Note that all the LOAD segments have base virtual addresses at or above 0x400000. This is an informal ABI requirement on x86-64-linux, and many systems are configured to enforce it by setting /proc/sys/vm/mmap_min_addr to 0x400000.
When this binary is processed by patchelf (as part of the build for the "extra-utils" derivation that's used in the NixOS initrd), the result has LOAD segments below 0x400000:
$ readelf -l /nix/store/r0bg4qxspbmfa6rm874ypl465r2ffjjq-extra-utils/bin/busybox
Elf file type is EXEC (Executable file)
Entry point 0x408840
There are 15 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x00000000003ff040 0x00000000003ff040
0x0000000000000348 0x0000000000000348 R 0x8
GNU_STACK 0x0000000000001000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
LOAD 0x0000000000000000 0x00000000003ff000 0x00000000003ff000
0x0000000000001000 0x0000000000001000 RW 0x1000
INTERP 0x0000000000000388 0x00000000003ff388 0x00000000003ff388
0x0000000000000051 0x0000000000000051 R 0x1
[Requesting program interpreter: /nix/store/r0bg4qxspbmfa6rm874ypl465r2ffjjq-extra-utils/lib/ld-linux-x86-64.so.2]
NOTE 0x00000000000003e0 0x00000000003ff3e0 0x00000000003ff3e0
0x0000000000000024 0x0000000000000024 R 0x4
NOTE 0x0000000000000408 0x00000000003ff408 0x00000000003ff408
0x0000000000000040 0x0000000000000040 R 0x8
LOAD 0x0000000000001000 0x0000000000400000 0x0000000000400000
0x0000000000006fd0 0x0000000000006fd0 R 0x1000
GNU_PROPERTY 0x0000000000001350 0x0000000000400350 0x0000000000400350
0x0000000000000040 0x0000000000000040 R 0x8
LOAD 0x0000000000008000 0x0000000000407000 0x0000000000407000
0x00000000000cb9e1 0x00000000000cb9e1 R E 0x1000
LOAD 0x00000000000d4000 0x00000000004d3000 0x00000000004d3000
0x0000000000028738 0x0000000000028738 R 0x1000
GNU_EH_FRAME 0x00000000000fc668 0x00000000004fb668 0x00000000004fb668
0x000000000000002c 0x000000000000002c R 0x4
NOTE 0x00000000000fc718 0x00000000004fb718 0x00000000004fb718
0x0000000000000020 0x0000000000000020 R 0x4
LOAD 0x00000000000fd160 0x00000000004fc160 0x00000000004fc160
0x00000000000040fb 0x00000000000047a8 RW 0x1000
GNU_RELRO 0x00000000000fd160 0x00000000004fc160 0x00000000004fc160
0x0000000000003ea0 0x0000000000003ea0 R 0x1
DYNAMIC 0x0000000000100108 0x00000000004ff108 0x00000000004ff108
0x0000000000000230 0x0000000000000230 RW 0x8
Section to Segment mapping:
Segment Sections...
00
01
02 .interp .note.gnu.build-id .note.gnu.property
03 .interp
04 .note.gnu.build-id
05 .note.gnu.property
06 .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
07
08 .init .plt .plt.got .text .fini
09 .rodata .eh_frame_hdr .eh_frame .note.ABI-tag
10 .eh_frame_hdr
11 .note.ABI-tag
12 .init_array .fini_array .data.rel.ro .dynamic .got .data .bss
13 .init_array .fini_array .data.rel.ro .dynamic .got
14 .dynamic
It appears that patchelf needed extra space for the patched .interp section, and therefore moved it to a lower address in the file, where more space could be found. Because the original file had a LOAD segment covering, among other things, the .interp section, it created a new LOAD segment to cover the relocated .interp section. So far, fine; but it also moved the virtual address of the new segment downward, below 0x400000. That's the problem. This binary will not execute on systems where mmap_min_addr has been set to 0x400000.
$ strace /nix/store/r0bg4qxspbmfa6rm874ypl465r2ffjjq-extra-utils/bin/busybox ash -c 'echo hello world'
execve("/nix/store/r0bg4qxspbmfa6rm874ypl465r2ffjjq-extra-utils/bin/busybox", ["/nix/store/r0bg4qxspbmfa6rm874yp"..., "ash", "-c", "echo hello world"], 0x7fff2d1b1c58 /* 72 vars */) = -1 EPERM (Operation not permitted)
+++ killed by SIGSEGV +++
Steps To Reproduce
On a Linux system with Nix available and the nix command and flakes enabled (it doesn't have to be a NixOS system), set /proc/sys/vm/mmap_min_addr to 0x400000. Create an empty directory. Put this flake in the empty directory.
{
inputs = {
nixpkgs.url = "github:zackw/nixpkgs/patch-1";
};
outputs = { self, nixpkgs, ... }: {
nixosConfigurations.demo = nixpkgs.lib.nixosSystem {
modules = [{
system.stateVersion = "25.05";
nix.registry.nixpkgs.flake = nixpkgs;
nixpkgs.hostPlatform = "x86_64-linux";
boot.loader.grub.device = "nodev";
fileSystems."/" = {
device = "/dev/sda1";
fsType = "xfs";
};
}];
};
};
}
Then run this command from within the directory:
nix build "path:${PWD}#nixosConfigurations.demo.config.system.build.toplevel"
You should get a build failure looking like this:
warning: creating lock file '/home/zack/projects/misc/server-configs/argh/flake.lock':
• Added input 'nixpkgs':
'github:zackw/nixpkgs/b71cc3c93040c10aa61af3ff2e502e04bb7012e2' (2025-09-10)
error: builder for '/nix/store/ji3bx8wfamqskx9blahvgmzyiph6v2ji-extra-utils.drv' failed with exit code 1;
last 25 log lines:
> + shift
> + local 'hooksSlice=failureHooks[@]'
> + local hook
[etc]
Then run nix log /nix/store/ji3bx8wfamqskx9blahvgmzyiph6v2ji-extra-utils.drv. Search for the string "testing patched programs". You should see this:
testing patched programs...
+++ /nix/store/r0bg4qxspbmfa6rm874ypl465r2ffjjq-extra-utils/bin/ash -c 'echo hello world'
+++ grep 'hello world'
+ exitHandler
+ exitCode=1
+ set +e
[etc]
And, if you try to run /nix/store/r0bg4qxspbmfa6rm874ypl465r2ffjjq-extra-utils/bin/busybox, it should segfault; contrariwise, /nix/store/3v58nb3cwghbi986nia32i4vrksn6ipl-busybox-1.36.1/busybox should work correctly. You should see the same program header tables as I showed above by running readelf -l on those two binaries.
Expected behavior
patchelf should compute the lowest p_vaddr of all LOAD segments in its input file, and it should take that as a hard lower bound; that is, it should ensure that all LOAD segments in its output have p_vaddr greater than or equal to this hard lower bound. This is only necessary for LOAD segments. This should always be possible, because -- unless there's a bug in the kernel or the dynamic linker -- none of the sections that patchelf needs to modify, need to be loaded at any particular address, and there's no requirement for the p_vaddr values (virtual load addresses) of LOAD segments to be in the same sequence as their p_offset values (locations within the file).
I am not 100% sure about this, but I think it is not necessary for the .interp section (technically the INTERP segment) to be loaded at all. In the original file, the LOAD segment that covers the .interp segment is actually there to cover the .hash section and several more after that, which are used by the dynamic linker and therefore do need to be loaded; it extends all the way down to load address 0x400000 and file offset 0 only because those both have to be page-aligned. The contents of the INTERP segment are read directly out of the file by the kernel (https://elixir.bootlin.com/linux/v6.16.6/source/fs/binfmt_elf.c#L874) and should not be needed after that. However, it looks like several of the other things patchelf can do may involve rewriting sections that do need to be loaded, and the principle applies to them too.
patchelf --version output
patchelf 0.15.2
Additional context
See earlier discussion at https://github.com/NixOS/nixpkgs/issues/441269 .
Just a heads up, if you want to figure out the exact version of patchelf that was used in that derivation you could do nix-tree --derivation /nix/store/ji3bx8wfamqskx9blahvgmzyiph6v2ji-extra-utils.drv, hit slash to search, and find patchelf.
@dtomvan Thanks for telling me about nix-tree. It was 0.15.2, brought in via stdenv, nothing weird going on there. (For some reason my store has both 0.15.0 and 0.15.2. It'd be helpful if build logs would start by listing all the inputs used in the derivation.)
This looks like the problem I ran into on riscv64-linux in https://github.com/NixOS/nixpkgs/issues/460825.
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x000000000000f040 0x000000000000f040
0x00000000000002a0 0x00000000000002a0 R 0x8
RISCV_ATTRIBUT 0x0000000000219e92 0x0000000000000000 0x0000000000000000
0x0000000000000057 0x0000000000000000 R 0x1
GNU_STACK 0x0000000000001000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
LOAD 0x0000000000000000 0x000000000000f000 0x000000000000f000
0x0000000000001000 0x0000000000001000 RW 0x1000
LOAD 0x0000000000001000 0x0000000000010000 0x0000000000010000
0x000000000020ded0 0x000000000020ded0 R E 0x1000
INTERP 0x00000000000142d0 0x00000000000232d0 0x00000000000232d0
0x000000000000005c 0x000000000000005c R 0x1
[Requesting program interpreter: /nix/store/kg61gdq4svvph0c4pcsf867r8nkbiqs7-bootstrap-tools/lib/ld-linux-riscv64-lp64d.so.1]
NOTE 0x0000000000014330 0x0000000000023330 0x0000000000023330
0x0000000000000020 0x0000000000000020 R 0x4
GNU_EH_FRAME 0x00000000001c41d8 0x00000000001d31d8 0x00000000001d31d8
0x000000000000967c 0x000000000000967c R 0x4
LOAD 0x000000000020ef58 0x000000000021ef58 0x000000000021ef58
0x000000000000af28 0x00000000000103b8 RW 0x1000
TLS 0x000000000020ef58 0x000000000021ef58 0x000000000021ef58
0x0000000000000000 0x0000000000000010 R 0x8
GNU_RELRO 0x000000000020ef58 0x000000000021ef58 0x000000000021ef58
0x00000000000080a8 0x00000000000080a8 R 0x1
DYNAMIC 0x0000000000216dd0 0x0000000000226dd0 0x0000000000226dd0
0x0000000000000230 0x0000000000000230 RW 0x8
This issue affects anyone who wishes to target the Google Play Store (Android) while deploying native libraries that have been modified by patchelf. Recent changes from Google Play Store requires that native libraries have their page sizes aligned to 16KB. It seems whenever one runs --replace-needed the affected LOAD segment will have its alignment reset to 4KB.
This currently impacts the Qt Multimedia module, on Android. Qt Multimedia relies on FFmpeg. We are able to compile FFmpeg using correct page size, but when modified by patchelf, the alignment is no longer correct. The issue has been reproduced on Android x86_64 binaries, and when booting an app with these libraries, it is rejected by Android. The alignment issue is also reproduced on Android ARM64 binaries, but I was unable to get an Android emulator to reject the libraries.
I have only reproduced this on version 0.17.2, as we are affected by a separate bug on version 0.18.
See Qt bugreport for more info: https://qt-project.atlassian.net/browse/QTBUG-142332
Below is the terminal output of readelf after FFmpeg x86_64 for Android is compiled, before Patchelf is used
nils@nils-ubuntu:~/qtdev/dev/ffmpeg/android-x64-test/latest/lib$ readelf -ld libavformat.so
Elf file type is DYN (Shared object file)
Entry point 0x0
There are 10 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x0000000000000230 0x0000000000000230 R 0x8
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000b5754 0x00000000000b5754 R 0x4000
LOAD 0x00000000000b5760 0x00000000000b9760 0x00000000000b9760
0x00000000001a0350 0x00000000001a0350 R E 0x4000
LOAD 0x0000000000255ab0 0x000000000025dab0 0x000000000025dab0
0x00000000000318b8 0x0000000000032550 RW 0x4000
LOAD 0x0000000000287368 0x0000000000293368 0x0000000000293368
0x0000000000000000 0x0000000000000058 RW 0x4000
DYNAMIC 0x0000000000286058 0x000000000028e058 0x000000000028e058
0x00000000000001f0 0x00000000000001f0 RW 0x8
GNU_RELRO 0x0000000000255ab0 0x000000000025dab0 0x000000000025dab0
0x00000000000318b8 0x0000000000032550 R 0x1
GNU_EH_FRAME 0x000000000008692c 0x000000000008692c 0x000000000008692c
0x0000000000005a94 0x0000000000005a94 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x0
NOTE 0x0000000000000270 0x0000000000000270 0x0000000000000270
0x0000000000000098 0x0000000000000098 R 0x4
Section to Segment mapping:
Segment Sections...
00
01 .note.android.ident .dynsym .gnu.version .gnu.version_d .gnu.version_r .gnu.hash .dynstr .rela.dyn .rela.plt .rodata .eh_frame_hdr .eh_frame
02 .text .plt
03 .data.rel.ro .fini_array .dynamic .got .got.plt .relro_padding
04 .bss
05 .dynamic
06 .data.rel.ro .fini_array .dynamic .got .got.plt .relro_padding
07 .eh_frame_hdr
08
09 .note.android.ident
Dynamic section at offset 0x286058 contains 31 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libavcodec.so]
0x0000000000000001 (NEEDED) Shared library: [libavutil.so]
0x0000000000000001 (NEEDED) Shared library: [libm.so]
0x0000000000000001 (NEEDED) Shared library: [libz.so]
0x0000000000000001 (NEEDED) Shared library: [libssl_3.so]
0x0000000000000001 (NEEDED) Shared library: [libcrypto_3.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x000000000000000e (SONAME) Library soname: [libavformat.so]
0x000000000000001e (FLAGS) SYMBOLIC BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW
0x0000000000000007 (RELA) 0x7de0
0x0000000000000008 (RELASZ) 195720 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffff9 (RELACOUNT) 8017
0x0000000000000017 (JMPREL) 0x37a68
0x0000000000000002 (PLTRELSZ) 12672 (bytes)
0x0000000000000003 (PLTGOT) 0x28e2d0
0x0000000000000014 (PLTREL) RELA
0x0000000000000006 (SYMTAB) 0x308
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000005 (STRTAB) 0x4e3c
0x000000000000000a (STRSZ) 12192 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x4a18
0x000000000000001a (FINI_ARRAY) 0x28e048
0x000000000000001c (FINI_ARRAYSZ) 16 (bytes)
0x000000006ffffff0 (VERSYM) 0x43b8
0x000000006ffffffc (VERDEF) 0x491c
0x000000006ffffffd (VERDEFNUM) 2
0x000000006ffffffe (VERNEED) 0x4954
0x000000006fffffff (VERNEEDNUM) 6
0x0000000000000000 (NULL) 0x0
Below is the terminal output of readelf after FFmpeg x86_64 for Android is compiled, after Patchelf is used
nils@nils-ubuntu:~/qtdev/dev/ffmpeg/android-x64/latest/lib$ readelf -ld libavformat.so
Elf file type is DYN (Shared object file)
Entry point 0x0
There are 12 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002a0 0x00000000000002a0 R 0x8
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000b5754 0x00000000000b5754 R 0x4000
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x0
GNU_EH_FRAME 0x000000000008692c 0x000000000008692c 0x000000000008692c
0x0000000000005a94 0x0000000000005a94 R 0x4
LOAD 0x00000000000b5760 0x00000000000b9760 0x00000000000b9760
0x00000000001a0350 0x00000000001a0350 R E 0x4000
LOAD 0x0000000000255ab0 0x000000000025dab0 0x000000000025dab0
0x00000000000318b8 0x0000000000032550 RW 0x4000
GNU_RELRO 0x0000000000255ab0 0x000000000025dab0 0x000000000025dab0
0x00000000000318b8 0x0000000000032550 R 0x1
DYNAMIC 0x0000000000286058 0x000000000028e058 0x000000000028e058
0x00000000000001f0 0x00000000000001f0 RW 0x8
LOAD 0x0000000000287368 0x0000000000293368 0x0000000000293368
0x0000000000000000 0x0000000000000058 RW 0x4000
NOTE 0x0000000000288000 0x0000000000294000 0x0000000000294000
0x0000000000000098 0x0000000000000098 R 0x4
LOAD 0x0000000000288000 0x0000000000294000 0x0000000000294000
0x0000000000003058 0x0000000000003058 RW 0x1000
LOAD 0x000000000028c000 0x0000000000298000 0x0000000000298000
0x0000000000007098 0x0000000000007098 RW 0x1000
Section to Segment mapping:
Segment Sections...
00
01 .gnu.version .gnu.version_d .gnu.version_r .gnu.hash .rela.dyn .rela.plt .rodata .eh_frame_hdr .eh_frame
02
03 .eh_frame_hdr
04 .text .plt
05 .data.rel.ro .fini_array .dynamic .got .got.plt .relro_padding
06 .data.rel.ro .fini_array .dynamic .got .got.plt .relro_padding
07 .dynamic
08 .bss
09 .note.android.ident
10 .note.android.ident
11 .dynsym .dynstr
Dynamic section at offset 0x286058 contains 31 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libavcodec.so]
0x0000000000000001 (NEEDED) Shared library: [libavutil.so]
0x0000000000000001 (NEEDED) Shared library: [libm.so]
0x0000000000000001 (NEEDED) Shared library: [libz.so]
0x0000000000000001 (NEEDED) Shared library: [libQt6FFmpegStub-ssl_x86_64.so]
0x0000000000000001 (NEEDED) Shared library: [libQt6FFmpegStub-crypto_x86_64.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x000000000000000e (SONAME) Library soname: [libavformat.so]
0x000000000000001e (FLAGS) SYMBOLIC BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW
0x0000000000000007 (RELA) 0x7de0
0x0000000000000008 (RELASZ) 195720 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffff9 (RELACOUNT) 8017
0x0000000000000017 (JMPREL) 0x37a68
0x0000000000000002 (PLTRELSZ) 12672 (bytes)
0x0000000000000003 (PLTGOT) 0x28e2d0
0x0000000000000014 (PLTREL) RELA
0x0000000000000006 (SYMTAB) 0x298000
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000005 (STRTAB) 0x29c0b0
0x000000000000000a (STRSZ) 12257 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x4a18
0x000000000000001a (FINI_ARRAY) 0x28e048
0x000000000000001c (FINI_ARRAYSZ) 16 (bytes)
0x000000006ffffff0 (VERSYM) 0x43b8
0x000000006ffffffc (VERDEF) 0x491c
0x000000006ffffffd (VERDEFNUM) 2
0x000000006ffffffe (VERNEED) 0x4954
0x000000006fffffff (VERNEEDNUM) 6
0x0000000000000000 (NULL) 0x0