C Language Experience Discussion (Part 7): Array Out-of-Bounds, the Number One Killer of Memory Safety
π Application Scenarios & Problem Description
Array out-of-bounds is the most common and dangerous memory safety issue in C language. In scenarios such as “string processing”, “buffer operations”, and “dynamic array management”, a slight oversight can lead to accessing memory beyond the array boundaries. This is as dangerous as crossing lane lines while drivingβthere may be no immediate consequences, but it can lead to serious accidents at any moment.
Common Scenarios:
- Forgetting to consider the target buffer size when copying strings
- Incorrect boundary conditions when iterating through arrays
- Using incorrect indices after dynamically allocating memory
- Loss of array size information when passing function parameters
Hazards: Array out-of-bounds can lead to program crashes, data corruption, security vulnerabilities, and even exploitation by malicious actors.
β οΈ Error Code & Output Results
Code Function Description: The following code demonstrates three common scenarios of array out-of-bounds:
- Scenario 1: Target buffer too small during string copy
- Scenario 2: Incorrect boundary conditions during iteration
- Scenario 3: Loss of array size information in functions
#include <stdio.h>
#include <string.h>
// Scenario 1: String copy out-of-bounds
void string_copy_demo() {
char small_buffer[5]; // Can only hold 4 characters + null terminator
char source[] = "Hello World"; // 11 characters
printf("=== String Copy Out-of-Bounds Demonstration ===\n");
printf("Target buffer size: %zu\n", sizeof(small_buffer));
printf("Source string length: %zu\n", strlen(source));
strcpy(small_buffer, source); // Dangerous: out-of-bounds write!
printf("Buffer content after copy: %s\n", small_buffer);
}
// Scenario 2: Loop boundary error
void loop_boundary_demo() {
int arr[5] = {1, 2, 3, 4, 5};
int sum = 0;
printf("\n=== Loop Boundary Error Demonstration ===\n");
printf("Array size: 5\n");
// Error: i <= 5 leads to out-of-bounds access arr[5]
for (int i = 0; i <= 5; i++) {
printf("Accessing arr[%d] = %d\n", i, arr[i]);
sum += arr[i];
}
printf("Incorrect total sum: %d\n", sum);
}
// Scenario 3: Array decay in function
void process_array(int arr[]) { // Array decays to pointer
printf("\n=== Array Decay in Function Demonstration ===\n");
printf("Size of arr in function: %zu (actually pointer size)\n", sizeof(arr));
// Dangerous: unknown real size of array, may go out-of-bounds
for (int i = 0; i < 10; i++) { // Assuming there are 10 elements
printf("Accessing arr[%d] = %d\n", i, arr[i]);
}
}
int main() {
string_copy_demo();
loop_boundary_demo();
int test_arr[3] = {10, 20, 30};
process_array(test_arr);
return 0;
}
Output Results:
=== String Copy Out-of-Bounds Demonstration ===
Target buffer size: 5
Source string length: 11
Buffer content after copy: Hello World
=== Loop Boundary Error Demonstration ===
Array size: 5
Accessing arr[0] = 1
Accessing arr[1] = 2
Accessing arr[2] = 3
Accessing arr[3] = 4
Accessing arr[4] = 5
Accessing arr[5] = 32767
Incorrect total sum: 32782
=== Array Decay in Function Demonstration ===
Size of arr in function: 8 (actually pointer size)
Accessing arr[0] = 10
Accessing arr[1] = 20
Accessing arr[2] = 30
Accessing arr[3] = 32767
Accessing arr[4] = 0
Accessing arr[5] = 0
Accessing arr[6] = 0
Accessing arr[7] = 0
Accessing arr[8] = 0
Accessing arr[9] = 0
π Explanation of Incorrect Output:
- Lines 1-4: String copy out-of-bounds, although the complete string was output, memory has already been corrupted.
- Lines 5-12: Loop out-of-bounds access arr[5], reading uninitialized memory value 32767.
- Lines 13-24: Array decayed to pointer in function, sizeof returns pointer size 8, loop accessed memory beyond array bounds.
π Error Analysis:
- Memory Corruption: Out-of-bounds writes can overwrite memory of other variables.
- Data Pollution: Out-of-bounds reads can retrieve meaningless data.
- Program Instability: May lead to program crashes or abnormal behavior.
- Security Vulnerabilities: May be exploited for attacks.
β Correct Code & Output Results
Code Function Description: The following code demonstrates how to safely handle array operations:
- Scenario 1: Use strncpy to limit copy length and ensure string termination
- Scenario 2: Correct loop boundary conditions
- Scenario 3: Pass array size parameters to avoid array decay issues
#include <stdio.h>
#include <string.h>
// Scenario 1: Safe string copy
void safe_string_copy_demo() {
char small_buffer[5];
char source[] = "Hello World";
printf("=== Safe String Copy ===\n");
printf("Target buffer size: %zu\n", sizeof(small_buffer));
printf("Source string length: %zu\n", strlen(source));
// Safe: limit copy length, ensure string termination
strncpy(small_buffer, source, sizeof(small_buffer) - 1);
small_buffer[sizeof(small_buffer) - 1] = '\0'; // Ensure string termination
printf("After safe copy: %s\n", small_buffer);
printf("Actual length: %zu\n", strlen(small_buffer));
}
// Scenario 2: Correct loop boundary
void correct_loop_demo() {
int arr[5] = {1, 2, 3, 4, 5};
int sum = 0;
printf("\n=== Correct Loop Boundary ===\n");
printf("Array size: 5\n");
// Correct: i < 5 ensures no out-of-bounds
for (int i = 0; i < 5; i++) {
printf("Accessing arr[%d] = %d\n", i, arr[i]);
sum += arr[i];
}
printf("Correct total sum: %d\n", sum);
}
// Scenario 3: Pass array size
void safe_process_array(int arr[], size_t size) {
printf("\n=== Safe Array Processing ===\n");
printf("Array size: %zu\n", size);
// Safe: use passed size parameter
for (size_t i = 0; i < size; i++) {
printf("Accessing arr[%zu] = %d\n", i, arr[i]);
}
}
int main() {
safe_string_copy_demo();
correct_loop_demo();
int test_arr[3] = {10, 20, 30};
safe_process_array(test_arr, 3);
return 0;
}
Output Results:
=== Safe String Copy ===
Target buffer size: 5
Source string length: 11
After safe copy: Hell
Actual length: 4
=== Correct Loop Boundary ===
Array size: 5
Accessing arr[0] = 1
Accessing arr[1] = 2
Accessing arr[2] = 3
Accessing arr[3] = 4
Accessing arr[4] = 5
Correct total sum: 15
=== Safe Array Processing ===
Array size: 3
Accessing arr[0] = 10
Accessing arr[1] = 20
Accessing arr[2] = 30
π Explanation of Correct Output:
- Lines 1-5: Safe copy only copied 4 characters “Hell”, no out-of-bounds.
- Lines 6-12: Loop correctly accessed arr[0] to arr[4], total sum is 15.
- Lines 13-17: Function correctly used passed size parameter, only accessed 3 elements.
β¨ Comparison Points:
- Boundary Control: Always check array boundaries, do not access out-of-bounds.
- Size Passing: Use size_t type when passing array size in functions.
- String Safety: Use strncpy and ensure string termination.
- Loop Conditions: Use
<span><</span>instead of<span><=</span>as boundary condition.
π Best Practices
1. Boundary Checking (Highly Recommended)
What is Boundary Checking? Boundary checking refers to verifying that the index is within a valid range before accessing array elements. This is a fundamental method to prevent array out-of-bounds.
// Recommended: Always check boundaries
if (index >= 0 && index < array_size) {
value = array[index];
} else {
// Handle out-of-bounds situation
printf("Error: Index %d out of range [0, %zu)\n", index, array_size);
}
Why is it Important? Boundary checking can detect out-of-bounds access early, preventing program crashes and data corruption.
2. Safe String Functions
// Recommended: Use safe string functions
char buffer[10];
strncpy(buffer, source, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0'; // Ensure string termination
// Or use snprintf
snprintf(buffer, sizeof(buffer), "%s", source);
3. Compiler Configuration
# Enable warnings when compiling with GCC
gcc -Wall -Wextra -Werror -fsanitize=address -o program program.c
# Enable warnings when compiling with Clang
clang -Wall -Wextra -Werror -fsanitize=address -o program program.c
4. Team Code Standards
- Mandatory Boundary Checking: All array accesses must validate indices.
- Function Design: Use size_t type when passing array sizes.
- Code Review: Focus on loop boundary conditions and string operations.
- Test Coverage: Include boundary condition test cases.
5. Recommended Tools
- AddressSanitizer: Runtime detection of memory errors.
- Valgrind: Memory error detection tool.
- Static Analysis Tools: Clang Static Analyzer, Cppcheck.
- Code Checking: Use lint tools to check for potential issues.
ποΈ Expanding Ideas
Defensive Programming: Smart Array Wrapper
// Safe array wrapper
typedef struct {
int* data;
size_t size;
} SafeArray;
SafeArray* create_safe_array(size_t size) {
SafeArray* arr = malloc(sizeof(SafeArray));
arr->data = calloc(size, sizeof(int));
arr->size = size;
return arr;
}
int safe_array_get(SafeArray* arr, size_t index) {
if (arr == NULL || index >= arr->size) {
fprintf(stderr, "Error: Array access out of bounds index=%zu, size=%zu\n",
index, arr->size);
return 0; // Return default value
}
return arr->data[index];
}
void safe_array_set(SafeArray* arr, size_t index, int value) {
if (arr == NULL || index >= arr->size) {
fprintf(stderr, "Error: Array set out of bounds index=%zu, size=%zu\n",
index, arr->size);
return;
}
arr->data[index] = value;
}
Design Pattern: RAII Concept
// Macro for automatic management of array lifecycle
#define AUTO_ARRAY(type, name, size) \
type name[size]; \
size_t name##_size = size
#define SAFE_ACCESS(array, index) \
((index) < array##_size ? array[index] : (fprintf(stderr, "Out-of-bounds access\n"), 0))
π Summary Checklist
- β Always Check Array Boundaries: Validate index range before access.
- β Use Safe String Functions: strncpy + manually add terminator.
- β Pass Array Size Parameters: Avoid array decay issues.
- β Enable Compiler Warnings: -Wall -Wextra -fsanitize=address.
- β Use Static Analysis Tools: Detect potential issues early.
One-Sentence Summary: Array out-of-bounds is the number one killer of memory safety; through boundary checking, safe functions, and tool assistance, these issues can be effectively avoided.
Next Issue Preview: We will explore the dangling pointer issue and learn how to avoid accessing freed memory.