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
| Impact | Details |
|---|---|
| Availability | Scope: Availability DoS: Crash, Exit, or Restart - Double-free and double-close operations typically cause crashes or undefined behavior. |
| Integrity | Scope: Integrity Modify Application Data - Heap corruption from double-free or file descriptor reuse can modify arbitrary data. |
| Confidentiality | Scope: 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
- MITRE Corporation. "CWE-675: Multiple Operations on Resource in Single-Operation Context." https://cwe.mitre.org/data/definitions/675.html
- CERT C Coding Standard. "MEM30-C. Do not access freed memory."
- CERT C Coding Standard. "FIO46-C. Do not access a closed file."