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:
- No stack canary detection – the overflow proceeds silently, no abort
- Writable GOT entries – the attacker overwrites a function pointer in the GOT to redirect execution
- 65,636 ROP gadgets – a Turing-complete instruction set for arbitrary code execution
- No FORTIFY_SOURCE – unsafe C functions like
strcpyandsprintfare 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:
- Add stack canaries to 347 binaries – buffer overflows that currently silently corrupt the stack would be detected and cause a controlled abort
- Enable full RELRO on 364 binaries – the GOT becomes read-only after startup, eliminating GOT overwrite attacks
- Enable FORTIFY_SOURCE on 352 binaries – unsafe C functions (strcpy, sprintf, strcat) are replaced with bounds-checked versions at compile time
- 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-buildflagssince 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
- 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.
- Phase 2: Add equivalent defaults to setuptools’ distutils integration, so
python setup.py build_extalso benefits. - Phase 3: Update pip documentation to describe the hardening defaults and opt-out mechanism.
- 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
readelfhardening 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