Skip to content

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

Additional Resources