Kernel Security Code Examples
Introduction
Understanding kernel security requires more than reading vulnerability definitions. You need to understand how security problems appear in real code.
This tutorial provides safe, educational code examples for the major concepts covered in the Kernel Security & Vulnerability Analysis course.
The examples focus on:
- Understanding vulnerable patterns
- Identifying incorrect assumptions
- Writing defensive fixes
- Creating regression tests
- Learning Linux security primitives
The examples are intentionally designed for controlled educational environments.
They do not provide instructions for exploiting real kernels, bypassing security protections, or attacking third-party systems.
Prerequisites
You should be comfortable with:
- C programming
- Linux command line
- Pointers
- Structures
- Processes
- Threads
- Memory allocation
- Basic system calls
- GCC
- Make
A Linux environment is recommended.
For kernel-specific experiments, use a disposable virtual machine rather than your primary workstation.
Lab Structure
Every example follows the same security-analysis process:
1Code 2 ↓ 3Identify Assumption 4 ↓ 5Find Security Problem 6 ↓ 7Understand Root Cause 8 ↓ 9Write Defensive Fix 10 ↓ 11Regression Test
Example 1: Linux File Permissions
Linux permissions are the first security mechanism we will examine.
Create a test file:
1touch secret.txt
Inspect its permissions:
1ls -l secret.txt
You may see:
1-rw-r--r-- 1 user user 0 Aug 22 20:00 secret.txt
The permission groups are:
1-rw-r--r-- 2 ||| ||| ||| 3 ||| ||| +++--- Others 4 ||| +++------- Group 5 +++----------- Owner
The three primary permissions are:
1r = read 2w = write 3x = execute
Change the file so only the owner can read and write it:
1chmod 600 secret.txt
Verify:
1ls -l secret.txt
Expected form:
1-rw-------
Security Lesson
Permissions establish an important access-control boundary before an application reaches more complicated security mechanisms.
Example 2: Checking Permissions in C
A program can inspect file metadata using stat().
1#include <stdio.h> 2#include <sys/stat.h> 3 4int main(void) 5{ 6 struct stat st; 7 8 if (stat("secret.txt", &st) != 0) { 9 perror("stat"); 10 return 1; 11 } 12 13 printf("File mode: %o\n", st.st_mode & 0777); 14 15 return 0; 16}
Compile:
1gcc permissions.c -o permissions
Run:
1./permissions
The important security idea is that applications should not assume a resource has the permissions they expect.
Security-sensitive code should verify authorization rather than relying on assumptions.
Example 3: Linux Capabilities
Linux capabilities allow privileges to be divided into smaller units.
Inspect the capabilities of your current shell:
1capsh --print
You can also inspect the capability information of a process:
1grep Cap /proc/self/status
You may see:
1CapInh: 2CapPrm: 3CapEff: 4CapBnd: 5CapAmb:
These values represent different capability sets.
Security Principle
Instead of giving an application unrestricted administrative privileges:
1Application 2 ↓ 3Full root privileges
prefer:
1Application 2 ↓ 3Only required privileges
This is the principle of least privilege.
Example 4: Process Isolation with Namespaces
Linux namespaces allow processes to have isolated views of certain system resources.
You can inspect the namespaces of your current process:
1ls -l /proc/self/ns/
You may see entries such as:
1ipc 2mnt 3net 4pid 5user 6uts
Each represents a namespace context.
For example:
1readlink /proc/self/ns/pid
might produce:
1pid:[4026531836]
The important concept is that two processes can exist on the same kernel while seeing different isolated resources.
Example 5: Resource Limits with Cgroups
Cgroups provide resource-management functionality.
A conceptual container configuration might enforce:
1CPU 2 ↓ 3Limited allocation 4 5Memory 6 ↓ 7Maximum allowed memory 8 9Processes 10 ↓ 11Maximum process count
Inspect cgroup information:
1cat /proc/self/cgroup
The exact output depends on whether the system uses cgroup v1 or cgroup v2.
Security Lesson
Resource exhaustion is also a security concern.
A service that can consume unlimited CPU, memory, or processes can potentially affect system availability.
Example 6: Seccomp System-Call Filtering
Seccomp can restrict system calls available to a process.
A simple C example can use the prctl() interface.
1#include <stdio.h> 2#include <unistd.h> 3#include <sys/prctl.h> 4#include <linux/seccomp.h> 5 6int main(void) 7{ 8 if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) { 9 perror("prctl"); 10 return 1; 11 } 12 13 if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT) != 0) { 14 perror("seccomp"); 15 return 1; 16 } 17 18 printf("Process is running with strict seccomp.\n"); 19 20 return 0; 21}
Compile:
1gcc seccomp_demo.c -o seccomp_demo
Why NO_NEW_PRIVS Matters
Before applying restrictions, applications commonly establish:
1NO_NEW_PRIVS 2 ↓ 3Prevent privilege gains 4 ↓ 5Apply security policy
Seccomp is useful because it reduces the system-call attack surface available to a process.
Example 7: Demonstrating an Unsafe Buffer Operation
The following example intentionally demonstrates a vulnerable programming pattern in a standalone toy program.
1#include <stdio.h> 2#include <string.h> 3 4int main(void) 5{ 6 char buffer[16]; 7 8 strcpy(buffer, "This string is longer than the buffer"); 9 10 printf("%s\n", buffer); 11 12 return 0; 13}
The problem is:
1buffer capacity 2 ↓ 316 bytes 4 5input 6 ↓ 7larger than 16 bytes 8 9result 10 ↓ 11out-of-bounds write
The program demonstrates the root cause of a buffer overflow without targeting a real system.
Defensive Version
Use bounded operations and explicitly enforce the intended limit.
1#include <stdio.h> 2#include <string.h> 3 4int main(void) 5{ 6 char buffer[16]; 7 8 snprintf(buffer, sizeof(buffer), "%s", "Safe input"); 9 10 printf("%s\n", buffer); 11 12 return 0; 13}
The security invariant is:
1Data written 2 ≤ 3Destination capacity
Example 8: Integer Overflow
Consider:
1#include <stdio.h> 2#include <stdint.h> 3 4int main(void) 5{ 6 uint32_t count = 100000; 7 uint32_t size = count * 100000; 8 9 printf("Size: %u\n", size); 10 11 return 0; 12}
The multiplication may produce a result larger than the representable range.
The dangerous pattern is:
1User-controlled count 2 ↓ 3Arithmetic 4 ↓ 5Overflow 6 ↓ 7Incorrect size 8 ↓ 9Unsafe memory operation
Defensive Approach
Validate arithmetic before performing security-sensitive calculations.
1#include <stdio.h> 2#include <stdint.h> 3#include <limits.h> 4 5int main(void) 6{ 7 uint32_t count = 100000; 8 uint32_t element_size = 100000; 9 10 if (count != 0 && 11 element_size > UINT32_MAX / count) { 12 fprintf(stderr, "Integer overflow detected\n"); 13 return 1; 14 } 15 16 uint32_t size = count * element_size; 17 18 printf("Safe size: %u\n", size); 19 20 return 0; 21}
The important security property is:
1Calculated size must be representable 2before it is used.
Example 9: Use-After-Free
A simplified standalone example:
1#include <stdio.h> 2#include <stdlib.h> 3 4int main(void) 5{ 6 int *value = malloc(sizeof(int)); 7 8 if (!value) 9 return 1; 10 11 *value = 42; 12 13 free(value); 14 15 printf("%d\n", *value); 16 17 return 0; 18}
The problem is:
1malloc() 2 ↓ 3use 4 ↓ 5free() 6 ↓ 7use again 8 ↓ 9Use-after-free
The memory is no longer owned by the program after free().
Defensive Version
Invalidate the pointer after releasing the resource:
1#include <stdio.h> 2#include <stdlib.h> 3 4int main(void) 5{ 6 int *value = malloc(sizeof(int)); 7 8 if (!value) 9 return 1; 10 11 *value = 42; 12 13 free(value); 14 value = NULL; 15 16 return 0; 17}
Setting the pointer to NULL is not a universal solution to lifetime bugs, but it can prevent accidental reuse through that particular pointer.
The deeper solution is to design clear ownership and lifetime rules.
Example 10: Double-Free
An intentionally unsafe toy example:
1#include <stdlib.h> 2 3int main(void) 4{ 5 int *value = malloc(sizeof(int)); 6 7 if (!value) 8 return 1; 9 10 free(value); 11 free(value); 12 13 return 0; 14}
The problem is:
1Allocate 2 ↓ 3Free 4 ↓ 5Free again 6 ↓ 7Invalid lifetime operation
Defensive Ownership Model
A better pattern is:
1free(value); 2value = NULL;
Then cleanup code can safely check:
1if (value != NULL) { 2 free(value); 3 value = NULL; 4}
However, the real fix is to establish which part of the program owns the object and ensure exactly one owner performs final cleanup.
Example 11: Race Condition
Consider a shared counter:
1#include <pthread.h> 2#include <stdio.h> 3 4int counter = 0; 5 6void *worker(void *arg) 7{ 8 for (int i = 0; i < 100000; i++) { 9 counter++; 10 } 11 12 return NULL; 13} 14 15int main(void) 16{ 17 pthread_t a, b; 18 19 pthread_create(&a, NULL, worker, NULL); 20 pthread_create(&b, NULL, worker, NULL); 21 22 pthread_join(a, NULL); 23 pthread_join(b, NULL); 24 25 printf("Counter: %d\n", counter); 26 27 return 0; 28}
The expression:
1counter++;
is not guaranteed to behave as one indivisible operation between threads.
Conceptually:
1Thread A Thread B 2 3read counter 4 read counter 5increment 6 increment 7write 8 write
One update can overwrite another.
Defensive Version
Use a mutex:
1#include <pthread.h> 2#include <stdio.h> 3 4int counter = 0; 5pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; 6 7void *worker(void *arg) 8{ 9 for (int i = 0; i < 100000; i++) { 10 pthread_mutex_lock(&lock); 11 12 counter++; 13 14 pthread_mutex_unlock(&lock); 15 } 16 17 return NULL; 18} 19 20int main(void) 21{ 22 pthread_t a, b; 23 24 pthread_create(&a, NULL, worker, NULL); 25 pthread_create(&b, NULL, worker, NULL); 26 27 pthread_join(a, NULL); 28 pthread_join(b, NULL); 29 30 printf("Counter: %d\n", counter); 31 32 pthread_mutex_destroy(&lock); 33 34 return 0; 35}
Now the critical section is protected.
1Lock 2 ↓ 3Modify shared state 4 ↓ 5Unlock
Example 12: TOCTOU Pattern
A simplified unsafe pattern is:
1if (access("data.txt", W_OK) == 0) { 2 FILE *file = fopen("data.txt", "w"); 3 4 if (file) { 5 /* use file */ 6 fclose(file); 7 } 8}
The conceptual problem is:
1Check 2 ↓ 3Time passes 4 ↓ 5Resource changes 6 ↓ 7Use
The file state may change between access() and fopen().
Better Design
Instead of checking separately and then performing the operation, perform the operation itself and handle its result:
1#include <stdio.h> 2 3int main(void) 4{ 5 FILE *file = fopen("data.txt", "w"); 6 7 if (!file) { 8 perror("fopen"); 9 return 1; 10 } 11 12 fprintf(file, "Controlled write\n"); 13 14 fclose(file); 15 16 return 0; 17}
The important principle is:
Avoid separate security checks when the state can change between the check and the operation.
Example 13: Secure Input Validation
A common defensive pattern is to validate input before processing it.
1#include <stdio.h> 2#include <stdlib.h> 3 4int main(void) 5{ 6 char input[32]; 7 8 if (!fgets(input, sizeof(input), stdin)) { 9 return 1; 10 } 11 12 char *end; 13 long value = strtol(input, &end, 10); 14 15 if (end == input) { 16 fprintf(stderr, "Invalid number\n"); 17 return 1; 18 } 19 20 if (value < 0 || value > 1000) { 21 fprintf(stderr, "Value outside allowed range\n"); 22 return 1; 23 } 24 25 printf("Accepted value: %ld\n", value); 26 27 return 0; 28}
Security validation should consider:
1Type 2 ↓ 3Range 4 ↓ 5Length 6 ↓ 7Format 8 ↓ 9Relationship with other values
Never assume user input is valid because it came from your own application.
Example 14: Memory-Safety Testing with AddressSanitizer
Compilers provide useful security-analysis tooling.
Compile a toy program with AddressSanitizer:
1gcc -fsanitize=address -g vulnerable.c -o vulnerable
Run:
1./vulnerable
AddressSanitizer can detect several memory-safety problems, including:
- Out-of-bounds accesses
- Use-after-free
- Double-free
- Some memory leaks
A typical analysis workflow is:
1Buggy Code 2 ↓ 3Sanitizer 4 ↓ 5Runtime Report 6 ↓ 7Identify Fault 8 ↓ 9Patch 10 ↓ 11Run Test Again
This is extremely useful when learning memory-safety analysis.
Example 15: UndefinedBehaviorSanitizer
Compile with:
1gcc -fsanitize=undefined -g example.c -o example
UBSan can detect various forms of undefined behavior.
For security research, sanitizers provide valuable feedback during development and testing.
A useful combination for educational C projects is:
1gcc -Wall -Wextra -g \ 2 -fsanitize=address,undefined \ 3 example.c -o example
Then execute:
1./example
The goal is not merely to make the program compile.
The goal is to discover unsafe behavior during testing.
Example 16: Compiler Security Warnings
Enable compiler warnings:
1gcc -Wall -Wextra -Wpedantic -Wconversion \ 2 example.c -o example
Warnings can reveal:
- Suspicious conversions
- Type mismatches
- Unused values
- Potential logic mistakes
- Dangerous assumptions
Treating warnings seriously is part of secure development.
A practical development configuration is:
1Warnings 2 + 3Sanitizers 4 + 5Static Analysis 6 + 7Regression Tests
Example 17: Checking ASLR
Linux exposes relevant process information through /proc.
You can inspect the process memory layout:
1cat /proc/self/maps
The exact addresses will depend on your system and process.
The important concept is that memory regions may be placed at varying addresses.
You can inspect the kernel's ASLR configuration with:
1cat /proc/sys/kernel/randomize_va_space
A commonly encountered configuration is:
12
which enables broad address-space randomization.
Do not disable security protections on production systems for experiments.
Use an isolated lab environment when studying their behavior.
Example 18: Inspecting Process Security State
Linux exposes useful process information under /proc.
For example:
1cat /proc/self/status
This provides information about:
- Process IDs
- Memory
- Capabilities
- Seccomp state
- User IDs
- Group IDs
You can search for specific security-related fields:
1grep -E 'Uid|Gid|Cap|Seccomp' /proc/self/status
This is useful for learning how Linux exposes process security information.
Example 19: Kernel Attack-Surface Review
When reviewing a component, create an interface map.
For example:
1 Kernel Component 2 | 3 +---------------+---------------+ 4 | | | 5 System Call ioctl() Device File 6 | | | 7 +---------------+---------------+ 8 | 9 User Input
For each interface, record:
1Interface: 2Input: 3Expected Type: 4Maximum Size: 5Validation: 6Privilege Required: 7Shared State: 8Error Handling:
This transforms a large codebase into smaller security-review units.
Example 20: Defensive Vulnerability Triage
Suppose your toy driver produces a crash during testing.
Do not immediately assume the cause.
Start with:
1Crash 2 ↓ 3Can it be reproduced? 4 ↓ 5Can the input be minimized? 6 ↓ 7Which function fails? 8 ↓ 9What object/state is involved? 10 ↓ 11What assumption is violated? 12 ↓ 13What security boundary is affected?
Then document the finding:
1Bug Class: 2Affected Function: 3Trigger: 4Observed Failure: 5Root Cause: 6Security Invariant: 7Defensive Fix: 8Regression Test:
This is the correct mindset for vulnerability triage.
Example 21: Defensive Patch Analysis
Suppose a vulnerable function contains:
1if (length > MAX_SIZE) 2 return -EINVAL; 3 4buffer = malloc(length * element_size);
A defensive review should ask:
1Is length negative? 2 3Can length * element_size overflow? 4 5Is MAX_SIZE appropriate? 6 7Is element_size trusted? 8 9Does the allocation size match the later copy? 10 11Can another thread modify related state?
A stronger defensive implementation could validate the multiplication:
1#include <stdint.h> 2#include <stdbool.h> 3 4bool safe_multiply_size( 5 size_t a, 6 size_t b, 7 size_t *result) 8{ 9 if (b != 0 && a > SIZE_MAX / b) 10 return false; 11 12 *result = a * b; 13 return true; 14}
Usage:
1size_t total; 2 3if (!safe_multiply_size(length, element_size, &total)) { 4 return -1; 5}
The important lesson is that security patches should establish explicit safety properties.
Example 22: Regression Test
After fixing a vulnerability, create a test that exercises the previously broken condition.
A conceptual test:
1#include <assert.h> 2 3int validate_length(size_t length, size_t maximum) 4{ 5 return length <= maximum; 6} 7 8int main(void) 9{ 10 assert(validate_length(10, 100) == 1); 11 assert(validate_length(100, 100) == 1); 12 assert(validate_length(101, 100) == 0); 13 14 return 0; 15}
Compile:
1gcc -Wall -Wextra regression.c -o regression
Run:
1./regression
If the program exits successfully, the assertions passed.
A regression test should preserve the security property that the patch introduced.
Example 23: Fuzzing a Safe Parser
Fuzzing does not need to begin with a kernel.
Start with a small parser.
Example:
1#include <stddef.h> 2 3int parse_value(const unsigned char *data, size_t length) 4{ 5 if (data == NULL) 6 return -1; 7 8 if (length > 1024) 9 return -1; 10 11 /* 12 * Process controlled input here. 13 */ 14 15 return 0; 16}
The security boundary is:
1Unknown Input 2 ↓ 3Length Validation 4 ↓ 5Parser
A fuzzer can then provide many different inputs and verify that:
1Valid input 2 ↓ 3Expected result 4 5Invalid input 6 ↓ 7Safe rejection
The important property is that unexpected input should not cause unsafe behavior.
Example 24: Kernel Security Lab Workflow
The complete course lab can combine everything:
1 +----------------------+ 2 | Toy Vulnerable Code | 3 +----------+-----------+ 4 | 5 ↓ 6 +----------------------+ 7 | Interface Analysis | 8 +----------+-----------+ 9 | 10 ↓ 11 +----------------------+ 12 | Input Validation | 13 | Review | 14 +----------+-----------+ 15 | 16 ↓ 17 +----------------------+ 18 | Memory / Race / | 19 | Lifetime Analysis | 20 +----------+-----------+ 21 | 22 ↓ 23 +----------------------+ 24 | Reproduce Safely | 25 +----------+-----------+ 26 | 27 ↓ 28 +----------------------+ 29 | Root Cause | 30 +----------+-----------+ 31 | 32 ↓ 33 +----------------------+ 34 | Defensive Patch | 35 +----------+-----------+ 36 | 37 ↓ 38 +----------------------+ 39 | Regression Test | 40 +----------+-----------+ 41 | 42 ↓ 43 +----------------------+ 44 | Security Verification| 45 +----------------------+
Building a Secure-Lab Toolkit
A useful educational environment can contain:
1kernel-security-lab/ 2├── examples/ 3│ ├── permissions/ 4│ ├── capabilities/ 5│ ├── namespaces/ 6│ ├── seccomp/ 7│ ├── memory-safety/ 8│ ├── race-conditions/ 9│ └── integer-safety/ 10│ 11├── tests/ 12│ ├── regression/ 13│ └── fuzz/ 14│ 15├── patches/ 16│ 17└── reports/
Each vulnerability should have:
1example 2 + 3test 4 + 5patch 6 + 7analysis
This makes the course reproducible and useful as a cybersecurity portfolio project.
Security Analysis Workflow
For every code example, use these questions:
11. What does the code do? 2 32. What data is untrusted? 4 53. What security boundary exists? 6 74. What privilege does the code have? 8 95. What assumptions are made? 10 116. Can those assumptions become false? 12 137. Can memory be accessed outside its lifetime? 14 158. Can arithmetic overflow? 16 179. Can multiple threads change the state? 18 1910. What security invariant should hold? 20 2111. How should the code be fixed? 22 2312. How can the fix be tested?
These questions are more valuable than memorizing vulnerability names.
Final Lab Challenge
Build a controlled toy driver or security-sensitive C component containing several independent defensive-analysis exercises.
For each issue:
1Find 2 ↓ 3Classify 4 ↓ 5Explain 6 ↓ 7Patch 8 ↓ 9Test
Your final report should include:
1Vulnerability: 2Affected Component: 3Root Cause: 4Security Boundary: 5Security Invariant: 6Defensive Patch: 7Regression Test: 8Verification:
Do not attempt to turn the lab into an attack against a real kernel, server, container host, or third-party system.
The objective is to develop the ability to read privileged code, recognize dangerous assumptions, fix security defects, and verify the fix.
Conclusion
Code-level kernel security becomes easier to understand when every vulnerability is treated as a broken security property.
The most useful patterns to recognize are:
1Out-of-bounds access 2 ↓ 3Broken memory boundary 4 5Use-after-free 6 ↓ 7Broken object lifetime 8 9Double-free 10 ↓ 11Broken ownership 12 13Race condition 14 ↓ 15Broken synchronization 16 17TOCTOU 18 ↓ 19Broken state assumption 20 21Integer overflow 22 ↓ 23Broken size calculation 24 25Privilege problem 26 ↓ 27Broken authorization boundary
The defensive workflow remains the same:
1Understand 2 ↓ 3Find 4 ↓ 5Analyze 6 ↓ 7Patch 8 ↓ 9Test 10 ↓ 11Verify
That workflow provides a strong foundation for Linux kernel security, vulnerability research, secure systems programming, and defensive cybersecurity engineering.