Introduction
In the natflow kernel module, we discovered a high-severity out-of-bounds array access vulnerability in natflow_conntrack.c at line 321. The conntrackinfo_read() function, which handles reading connection tracking information, used ct->proto.tcp.state directly as an index into the tcp_conntrack_names[] array without any validation.
This matters because kernel modules operate with the highest privileges on a Linux system. An out-of-bounds read in kernel space doesn't just crash an application—it can leak sensitive kernel memory (including cryptographic keys, credentials, or memory layout information useful for further exploits) or cause a complete system crash.
The Vulnerability Explained
The vulnerable code in natflow_conntrack.c looked like this:
case IPPROTO_TCP:
ct_i->len += sprintf(ct_i->data + ct_i->len, "%s ", tcp_conntrack_names[ct->proto.tcp.state]);
The problem is deceptively simple: ct->proto.tcp.state is used directly as an array index with no validation. The tcp_conntrack_names[] array contains a fixed number of string entries corresponding to valid TCP connection states (like TCP_CONNTRACK_NONE, TCP_CONNTRACK_SYN_SENT, TCP_CONNTRACK_ESTABLISHED, etc.).
How Could This Be Exploited?
If an attacker can manipulate the TCP state value stored in a connection tracking entry—whether through crafted network packets, race conditions, or memory corruption elsewhere—they could set ct->proto.tcp.state to a value like TCP_CONNTRACK_MAX + 100.
When the kernel executes tcp_conntrack_names[ct->proto.tcp.state], it calculates the memory address as:
address = base_address_of_tcp_conntrack_names + (malicious_state * sizeof(char*))
This reads a pointer from kernel memory far beyond the intended array, then attempts to dereference that pointer in the sprintf() call. The result could be:
- Information Disclosure: The "string" printed could contain arbitrary kernel memory contents
- Kernel Crash: If the out-of-bounds pointer points to unmapped memory, the kernel panics
- Exploitation Primitive: Controlled memory reads can help attackers bypass KASLR (Kernel Address Space Layout Randomization)
Real-World Attack Scenario
An attacker on the local network sends specially crafted TCP packets designed to create connection tracking entries with corrupted state values. When an administrator runs a diagnostic tool that reads /proc entries populated by conntrackinfo_read(), the kernel reads beyond the array bounds, potentially including:
- Portions of the kernel heap containing credentials
- Function pointers useful for building ROP chains
- Memory layout information defeating KASLR
The Fix
The fix adds a bounds check before accessing the array, using the kernel's ARRAY_SIZE() macro:
Before (Vulnerable)
case IPPROTO_TCP:
ct_i->len += sprintf(ct_i->data + ct_i->len, "%s ", tcp_conntrack_names[ct->proto.tcp.state]);
After (Fixed)
case IPPROTO_TCP:
ct_i->len += sprintf(ct_i->data + ct_i->len, "%s ",
ct->proto.tcp.state < ARRAY_SIZE(tcp_conntrack_names) ?
tcp_conntrack_names[ct->proto.tcp.state] : "UNKNOWN");
Why This Works
-
ARRAY_SIZE()macro: This kernel macro calculates the number of elements in a statically-allocated array at compile time, ensuring the bounds check always matches the actual array size. -
Ternary operator for inline validation: The check happens in the same expression as the access, making it impossible to forget the validation or have it optimized away.
-
Safe fallback value: Invalid states now print "UNKNOWN" instead of causing undefined behavior. This maintains functionality while eliminating the security risk.
-
No performance impact: The bounds check is a simple integer comparison—negligible overhead compared to the
sprintf()call that follows.
Prevention & Best Practices
For Kernel Module Developers
-
Always validate array indices: Any value from external sources (network, user input, other kernel subsystems) must be bounds-checked before use as an array index.
-
Use
ARRAY_SIZE()consistently: Never hardcode array sizes. TheARRAY_SIZE()macro automatically stays synchronized with array declarations. -
Prefer enums with explicit bounds: Define
TCP_CONNTRACK_MAXor similar constants and use them for validation:
c if (state >= TCP_CONNTRACK_MAX) { return -EINVAL; } -
Enable kernel static analysis: Run
sparseand enable-Warray-boundsin your build configuration.
General C Best Practices
- Use static analysis tools (Coverity, CodeQL, Semgrep) in CI/CD pipelines
- Consider using bounded string functions like
snprintf()instead ofsprintf() - Implement defensive programming: validate all inputs, even from "trusted" sources
Key Takeaways
- Never use external values as array indices without bounds checking—the
ct->proto.tcp.statefield should have been validated before use intcp_conntrack_names[] - Use
ARRAY_SIZE()for bounds checks in kernel code—it's compile-time evaluated and always matches the actual array size - Kernel memory disclosure vulnerabilities are critical—even a "simple" array overread can leak KASLR offsets, credentials, or cryptographic material
- The fix pattern is reusable:
index < ARRAY_SIZE(array) ? array[index] : defaultis a safe idiom for any array access with potentially untrusted indices - Regression tests should cover boundary conditions—the included test explicitly checks
TCP_CONNTRACK_MAXand values beyond it
How Orbis AppSec Detected This
- Source: The
ct->proto.tcp.statefield from connection tracking structures, which can be influenced by network packet processing - Sink: Direct array indexing at
tcp_conntrack_names[ct->proto.tcp.state]innatflow_conntrack.c:321 - Missing control: No bounds validation before using the state value as an array index
- CWE: CWE-125 (Out-of-bounds Read)
- Fix: Added
ARRAY_SIZE()bounds check with ternary operator to return "UNKNOWN" for invalid state values
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This vulnerability demonstrates a fundamental truth about C programming: every array access is a potential security boundary. The tcp_conntrack_names[] array access seemed innocuous—just converting a state enum to a human-readable string—but the missing bounds check created a kernel memory disclosure vulnerability.
The fix is elegant in its simplicity: a single conditional expression with ARRAY_SIZE() transforms dangerous undefined behavior into safe, predictable output. When writing kernel code or any security-sensitive C, make bounds checking reflexive. The few CPU cycles spent on validation are infinitely cheaper than the cost of a kernel memory leak or system crash.