Secure Programs HOWTO: writing safer software on Linux
Secure programs come from a few habits: treat every input as hostile, never build commands or queries by pasting strings together, pick memory-safe languages where you can, and let the compiler, sanitizers and scanners catch what you miss. This page covers each habit with concrete code, the compiler flags the OpenSSF recommends for C and C++, and tools that run on any current Linux distribution.
Principles that still hold
- Least privilege. Run with the fewest rights that work. A web app does not need root, and a backup job does not need write access to
/etc. - Fail closed. If a check cannot be completed (a parse error, a timeout), deny the request instead of letting it through.
- Keep it small. Every feature, dependency and open port is code an attacker can try. Less code means fewer bugs.
- Defense in depth. Assume one layer will fail. Validation, sandboxing and hardening each catch different mistakes.
Treat all input as hostile
Input is anything your program did not create itself: command-line arguments, environment variables, files, network data, database rows, other processes' output, even file names. Validate it where it enters your program:
- Allow, do not block. Describe what valid input looks like (for example, a user name is 1–32 characters from
a-z0-9_-) and reject the rest. Trying to list every bad character always misses one. - Check length, range and type before use. An age of -5 or a quantity of 4,000,000,000 should never reach your business logic.
- Parse into a typed value once, then pass the typed value around. Do not re-check raw strings in many places.
- Limit size. Cap request bodies, upload sizes and line lengths so one client cannot use all your memory.
Never build commands by pasting strings
Injection bugs happen when data is glued into text that another interpreter reads: a shell, an SQL engine, HTML, a file path.
Shell commands
Do not pass user data to system(), popen(), Python's subprocess with shell=True, or backticks. Call the program directly with an argument list, so no shell parses the input:
# Python: unsafe - a file name like "x; rm -rf ~" runs rm
subprocess.run(f"convert {name} out.png", shell=True)
# Python: safe - name is one argument, never parsed by a shell
subprocess.run(["convert", "--", name, "out.png"], check=True)
In C, use posix_spawn() or fork() plus execve() with an argv array. The -- stops the called program from reading a name that starts with - as an option.
SQL
Always use parameterized queries. The database driver sends the data separately from the SQL text, so it can never change the query:
# unsafe
cur.execute("SELECT * FROM users WHERE email = '" + email + "'")
# safe
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
File paths and output
If a user picks a file name, a name like ../../etc/passwd can escape your directory. Resolve the full path and check that it still starts with your base directory, or better, open files relative to a directory descriptor with openat(). When you write user data into HTML, escape it for that context, or use a template engine that escapes by default. In C, never pass user data as the format string: printf(buf) is a bug, printf("%s", buf) is correct.
Memory safety
Out-of-bounds writes, use-after-free and similar memory errors in C and C++ cause a large share of serious vulnerabilities. For new projects, consider a memory-safe language such as Rust, Go, Java, C# or Python. In June 2025 CISA and the NSA published joint guidance urging exactly this. For existing C code, these rules prevent most problems:
- Never use
gets(). It cannot be used safely and was removed from the C11 standard. Usefgets()orgetline(). - Use
snprintf()instead ofsprintf(), and check its return value: a result equal to or larger than the buffer size means the output was cut short. - For string copies,
strlcpy()andstrlcat()are in glibc since 2.38 and in POSIX.1-2024. They always terminate the string and return the length they tried to create, so you can detect truncation. Avoidstrcpy(),strcat()andstrncpy()(which does not always add the terminating zero). - Check size arithmetic before
malloc(). GCC (and Clang) provide checked arithmetic:if (__builtin_mul_overflow(n, size, &total)) return -1;. - Set pointers to
NULLafterfree()and give each allocation one clear owner. - Check every return value, especially from
read(),write(),malloc()andsetuid().
Compiler hardening flags
Hardening flags make the compiler warn about risky code and add run-time checks that turn many memory bugs into a clean crash instead of an exploit. The OpenSSF Compiler Options Hardening Guide (updated August 2026) recommends this baseline for GCC and Clang:
-O2 -Wall -Wformat -Wformat=2 -Wconversion -Wimplicit-fallthrough \
-Werror=format-security \
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 \
-D_GLIBCXX_ASSERTIONS \
-fstrict-flex-arrays=3 \
-fstack-clash-protection -fstack-protector-strong \
-Wl,-z,nodlopen -Wl,-z,noexecstack \
-Wl,-z,relro -Wl,-z,now \
-Wl,--as-needed -Wl,--no-copy-dt-needed-entries
Add -fPIE -pie for programs (or -fPIC -shared for libraries), -fcf-protection=full on x86-64 and -mbranch-protection=standard on 64-bit ARM. Leave out -Wl,-z,nodlopen when you build a plugin or any other library that programs load with dlopen(): it marks the library as not loadable that way. What the main ones do:
-D_FORTIFY_SOURCE=3- Replaces calls such as
memcpyandstrcpywith checked versions when the buffer size can be worked out. Level 3 needs GCC 12 or newer and works only with optimization (-O1or higher). The-Ufirst avoids a "redefined" warning on compilers that already set a default level (Ubuntu's GCC uses level 2, for example). -fstack-protector-strong- Puts a guard value next to local arrays and stops the program if a stack buffer overflow changes it.
-fstack-clash-protection- Stops large stack allocations from jumping over the guard page into other memory.
-fPIE -pie- Builds a position-independent executable, so address space layout randomization (ASLR) can move the program itself, not just its libraries.
-Wl,-z,relro -Wl,-z,now- "Full RELRO": the dynamic linker resolves all symbols at start-up and then makes the relocation tables read-only, so an attacker cannot overwrite them.
GCC 14 and newer also offer -fhardened, one switch that enables a fixed set of these: _FORTIFY_SOURCE=3, _GLIBCXX_ASSERTIONS, -ftrivial-auto-var-init=zero, PIE, full RELRO, -fstack-protector-strong, -fstack-clash-protection and, on x86-64, -fcf-protection=full. Run gcc --help=hardened to see the list for your version. As of November 2026 the newest GCC release is 16.2.
Distribution packages are already built with most of these flags. Your own builds are not, unless you add them, for example in CMake with add_compile_options() and add_link_options(), or with CFLAGS and LDFLAGS for a Makefile.
Check a binary
readelf -h ./myprog | grep Type # "DYN (Position-Independent Executable file)"
readelf -lW ./myprog | grep GNU_RELRO # present: RELRO is on
readelf -d ./myprog | grep -E 'BIND_NOW|FLAGS' # BIND_NOW / NOW: full RELRO
readelf -lW ./myprog | grep GNU_STACK # should be "RW", not "RWE"
Fedora and RHEL also ship annocheck (package annobin-annocheck), which reports every hardening property in one run.
Sanitizers and fuzzing in testing
Sanitizers add checks at compile time and report the exact line of a bug when your tests run:
gcc -g -O1 -fno-omit-frame-pointer -fsanitize=address,undefined -o myprog-test myprog.c
./myprog-test
AddressSanitizer (address) finds buffer overflows and use-after-free. UndefinedBehaviorSanitizer (undefined) finds signed overflow, bad shifts and similar. ThreadSanitizer (-fsanitize=thread) finds data races and cannot be combined with ASan. Sanitized programs run roughly 2 to 4 times slower, so use them in tests and CI, not in production, and never in setuid programs. Build test binaries without _FORTIFY_SOURCE, because it does not work well with the sanitizers.
Fuzzing feeds millions of generated inputs into a function. Pair a fuzzer such as libFuzzer (built into Clang) or AFL++ with ASan to find parsing bugs before attackers do.
Handle secrets and randomness
- Do not put passwords, API keys or private keys in source code or in git. Load them at run time from a file with mode 600, or from your platform's secret store. systemd services can receive them with
LoadCredential=. - Environment variables are visible to child processes and often end up in crash reports and logs. Prefer files for long-lived secrets.
- For keys, tokens and session IDs, use the kernel's random generator:
getrandom()in C, thesecretsmodule in Python. Never userand()orrandom()for security. - Wipe secrets from memory with
explicit_bzero(); a plainmemset()beforefree()may be removed by the optimizer. - Do not invent your own cryptography or password hashing. Use a maintained library (libsodium, OpenSSL 3) and a password hash such as Argon2 or bcrypt.
- Scan repositories for leaked secrets with a tool such as gitleaks:
gitleaks git -v .
Files, temp files and privileges
- Create temporary files with
mkstemp()ortmpfile(), never with a predictable name in/tmp. - Avoid "check, then use" races: do not test a file with
access()and then open it. Open it and handle the error. - Avoid setuid programs. If a task needs root, run a small service with only that job and let the main program talk to it.
- Sandbox services with systemd:
NoNewPrivileges=yes,ProtectSystem=strict,ProtectHome=yes,PrivateTmp=yesandDynamicUser=yescost one line each.systemd-analyze security myapp.serviceshows what is still open.
Scan your dependencies
Most code you ship was written by someone else. Check it for known vulnerabilities on every build:
osv-scanner scan -r ./my-project # many ecosystems, uses the OSV database
pip-audit -r requirements.txt # Python
npm audit # JavaScript
cargo audit # Rust (install with: cargo install cargo-audit)
Commit lock files so builds are repeatable, update dependencies regularly in small steps, and remove the ones you no longer use.
Checklist before release
- Every external input validated against an allow-list, with size limits.
- No shell strings, no SQL string building, no user-controlled format strings.
- Builds use the hardening flags (or
-fhardened);readelforannocheckconfirms it. - Tests and fuzzers run under ASan and UBSan in CI.
- No secrets in the repository; secret scan passes.
- Dependency scan passes; lock files committed.
- The service runs as an unprivileged user with systemd sandboxing.
David A. Wheeler's classic book behind this HOWTO is still published by its author at dwheeler.com; the LDP copy is at tldp.org.
Sources
- Compiler Options Hardening Guide for C and C++, OpenSSF Best Practices Working Group
- Program instrumentation options (-fhardened, -fstack-protector, -fsanitize), GCC manual
- Integer overflow built-in functions, GCC manual
- GCC home page (current releases)
- readelf(1), gets(3), strlcpy(3), bzero(3), mkstemp(3), posix_spawn(3) and getrandom(2), man7.org
- systemd.exec(5), man7.org
- Input Validation Cheat Sheet and Query Parameterization Cheat Sheet, OWASP
- Memory Safe Languages: Reducing Vulnerabilities in Modern Software Development, CISA and NSA
- OSV-Scanner usage
- pip-audit, PyPA
- npm audit, npm documentation and cargo-audit, RustSec
- annobin-annocheck package, Fedora Packages
- gitleaks
- Secure Programming HOWTO, David A. Wheeler