CWE-252: Unchecked Return Value
Overview
Unchecked return values occur when code ignores error indicators from security-critical functions (setuid, chroot, malloc, read, write), assuming operations succeeded when they may have failed, leading to continued execution with wrong privileges, uninitialized memory, or invalid state.
OWASP Classification
A10:2025 - Mishandling of Exceptional Conditions
Risk
High: Ignoring return values causes privilege escalation (failed setuid but continuing as root), null pointer dereferences (failed malloc), incomplete I/O (partial read/write), failed security controls (chroot failed but continuing), and incorrect program state leading to vulnerabilities.
Remediation Steps
Core principle: Always check return values and handle unexpected results securely.
Locate Unchecked Return Values
When reviewing security scan results:
- Find setuid/setgid calls: Look for privilege changes without checking return
- Check malloc/calloc: Identify memory allocations without NULL checks
- Review file operations: Find open, read, write, close without error checks
- Identify chroot/chdir: Look for jail setup without validation
- Check crypto operations: Find signature/encryption without result checks
Vulnerable patterns:
// Ignoring critical return values
setuid(0); // Privilege change unchecked!
malloc(size); // NULL check missing!
chroot("/jail"); // Could fail silently!
read(fd, buf, size); // Partial read ignored!
Always Check Security-Critical Functions (Primary Defense)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
// VULNERABLE - setuid return value ignored
void drop_privileges_bad() {
// Attack: if setuid fails, continues running as root!
setuid(1000); // RETURN VALUE IGNORED!
// Attacker still has root privileges
execute_untrusted_code();
}
// SECURE - check setuid return value
void drop_privileges_safe() {
uid_t target_uid = 1000;
// Check return value
if (setuid(target_uid) != 0) {
perror("setuid failed");
fprintf(stderr, "Failed to drop privileges to UID %d\n", target_uid);
exit(1); // Abort if can't drop privileges
}
// Verify privilege drop succeeded
if (getuid() != target_uid || geteuid() != target_uid) {
fprintf(stderr, "Privilege drop verification failed!\n");
exit(1);
}
printf("Successfully dropped privileges to UID %d\n", target_uid);
// Now safe to execute untrusted code
execute_untrusted_code();
}
// VULNERABLE - malloc return not checked
void allocate_memory_bad(size_t size) {
char *buffer = malloc(size); // Could return NULL!
// Attack: malloc fails, buffer is NULL
strcpy(buffer, "data"); // NULL POINTER DEREFERENCE!
}
// SECURE - check malloc return value
void allocate_memory_safe(size_t size) {
char *buffer = malloc(size);
if (buffer == NULL) {
fprintf(stderr, "Memory allocation failed for size %zu\n", size);
exit(1); // Or return error to caller
}
// Safe to use buffer
strcpy(buffer, "data");
free(buffer);
}
// VULNERABLE - chroot return ignored
void setup_jail_bad() {
chroot("/var/jail"); // Could fail!
chdir("/"); // Could fail!
// Attack: if chroot failed, not actually jailed!
run_untrusted_service();
}
// SECURE - check chroot/chdir returns
void setup_jail_safe() {
if (chroot("/var/jail") != 0) {
perror("chroot failed");
exit(1);
}
if (chdir("/") != 0) {
perror("chdir failed");
exit(1);
}
printf("Jail setup successful\n");
run_untrusted_service();
}
Check File Operation Return Values
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
// VULNERABLE - file operations unchecked
void write_data_bad(const char *filename, const char *data) {
int fd = open(filename, O_WRONLY | O_CREAT, 0644); // Could fail!
write(fd, data, strlen(data)); // fd could be -1!
close(fd); // Closing invalid fd
}
// SECURE - check all file operations
void write_data_safe(const char *filename, const char *data) {
// Check open
int fd = open(filename, O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("Failed to open file");
return;
}
// Check write - must handle partial writes!
size_t total = strlen(data);
size_t written = 0;
while (written < total) {
ssize_t result = write(fd, data + written, total - written);
if (result == -1) {
perror("Write failed");
close(fd);
return;
}
written += result;
}
// Check close
if (close(fd) == -1) {
perror("Failed to close file");
}
printf("Successfully wrote %zu bytes\n", written);
}
// VULNERABLE - read return value ignored
void read_data_bad(int fd, char *buffer, size_t size) {
read(fd, buffer, size); // Could return -1 or less than size!
// Assuming buffer is fully populated - WRONG!
buffer[size - 1] = '\0';
}
// SECURE - check read return value
ssize_t read_data_safe(int fd, char *buffer, size_t size) {
ssize_t bytes_read = read(fd, buffer, size);
if (bytes_read == -1) {
perror("Read failed");
return -1;
}
if (bytes_read == 0) {
// EOF reached
printf("End of file\n");
}
// Null-terminate based on actual bytes read
if (bytes_read < size) {
buffer[bytes_read] = '\0';
}
return bytes_read;
}
Use Error-Checking Wrappers
// Wrapper functions that check returns
void *xmalloc(size_t size) {
void *ptr = malloc(size);
if (ptr == NULL) {
fprintf(stderr, "Fatal: memory allocation of %zu bytes failed\n", size);
abort();
}
return ptr;
}
int xsetuid(uid_t uid) {
if (setuid(uid) != 0) {
fprintf(stderr, "Fatal: failed to set UID to %d\n", uid);
perror("setuid");
exit(1);
}
// Verify it worked
if (getuid() != uid || geteuid() != uid) {
fprintf(stderr, "Fatal: UID change verification failed\n");
exit(1);
}
return 0;
}
int xopen(const char *pathname, int flags, mode_t mode) {
int fd = open(pathname, flags, mode);
if (fd == -1) {
fprintf(stderr, "Failed to open %s: ", pathname);
perror("");
exit(1);
}
return fd;
}
// Usage - no need to check returns
void use_wrappers() {
char *buf = xmalloc(1024); // Guaranteed non-NULL
int fd = xopen("/tmp/file", O_RDONLY, 0); // Guaranteed valid
xsetuid(1000); // Guaranteed to succeed or exit
}
Rust Result types (compile-time enforcement):
use std::fs::File;
use std::io::Read;
// SECURE - Result type forces error handling
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut file = File::open(path)?; // ? propagates error
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
// Caller must handle Result
fn main() {
match read_file("/etc/passwd") {
Ok(contents) => println!("File: {}", contents),
Err(e) => eprintln!("Error: {}", e),
}
}
Enable Compiler Warnings
// Use __attribute__((warn_unused_result))
__attribute__((warn_unused_result))
int secure_function() {
return 0;
}
void caller() {
secure_function(); // Compiler warning!
}
Compilation flags:
# GCC/Clang - warn on unused results
gcc -Wunused-result -Werror program.c
# C++17 - [[nodiscard]] attribute
[[nodiscard]] int important_function();
# Rust - #[must_use] attribute
#[must_use]
fn critical_operation() -> Result<(), Error> {
// ...
}
Test Return Value Checking
#include <assert.h>
#include <sys/stat.h>
void test_return_value_checking() {
// Test 1: setuid checked
printf("Test 1: Privilege drop checking\n");
if (getuid() == 0) {
drop_privileges_safe();
assert(getuid() != 0); // Should no longer be root
printf("✓ Privilege drop verified\n");
}
// Test 2: malloc checked
printf("Test 2: NULL pointer checking\n");
char *buf = xmalloc(1024);
assert(buf != NULL);
free(buf);
printf("✓ Allocation succeeded\n");
// Test 3: File operations checked
printf("Test 3: File operation checking\n");
write_data_safe("/tmp/test", "test data");
struct stat st;
assert(stat("/tmp/test", &st) == 0);
assert(st.st_size == 9); // "test data" = 9 bytes
printf("✓ File write verified\n");
// Test 4: Simulate failures
printf("Test 4: Error handling\n");
ssize_t result = read_data_safe(-1, buf, 100); // Invalid fd
assert(result == -1); // Should return error
printf("✓ Error handling works\n");
printf("All return value checks passed!\n");
}
Common Vulnerable Patterns
- Ignoring setuid() return value
- Not checking malloc() for NULL
- Assuming chroot() always succeeds
- Ignoring read/write byte counts
- Not validating chmod/chown results
Security Checklist
- All setuid/setgid/seteuid calls checked
- All malloc/calloc checked for NULL
- All chroot/chdir return values validated
- File operations (open, read, write) checked
- Partial read/write handled correctly
- Compiler warnings enabled (-Wunused-result)
- Critical functions marked warn_unused_result/nodiscard
- Tests verify error conditions handled