-
-
Couldn't load subscription status.
- Fork 510
Description
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 NixOS/nixpkgs#441269 .