GCC 15 on Alpine Linux changes `-static` to imply `-pie` (i.e. `-static-pie)`

When linking an executable with -static flag, starting with GCC 15 (as available in Alpine Linux 3.23+) implies the flag -pie.
PIE stands for Position-independent executable. That’s generally a good idea as it enables address space layout randomization for improved security.
The resulting binary is still statically linked (file reports it as static-pie linked) and doesn’t load any libraries at runtime. But due to PIE it apparently cannot start itself and needs ld-musl as entrypoint.

Many people use Alpine Linux to produce portable binaries without any runtime dependencies. PIE binaries are incompatible for that due to their ld dependency.

The above was imprecise. See GCC 15 on Alpine Linux changes `-static` to imply `-pie` (i.e. `-static-pie)` - #6 by straight-shoota for a corrected version

$ # alpine:3.24
$ apk add -q crystal gc-static file
$ echo 'puts "Hello World!" > hello-world.cr
$ crystal build --static hello-world.cr
$ file hello-world
hello-world: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, BuildID[sha1]=48511bcc87143bd60f2858093d36e8c4a2d503d6, with debug_info, not stripped
$ ldd hello-world
        /lib/ld-musl-x86_64.so.1 (0x722641f37000)

The link flag -no-pie disables PIE and builds dependency-free static binaries. (it can be used fine with older GCC versions).

$ crystal build --static hello-world.cr --link-flags=-no-pie
$ file hello-world
hello-world: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=d315263739c7f37ba7de4d7ea981cf6f53e07a08, with debug_info, not stripped
$ ldd hello-world
/lib/ld-musl-x86_64.so.1: hello-world: Not a valid dynamic program

We should think about where adjustments are necessary to account for this configuration change.

tl;dr

I understand the same as you, that a static build means no dependencies for real portability, which means no PIE (-no-pie by default).

And for the cases in which PIE is required, using an opt-in flag should be good, like:

  • crystal build --static --link-flags="-static-pie"
  • or --link-flags="-pie".

I know that this is not universal and, for example, OpenBSD does static PIE by default for ASLR.

But, in vanilla GCC, -static has always meant: “On systems that support dynamic linking, this overrides -pie and prevents linking with the shared libraries.”

The issue is with Alpine’s GCC, built with --enable-default-pie. So both cc -static and cc -static-pie will produce a static PIE.

Returning to not adding PIE by default follows vanilla GCC behavior.

One extra nuance worth adding to your analysis: the portability break comes specifically from the binary keeping a PT_INTERP for ld-musl, not from PIE itself. A properly-linked static-PIE has no interpreter — musl’s rcrt1.o makes the binary relocate itself at startup (this is the exact bug the 2019 musl-gcc.specs.sh patch fixed: musl - [PATCH] fix musl-gcc.specs.sh to correctly handle -static-pie). Rust produces interpreter-free static-PIE by default on musl and those binaries run in scratch with ASLR: Building a statically linked binary in Alpine Linux - help - The Rust Programming Language Forum

From my pov there seem to be two paths:

  1. Pin --static to non-PIE (what the post suggests) — restores portability, matches Go’s default for static binaries, works with older GCC.
  2. Make --static produce interpreter-free static-PIE (the Rust approach) — portability and ASLR, but requires driving the linker’s crt selection rather than relying on the driver default.

Either way, the compiler pinning the behavior explicitly seems better than inheriting whatever the driver does, since that’s what silently changed.

Just a question: does the portable-compiler binary need the same treatment, and is there a preferred direction between the two?

Yes, that’s where I noticed this. See Opt out of PIE executables on Linux by straight-shoota · Pull Request #442 · crystal-lang/distribution-scripts · GitHub

I suppose if we can get static-PIE without interpreter, that wouldn’t be too bad.
But the quick path is just to build static without PIE.

There is another issue: Apparently Crystal’s DWARF parser cannot parse the debug information in PIE executables: Crystal built with Alpine 3.24 do not include backtrace in result binaries. · Issue #445 · crystal-lang/distribution-scripts · GitHub

This seems like a strong feature request for the standard library (DWARF address lookup fails with static-pie musl · Issue #17186 · crystal-lang/crystal · GitHub).

However, the above issue made me realize that static-pie executables are actually portable.

Alpine’s ldd reports a dependency on ld-musl but that doesn’t seem to be actually fixed in the executable. It runs fine on a glibc system.
Debian’s ldd still reports it as statically linked, without any dynamic dependency.

So part of my initial assessment was apparently wrong, and I should’ve properly verified the actual behaviour.
The original failure (GCC 15 on Alpine Linux changes `-static` to imply `-pie` (i.e. `-static-pie)` · Issue #441 · crystal-lang/distribution-scripts · GitHub) is actually just a broken test. The actual file still works.
We use ldd to detect whether the executable is statically linked, and the reporting for static-pie linked executables is different from what we expect.
We probably don’t need to build with -no-pie, we just need to fix the test.

That’s good news, @straight-shoota!

(post deleted by author)