Multiple Operations on Resource in Single-Operation Context

Description

Multiple Operations on Resource in Single-Operation Context is a vulnerability where a program performs the same operation on a resource two or more times when the operation should only be applied once. This commonly manifests as double-free vulnerabilities, double-close of file handles, duplicate lock operations, or repeated initialization. The issue typically arises from unclear ownership of resources, improper error handling that triggers duplicate cleanup, or confusion about which code path is responsible for resource management. When operations that should be atomic or singular are repeated, the program's internal state becomes corrupted.

Risk

Duplicate operations on resources create serious security and stability risks. Double-free vulnerabilities in memory management can corrupt heap metadata, enabling arbitrary code execution. Double-close of file descriptors can close unrelated files if descriptor numbers are reused, leading to data corruption or security breaches. Duplicate unlocking of mutexes causes undefined behavior and can destabilize multi-threaded applications. Repeated initialization may overwrite valid data or leak previously allocated resources. These bugs often create exploitable conditions where attackers can manipulate program state through heap corruption or file descriptor confusion.

Solution

Implement clear ownership semantics for all resources. Use single-owner patterns where one component is responsible for resource lifecycle. Set pointers to NULL after freeing to prevent double-free. Close file descriptors by setting them to -1 after closing. Use reference counting for shared resources. Implement state tracking to detect and prevent duplicate operations. Use RAII (Resource Acquisition Is Initialization) patterns in C++ or try-with-resources in Java to ensure single cleanup. Review error handling paths carefully to prevent duplicate cleanup. Use static and dynamic analysis tools to detect multiple-operation bugs.

Common Consequences

ImpactDetails
AvailabilityScope: Availability

DoS: Crash, Exit, or Restart - Double-free and double-close operations typically cause crashes or undefined behavior.
IntegrityScope: Integrity

Modify Application Data - Heap corruption from double-free or file descriptor reuse can modify arbitrary data.
ConfidentialityScope: Confidentiality, Integrity, Availability

Execute Unauthorized Code or Commands - Heap corruption from double-free is often exploitable for arbitrary code execution.

Example Code

Vulnerable Code

// Vulnerable: Double-free
#include <stdlib.h>

void vulnerable_double_free(char* buffer, int error_occurred) {
    if (error_occurred) {
        free(buffer);  // First free
        // Error handling continues...
    }

    // Later in the function
    free(buffer);  // Second free - heap corruption!
}

// Vulnerable: Double-free in error handling
char* vulnerable_create_message(const char* prefix, const char* suffix) {
    char* result = malloc(256);
    if (result == NULL) return NULL;

    char* temp = malloc(128);
    if (temp == NULL) {
        free(result);  // Cleanup on error
        return NULL;
    }

    if (format_message(result, prefix, suffix) < 0) {
        free(temp);
        free(result);
        // Oops, if caller also frees on NULL return...
        return NULL;
    }

    free(temp);
    return result;
}

void vulnerable_caller() {
    char* msg = vulnerable_create_message("Hello", "World");
    if (msg == NULL) {
        // Some code paths might try to free msg here
        // But it was already freed inside the function!
    }
    free(msg);  // Double-free if error occurred!
}
// Vulnerable: Double-close of file descriptor
#include <unistd.h>
#include <fcntl.h>

void vulnerable_double_close() {
    int fd = open("data.txt", O_RDONLY);
    if (fd < 0) return;

    // Process file...
    if (process_file(fd) < 0) {
        close(fd);  // Close on error
        // Continue with error handling...
    }

    // Later cleanup
    close(fd);  // Double-close!
    // fd number may have been reused for another file
    // We just closed the wrong file!
}

// Vulnerable: File descriptor reuse attack scenario
void vulnerable_fd_reuse() {
    int sensitive_fd = open("/etc/shadow", O_RDONLY);  // fd = 5

    int user_fd = open("userfile.txt", O_RDONLY);  // fd = 6

    close(user_fd);  // Close user file
    close(user_fd);  // Double-close - closes fd 6 again

    // If fd 6 was reassigned to another file between closes,
    // we just closed an unintended file!

    // Even worse: if another thread opened a file and got fd 6,
    // we corrupted their file handle
}
// Vulnerable: Double-unlock of mutex
#include <pthread.h>

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int shared_data = 0;

void vulnerable_double_unlock(int error_occurred) {
    pthread_mutex_lock(&lock);

    shared_data++;

    if (error_occurred) {
        pthread_mutex_unlock(&lock);  // Unlock on error
        // Handle error...
    }

    // Normal path
    pthread_mutex_unlock(&lock);  // Double-unlock if error occurred!
    // Undefined behavior - may corrupt mutex state
}
// Vulnerable: Double socket bind
#include <sys/socket.h>
#include <netinet/in.h>

void vulnerable_double_bind() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in addr1 = {
        .sin_family = AF_INET,
        .sin_port = htons(21),
        .sin_addr.s_addr = INADDR_ANY
    };

    bind(sockfd, (struct sockaddr*)&addr1, sizeof(addr1));

    // Later, possibly in different code path
    struct sockaddr_in addr2 = {
        .sin_family = AF_INET,
        .sin_port = htons(21),
        .sin_addr.s_addr = inet_addr("192.168.1.1")
    };

    bind(sockfd, (struct sockaddr*)&addr2, sizeof(addr2));  // Double bind!
    // Second bind may fail or cause unexpected behavior
}

Fixed Code

// Fixed: Prevent double-free with NULL assignment
#include <stdlib.h>

void safe_free(char** ptr) {
    if (ptr != NULL && *ptr != NULL) {
        free(*ptr);
        *ptr = NULL;  // Prevent double-free
    }
}

void secure_operation(char* buffer, int error_occurred) {
    if (error_occurred) {
        safe_free(&buffer);  // Free and NULL
        return;
    }

    // Normal processing...
    safe_free(&buffer);  // Safe even if called twice
}

// Fixed: Clear ownership semantics
char* secure_create_message(const char* prefix, const char* suffix) {
    char* result = malloc(256);
    if (result == NULL) return NULL;

    char* temp = malloc(128);
    if (temp == NULL) {
        free(result);
        return NULL;  // Caller knows: NULL means nothing to free
    }

    if (format_message(result, prefix, suffix) < 0) {
        free(temp);
        free(result);
        return NULL;  // Clear contract: NULL = error, nothing to free
    }

    free(temp);
    return result;  // Caller owns this memory
}

void secure_caller() {
    char* msg = secure_create_message("Hello", "World");
    if (msg == NULL) {
        // Error occurred, but function cleaned up
        handle_error();
        return;  // Don't free - nothing allocated
    }

    // Use message...

    free(msg);  // Single free - we own this
    msg = NULL;  // Good practice
}
// Fixed: Prevent double-close with fd tracking
#include <unistd.h>
#include <fcntl.h>

int safe_close(int* fd) {
    if (fd != NULL && *fd >= 0) {
        int result = close(*fd);
        *fd = -1;  // Mark as closed
        return result;
    }
    return 0;
}

void secure_file_operation() {
    int fd = open("data.txt", O_RDONLY);
    if (fd < 0) return;

    if (process_file(fd) < 0) {
        safe_close(&fd);  // Close and mark
        handle_error();
        return;
    }

    // Normal cleanup
    safe_close(&fd);  // Safe - fd is now -1 if already closed
}

// Fixed: Using wrapper struct for resource tracking
typedef struct {
    int fd;
    int is_open;
} SafeFile;

SafeFile safe_open(const char* path, int flags) {
    SafeFile sf = { .fd = -1, .is_open = 0 };
    sf.fd = open(path, flags);
    if (sf.fd >= 0) {
        sf.is_open = 1;
    }
    return sf;
}

void safe_close_file(SafeFile* sf) {
    if (sf != NULL && sf->is_open && sf->fd >= 0) {
        close(sf->fd);
        sf->fd = -1;
        sf->is_open = 0;
    }
}
// Fixed: Proper mutex handling
#include <pthread.h>
#include <stdbool.h>

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int shared_data = 0;

void secure_operation(int error_occurred) {
    bool lock_held = false;

    pthread_mutex_lock(&lock);
    lock_held = true;

    shared_data++;

    if (error_occurred) {
        // Unlock only if we hold the lock
        if (lock_held) {
            pthread_mutex_unlock(&lock);
            lock_held = false;
        }
        handle_error();
        return;  // Already unlocked
    }

    // Normal path
    if (lock_held) {
        pthread_mutex_unlock(&lock);
        lock_held = false;
    }
}

// Better: Use RAII-style cleanup (C++ or cleanup attribute in GCC)
// C++ example:
/*
class MutexGuard {
    pthread_mutex_t& mutex;
public:
    MutexGuard(pthread_mutex_t& m) : mutex(m) {
        pthread_mutex_lock(&mutex);
    }
    ~MutexGuard() {
        pthread_mutex_unlock(&mutex);  // Single unlock in destructor
    }
};

void secure_cpp_operation(int error_occurred) {
    MutexGuard guard(lock);  // Lock acquired

    shared_data++;

    if (error_occurred) {
        // Exception or return - guard unlocks automatically
        throw std::runtime_error("error");
    }

    // Normal return - guard unlocks automatically
}
*/
// Fixed: Socket binding with proper state tracking
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdbool.h>

typedef struct {
    int sockfd;
    bool is_bound;
    struct sockaddr_in bound_addr;
} SafeSocket;

SafeSocket create_socket() {
    SafeSocket ss = { .sockfd = -1, .is_bound = false };
    ss.sockfd = socket(AF_INET, SOCK_STREAM, 0);
    return ss;
}

int safe_bind(SafeSocket* ss, struct sockaddr_in* addr) {
    if (ss == NULL || ss->sockfd < 0) return -1;

    // Fixed: Prevent double bind
    if (ss->is_bound) {
        // Already bound - error or ignore based on policy
        return -1;
    }

    int result = bind(ss->sockfd, (struct sockaddr*)addr, sizeof(*addr));
    if (result == 0) {
        ss->is_bound = true;
        ss->bound_addr = *addr;
    }
    return result;
}

void close_socket(SafeSocket* ss) {
    if (ss != NULL && ss->sockfd >= 0) {
        close(ss->sockfd);
        ss->sockfd = -1;
        ss->is_bound = false;
    }
}

CVE Examples

  • CVE-2009-0935: Mutex unlocked twice leading to corruption via invalid memory address.
  • CVE-2019-13351: Double-close of file descriptor causes wrong file associations.
  • CVE-2004-1939: XSS bypass using double-encoded URL characters (double-decode issue).

References

  1. MITRE Corporation. "CWE-675: Multiple Operations on Resource in Single-Operation Context." https://cwe.mitre.org/data/definitions/675.html
  2. CERT C Coding Standard. "MEM30-C. Do not access freed memory."
  3. CERT C Coding Standard. "FIO46-C. Do not access a closed file."