Deletion of Data Structure Sentinel

Description

Deletion of Data Structure Sentinel is a vulnerability where the accidental deletion of a data-structure sentinel can cause serious programming logic problems. Data-structure sentinels mark the structure of data, such as null characters terminating strings or special markers indicating the end of linked lists. When code allows easy access to and modification of this control data without proper protection, sentinels can be inadvertently removed. This causes functions that rely on sentinels to malfunction, potentially reading beyond intended boundaries, corrupting data, or enabling unauthorized access.

Risk

Deleted sentinels create unpredictable and dangerous behavior. Removing the null terminator from a C string causes string functions to read past the intended buffer, potentially leaking sensitive data or causing crashes. Deleted list terminators cause iteration to continue into invalid memory. The risk extends to security controls when sentinel deletion allows bypassing access restrictions or length limits. Buffer over-reads can leak sensitive information from memory. The vulnerability is particularly dangerous because its effects are not immediately visible and may manifest unpredictably.

Solution

Use abstraction libraries to shield applications from risky APIs that allow direct sentinel manipulation. Employ compiler-based buffer overflow protections such as Microsoft Visual Studio /GS flag, GCC FORTIFY_SOURCE, or technologies like StackGuard and ProPolice. Leverage OS-level preventive functionality such as ASLR and non-executable memory regions. Design data structures with clear interfaces that prevent direct access to sentinels. Validate all operations that could affect sentinels. Use length-prefixed data structures instead of sentinel-terminated ones where practical.

Common Consequences

ImpactDetails
AvailabilityScope: Availability, Other

DoS: Crash/Exit/Restart - Data structures may malfunction or cause crashes when sentinels are deleted.
AuthorizationScope: Authorization, Other

Gain Privileges or Assume Identity - Removing control characters like NULL may breach access controls or bypass security checks.
ConfidentialityScope: Confidentiality

Read Memory - Deleted string terminators cause reads beyond intended boundaries, potentially leaking sensitive data.

Example Code

Vulnerable Code

// Vulnerable: Null terminator overwritten
#include <stdio.h>
#include <string.h>

void vulnerable_string_fill(char *buffer, size_t buffer_size) {
    // Vulnerable: Fills entire buffer including null terminator position
    for (size_t i = 0; i < buffer_size; i++) {
        buffer[i] = 'a';
    }

    // Null terminator overwritten!
    // printf("%s") will read beyond buffer

    printf("Content: %s\n", buffer);  // Reads past buffer
}

void vulnerable_copy() {
    char buffer[10];

    // Vulnerable: Copies exactly 10 chars, no room for null
    strncpy(buffer, "0123456789", 10);

    // Vulnerable: buffer is not null-terminated
    printf("%s\n", buffer);  // Undefined behavior
}

void vulnerable_input_processing() {
    char buffer[256];

    // Vulnerable: fgets includes newline, strlen doesn't account for it
    fgets(buffer, sizeof(buffer), stdin);

    // Try to remove newline by overwriting with null
    size_t len = strlen(buffer);
    if (buffer[len - 1] == '\n') {
        buffer[len - 1] = '\0';
    }

    // Vulnerable: If input is exactly 255 chars with no newline
    // len - 1 might corrupt actual data or sentinel
}
// Vulnerable: Linked list sentinel deleted
#include <stdlib.h>

typedef struct Node {
    int data;
    struct Node* next;
} Node;

typedef struct {
    Node* head;
    Node* sentinel;  // Special end marker
} LinkedList;

void vulnerable_delete_node(LinkedList* list, int value) {
    Node* current = list->head;
    Node* prev = NULL;

    while (current != NULL) {
        if (current->data == value) {
            if (prev == NULL) {
                list->head = current->next;
            } else {
                prev->next = current->next;
            }

            // Vulnerable: No check if deleting sentinel!
            free(current);
            return;
        }
        prev = current;
        current = current->next;
    }

    // If attacker causes deletion of sentinel,
    // iteration will go past end of list
}

void iterate_list(LinkedList* list) {
    Node* current = list->head;

    // Vulnerable: Relies on sentinel to stop iteration
    while (current != list->sentinel) {
        process(current->data);
        current = current->next;

        // If sentinel was deleted, this loops forever
        // or accesses invalid memory
    }
}
# Vulnerable: Delimiter removal in data processing
class VulnerableParser:
    RECORD_DELIMITER = '\x00'  # Null byte separates records

    def parse_records(self, data):
        records = []
        current = ""

        for char in data:
            if char == self.RECORD_DELIMITER:
                records.append(current)
                current = ""
            else:
                current += char

        # Vulnerable: If last delimiter is missing, data lost
        return records

    def clean_input(self, data):
        # Vulnerable: Strips all control characters including delimiters
        cleaned = ''.join(c for c in data if c.isprintable() or c == ' ')

        # Record delimiters removed - data structure corrupted
        return cleaned

    def truncate_record(self, record, max_length):
        # Vulnerable: May cut off sentinel
        if len(record) > max_length:
            return record[:max_length]  # Delimiter potentially removed
        return record
// Vulnerable: Array sentinel removal
public class VulnerableArrayHandler {

    private static final int SENTINEL = Integer.MIN_VALUE;

    public int[] processArray(int[] data) {
        // Array expected to end with SENTINEL

        // Vulnerable: User can modify array including sentinel
        for (int i = 0; i < data.length; i++) {
            if (data[i] == SENTINEL) {
                // Vulnerable: Overwrites sentinel
                data[i] = 0;  // "Clean" the data
            }
        }

        return data;
        // Sentinel deleted - iteration won't know where to stop
    }

    public void iterateToSentinel(int[] data) {
        int i = 0;
        // Vulnerable: Assumes sentinel exists
        while (data[i] != SENTINEL) {
            process(data[i]);
            i++;
            // If sentinel was removed, ArrayIndexOutOfBoundsException
        }
    }
}

Fixed Code

// Fixed: Proper null terminator handling
#include <stdio.h>
#include <string.h>

void secure_string_fill(char *buffer, size_t buffer_size) {
    if (buffer_size == 0) return;

    // Fixed: Leave room for null terminator
    for (size_t i = 0; i < buffer_size - 1; i++) {
        buffer[i] = 'a';
    }

    // Fixed: Always ensure null termination
    buffer[buffer_size - 1] = '\0';

    printf("Content: %s\n", buffer);
}

void secure_copy() {
    char buffer[11];  // Fixed: Extra byte for null

    // Fixed: Copy at most n-1 chars, ensure null termination
    strncpy(buffer, "0123456789", sizeof(buffer) - 1);
    buffer[sizeof(buffer) - 1] = '\0';  // Fixed: Guarantee termination

    printf("%s\n", buffer);
}

// Fixed: Use safer string functions
void secure_copy_with_strlcpy() {
    char buffer[10];

    // Fixed: strlcpy always null-terminates
    strlcpy(buffer, "0123456789", sizeof(buffer));
    // Result: "012345678\0" (9 chars + null)

    printf("%s\n", buffer);
}

void secure_input_processing() {
    char buffer[256];

    if (fgets(buffer, sizeof(buffer), stdin) == NULL) {
        return;  // Fixed: Handle error
    }

    // Fixed: Safe newline removal
    size_t len = strlen(buffer);
    if (len > 0 && buffer[len - 1] == '\n') {
        buffer[len - 1] = '\0';
    }

    // Fixed: Buffer is always properly terminated
}
// Fixed: Protected linked list sentinel
#include <stdlib.h>
#include <stdbool.h>

typedef struct Node {
    int data;
    struct Node* next;
    bool is_sentinel;  // Fixed: Flag to identify sentinel
} Node;

typedef struct {
    Node* head;
    Node* sentinel;
} LinkedList;

LinkedList* create_list() {
    LinkedList* list = malloc(sizeof(LinkedList));

    // Fixed: Create protected sentinel
    list->sentinel = malloc(sizeof(Node));
    list->sentinel->data = 0;
    list->sentinel->next = NULL;
    list->sentinel->is_sentinel = true;

    list->head = list->sentinel;
    return list;
}

bool secure_delete_node(LinkedList* list, int value) {
    Node* current = list->head;
    Node* prev = NULL;

    while (current != list->sentinel) {
        if (current->data == value) {
            // Fixed: Check if trying to delete sentinel
            if (current->is_sentinel) {
                // Cannot delete sentinel
                return false;
            }

            if (prev == NULL) {
                list->head = current->next;
            } else {
                prev->next = current->next;
            }

            free(current);
            return true;
        }
        prev = current;
        current = current->next;
    }

    return false;  // Not found
}

void iterate_list(LinkedList* list) {
    Node* current = list->head;

    // Fixed: Check both sentinel flag and pointer
    while (current != NULL && !current->is_sentinel) {
        process(current->data);
        current = current->next;
    }
}
# Fixed: Protected delimiter handling
class SecureParser:
    RECORD_DELIMITER = '\x00'

    def parse_records(self, data):
        records = []
        current = ""

        for char in data:
            if char == self.RECORD_DELIMITER:
                records.append(current)
                current = ""
            else:
                current += char

        # Fixed: Handle final record without delimiter
        if current:
            records.append(current)

        return records

    def clean_input(self, data, preserve_delimiters=True):
        # Fixed: Optionally preserve delimiters
        if preserve_delimiters:
            cleaned = ''.join(
                c for c in data
                if c.isprintable() or c == ' ' or c == self.RECORD_DELIMITER
            )
        else:
            cleaned = ''.join(c for c in data if c.isprintable() or c == ' ')

        return cleaned

    def truncate_record(self, record, max_length):
        # Fixed: Ensure delimiter is preserved
        if len(record) > max_length:
            # Check if record ends with delimiter
            ends_with_delim = record.endswith(self.RECORD_DELIMITER)
            truncated = record[:max_length]

            # Fixed: Re-add delimiter if it was present
            if ends_with_delim and not truncated.endswith(self.RECORD_DELIMITER):
                truncated = truncated[:-1] + self.RECORD_DELIMITER

            return truncated
        return record

    # Fixed: Use explicit length instead of sentinel
    def parse_records_with_length(self, data, record_lengths):
        """Parse using explicit lengths instead of sentinels."""
        records = []
        offset = 0

        for length in record_lengths:
            if offset + length > len(data):
                raise ValueError("Data shorter than specified lengths")
            records.append(data[offset:offset + length])
            offset += length

        return records
// Fixed: Array without sentinel dependency
public class SecureArrayHandler {

    // Fixed: Use wrapper class instead of sentinel
    public static class SafeArray {
        private final int[] data;
        private final int length;  // Explicit length

        public SafeArray(int[] source) {
            this.length = source.length;
            this.data = Arrays.copyOf(source, length);
        }

        public int get(int index) {
            if (index < 0 || index >= length) {
                throw new IndexOutOfBoundsException();
            }
            return data[index];
        }

        public void set(int index, int value) {
            if (index < 0 || index >= length) {
                throw new IndexOutOfBoundsException();
            }
            data[index] = value;
        }

        public int length() {
            return length;
        }

        public void iterate(Consumer<Integer> processor) {
            for (int i = 0; i < length; i++) {
                processor.accept(data[i]);
            }
        }
    }

    // Fixed: If sentinel is required, protect it
    public static class SentinelArray {
        private static final int SENTINEL = Integer.MIN_VALUE;
        private final int[] data;

        public SentinelArray(int[] source) {
            // Fixed: Copy and add sentinel
            this.data = new int[source.length + 1];
            System.arraycopy(source, 0, this.data, 0, source.length);
            this.data[source.length] = SENTINEL;
        }

        public void set(int index, int value) {
            // Fixed: Prevent overwriting sentinel
            if (index < 0 || index >= data.length - 1) {
                throw new IndexOutOfBoundsException();
            }
            if (value == SENTINEL) {
                throw new IllegalArgumentException("Cannot use sentinel value");
            }
            data[index] = value;
        }

        public void iterate(Consumer<Integer> processor) {
            for (int i = 0; data[i] != SENTINEL; i++) {
                processor.accept(data[i]);
            }
        }
    }
}

CVE Examples

No specific CVEs are listed in the MITRE database for this CWE. However, the pattern is related to:

  • Buffer over-read vulnerabilities (CWE-125)
  • Improper null termination (CWE-170)
  • Off-by-one errors affecting string terminators

References

  1. MITRE Corporation. "CWE-463: Deletion of Data Structure Sentinel." https://cwe.mitre.org/data/definitions/463.html
  2. CERT C Secure Coding Standard. "STR32-C. Do not pass a non-null-terminated character sequence to a library function that expects a string."