PR Proposal: Enable Binary Hardening Flags by Default for C Extension Builds

Target repositories: pypa/pip, pypa/setuptools
Author: Aljefra Security Research
Date: 2026-04-12
Status: Draft


Summary

pip and setuptools should set standard binary hardening compiler/linker flags (-fstack-protector-strong, -D_FORTIFY_SOURCE=2, -Wl,-z,relro,-z,now) by default when building C extension modules from source. These are the same flags that every major Linux distribution already uses to build system packages, but they are absent from pip’s build pipeline. As a result, 93% of Python C extensions ship without stack canaries and 98% lack full RELRO, creating an ecosystem-wide security gap that affects every Python process loading native code.


Problem Description

The Data

We analyzed 373 C extension binaries across 46 widely-used Python packages (numpy, scipy, pandas, cryptography, lxml, grpc, Pillow, psycopg2, aiohttp, and others). The findings:

Hardening Property System Libraries (glibc, OpenSSL) Python C Extensions Gap
Stack canaries 83% 7.0% (26/373) 76 percentage points
Full RELRO 83% 2.4% (9/373) 81 percentage points
FORTIFY_SOURCE 83% 5.6% (21/373) 77 percentage points

Additional findings:

  • 65,636 ROP gadgets are available across all loaded binaries in a typical Python process, enabling Turing-complete code-reuse attack chains
  • 364/373 binaries use lazy binding (no BIND_NOW), leaving GOT entries writable and exploitable
  • 42 libraries have RPATH entries with deep directory traversal, enabling library injection
  • 22 packages import known-dangerous C functions (strcpy, sprintf, strcat, etc.) without FORTIFY_SOURCE protection
  • Only Rust-based extensions (cryptography, pydantic_core, bcrypt) consistently have full RELRO, because the Rust compiler enables it by default

Root Cause

When a user runs pip install <package> and the package contains C extensions, pip invokes setuptools’ build system, which calls the system C compiler. Neither pip nor setuptools inject any hardening flags into this compilation. The compiler’s defaults (-O2 at best, no hardening) are used.

Every major Linux distribution’s package manager (apt, dnf, pacman) automatically injects these hardening flags when building from source. pip is the only major package manager that does not.

Why This Matters

A buffer overflow vulnerability in any single C extension (numpy, Pillow, lxml, etc.) gives an attacker:

  1. No stack canary detection – the overflow proceeds silently, no abort
  2. Writable GOT entries – the attacker overwrites a function pointer in the GOT to redirect execution
  3. 65,636 ROP gadgets – a Turing-complete instruction set for arbitrary code execution
  4. No FORTIFY_SOURCE – unsafe C functions like strcpy and sprintf are not replaced with bounds-checked variants

This is not a theoretical concern. Buffer overflows in Python C extensions have been reported in numpy (CVE-2021-41495), Pillow (CVE-2022-22816, CVE-2022-22817), lxml (CVE-2022-2309), and others.


Proposed Change

pip (pypa/pip)

Modify src/pip/_internal/build_env.py and/or src/pip/_internal/commands/install.py to inject default hardening flags into the build environment when building from source:

# In the build environment setup, before invoking the build backend:
import os

_HARDENING_CFLAGS = "-fstack-protector-strong -D_FORTIFY_SOURCE=2 -fstack-clash-protection"
_HARDENING_LDFLAGS = "-Wl,-z,relro,-z,now"

def _inject_hardening_flags(env: dict) -> dict:
    """Inject binary hardening flags unless the user has explicitly set them."""
    env = dict(env)

    # Respect user-set flags -- append rather than override
    existing_cflags = env.get("CFLAGS", "")
    existing_ldflags = env.get("LDFLAGS", "")

    # Only add flags that aren't already present
    for flag in _HARDENING_CFLAGS.split():
        if flag not in existing_cflags:
            existing_cflags += f" {flag}"

    for flag in _HARDENING_LDFLAGS.split():
        if flag not in existing_ldflags:
            existing_ldflags += f" {flag}"

    env["CFLAGS"] = existing_cflags.strip()
    env["LDFLAGS"] = existing_ldflags.strip()
    return env

setuptools (pypa/setuptools)

Modify setuptools/_distutils/unixccompiler.py (or setuptools/_distutils/sysconfig.py) to include hardening flags in the default compiler configuration:

# In UnixCCompiler or the sysconfig defaults:
_DEFAULT_HARDENING_FLAGS = {
    "compiler": ["-fstack-protector-strong", "-D_FORTIFY_SOURCE=2", "-fstack-clash-protection"],
    "linker": ["-Wl,-z,relro,-z,now"],
}

Opt-Out Mechanism

Add a --no-binary-hardening flag to pip and an environment variable PIP_NO_BINARY_HARDENING=1 so that:

  • Users on constrained platforms (embedded, WASM, non-glibc) can disable the flags
  • CI/CD pipelines that need exact flag control can opt out
  • Package maintainers testing specific compiler configurations are not blocked

Backward Compatibility Analysis

Low Risk

These flags have been the default in Debian, Fedora, Ubuntu, Arch, and SUSE for over a decade. They are supported by:

  • GCC >= 4.9 (released 2014)
  • Clang >= 3.5 (released 2014)
  • musl libc (Alpine Linux) – fully compatible
  • glibc >= 2.17 (released 2012)

Known Edge Cases

Scenario Impact Mitigation
Very old GCC (< 4.9) -fstack-clash-protection not recognized Flag detection: only add flags the compiler supports
WASM/Emscripten builds Linker flags not applicable Detect target platform, skip flags for non-ELF targets
musl libc (Alpine) All flags supported No action needed
Cross-compilation Flags may not apply to target Respect CROSSCOMPILE env; opt-out flag available
Windows/macOS Different linker flags Only apply on Linux/ELF targets; use platform-appropriate equivalents for macOS (-Wl,-bind_at_load)
Packages with custom setup.py flags User flags may conflict Append (not prepend) hardening flags; user flags take precedence

ABI Compatibility

Hardening flags do not change the ABI. A binary compiled with -fstack-protector-strong is fully link-compatible with one compiled without it. There are no symbol changes, no struct layout changes, and no calling convention changes.


Performance Impact

Negligible

These flags are used by every major Linux distribution for all system packages, including performance-critical libraries like glibc, OpenSSL, zlib, and the Linux kernel.

Measured overhead from published benchmarks:

Flag Overhead Source
-fstack-protector-strong < 1% on most workloads Red Hat Performance Team, 2014
-D_FORTIFY_SOURCE=2 < 0.1% (compile-time replacement of unsafe functions) Ubuntu Security Hardening Guide
-Wl,-z,relro,-z,now One-time startup cost of ~1-5ms for symbol resolution Fedora Hardening Guidelines
-fstack-clash-protection < 0.5% GCC documentation

For Python C extensions, the startup cost of full RELRO (eager binding) is typically under 1 millisecond because Python extensions have small GOT tables (median: 17 entries in our dataset).


Security Impact

Immediate Benefits

Applying these flags to the 373 binaries we analyzed would:

  1. Add stack canaries to 347 binaries – buffer overflows that currently silently corrupt the stack would be detected and cause a controlled abort
  2. Enable full RELRO on 364 binaries – the GOT becomes read-only after startup, eliminating GOT overwrite attacks
  3. Enable FORTIFY_SOURCE on 352 binaries – unsafe C functions (strcpy, sprintf, strcat) are replaced with bounds-checked versions at compile time
  4. Eliminate lazy binding on 364 binaries – all symbols resolved at startup, closing the window for GOT hijacking

Ecosystem-Wide Scale

PyPI hosts over 400,000 packages. Based on our analysis, approximately 5,000-10,000 packages contain C extensions that would benefit from these flags. Every pip install from source would automatically produce hardened binaries.


References

Distribution Hardening Standards

  • Debian Hardening Guide: https://wiki.debian.org/Hardening – Debian has mandated these flags via dpkg-buildflags since 2012
  • Fedora Packaging Guidelines: https://docs.fedoraproject.org/en-US/packaging-guidelines/#_compiler_flags – -fstack-protector-strong, -D_FORTIFY_SOURCE=2, full RELRO are mandatory
  • Ubuntu Security Hardening: https://wiki.ubuntu.com/Security/Features – enabled by default since Ubuntu 12.04
  • Arch Linux makepkg.conf: enables -fstack-protector-strong, RELRO, BIND_NOW by default
  • SUSE Secure Coding Guidelines: mandates the same flag set

Relevant Standards and Research

  • CWE-120: Buffer Copy without Checking Size of Input (strcpy, sprintf)
  • CWE-121: Stack-based Buffer Overflow (mitigated by stack canaries)
  • NIST SP 800-218: Secure Software Development Framework – recommends compiler hardening
  • OWASP C/C++ Secure Coding Practices: recommends all proposed flags

Prior Art in Other Package Managers

  • npm/node-gyp: Does not inject flags (same gap exists)
  • cargo (Rust): Enables full RELRO and stack protection by default
  • Go compiler: Enables full RELRO, PIE, and stack protection by default
  • dpkg-buildflags (Debian): Injects all proposed flags automatically

Implementation Plan

  1. Phase 1: Add flag injection to pip with opt-out mechanism. Emit a one-time informational message the first time hardening flags are used, so users are aware.
  2. Phase 2: Add equivalent defaults to setuptools’ distutils integration, so python setup.py build_ext also benefits.
  3. Phase 3: Update pip documentation to describe the hardening defaults and opt-out mechanism.
  4. Phase 4: Propose a PEP to standardize build-time hardening flags across Python build backends (meson-python, scikit-build-core, etc.).

Testing Strategy

  • Unit tests: verify flags are injected into the build environment and that opt-out works
  • Integration tests: build numpy, Pillow, and lxml with the flags enabled; verify binaries pass readelf hardening checks
  • Performance tests: benchmark startup time and runtime for numpy/scipy with and without full RELRO
  • Compatibility tests: build on Debian, Fedora, Alpine, macOS, and Windows; verify no regressions

Aljefra Security Research – Binary Composition Vulnerability Analysis
Contact: [email protected]
Data: 373 binaries across 46 packages, analyzed 2026-04-12