Skip to content

patchelf should never generate LOAD segments with p_vaddr less than the lowest such address in the original file #622

@zackw

Description

@zackw

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 .

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions