Secure Programs HOWTO: writing safer software on Linux

Updated November 2026

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

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:

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:

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 memcpy and strcpy with checked versions when the buffer size can be worked out. Level 3 needs GCC 12 or newer and works only with optimization (-O1 or higher). The -U first 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

Files, temp files and privileges

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

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