Back to Blog
critical SEVERITY5 min read

How out-of-bounds array access happens in C kernel modules and how to fix it

A high-severity out-of-bounds array access vulnerability was discovered in the natflow_conntrack.c kernel module where the `ct->proto.tcp.state` value was used directly as an array index without bounds validation. An attacker capable of manipulating TCP connection state could trigger reads beyond the `tcp_conntrack_names[]` array, potentially leaking kernel memory or causing system crashes. The fix adds a simple bounds check using `ARRAY_SIZE()` before array access.

O
By Orbis AppSec
Published July 5, 2026Reviewed July 5, 2026

Answer Summary

This is an out-of-bounds array access vulnerability (CWE-125) in a C Linux kernel module where `ct->proto.tcp.state` is used as an index into `tcp_conntrack_names[]` without validation. If the state value exceeds the array bounds, the kernel reads unintended memory, risking information disclosure or crashes. The fix uses a ternary operator with `ARRAY_SIZE()` to validate the index before access, returning "UNKNOWN" for invalid states.

Vulnerability at a Glance

cweCWE-125
fixAdded ARRAY_SIZE() bounds check before array access
riskKernel memory disclosure or system crash
languageC (Linux kernel module)
root causeTCP state used as array index without bounds validation
vulnerabilityOut-of-bounds array read

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:

  1. Information Disclosure: The "string" printed could contain arbitrary kernel memory contents
  2. Kernel Crash: If the out-of-bounds pointer points to unmapped memory, the kernel panics
  3. 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

  1. 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.

  2. 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.

  3. Safe fallback value: Invalid states now print "UNKNOWN" instead of causing undefined behavior. This maintains functionality while eliminating the security risk.

  4. 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

  1. 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.

  2. Use ARRAY_SIZE() consistently: Never hardcode array sizes. The ARRAY_SIZE() macro automatically stays synchronized with array declarations.

  3. Prefer enums with explicit bounds: Define TCP_CONNTRACK_MAX or similar constants and use them for validation:
    c if (state >= TCP_CONNTRACK_MAX) { return -EINVAL; }

  4. Enable kernel static analysis: Run sparse and enable -Warray-bounds in 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 of sprintf()
  • 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.state field should have been validated before use in tcp_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] : default is a safe idiom for any array access with potentially untrusted indices
  • Regression tests should cover boundary conditions—the included test explicitly checks TCP_CONNTRACK_MAX and values beyond it

How Orbis AppSec Detected This

  • Source: The ct->proto.tcp.state field from connection tracking structures, which can be influenced by network packet processing
  • Sink: Direct array indexing at tcp_conntrack_names[ct->proto.tcp.state] in natflow_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.

References

Frequently Asked Questions

What is an out-of-bounds array access vulnerability?

An out-of-bounds array access occurs when code reads or writes memory outside the allocated bounds of an array, typically caused by using an unchecked index value that exceeds the array's size.

How do you prevent out-of-bounds array access in C?

Always validate array indices against the array size before access using macros like ARRAY_SIZE(), sizeof(), or explicit bounds constants. In kernel code, use kernel-provided macros and static analysis tools.

What CWE is out-of-bounds array access?

Out-of-bounds read is classified as CWE-125 (Out-of-bounds Read), while out-of-bounds write is CWE-787 (Out-of-bounds Write).

Is using unsigned integers enough to prevent out-of-bounds access?

No, unsigned integers only prevent negative indices. You must still validate that the index is less than the array size to prevent access beyond the upper bound.

Can static analysis detect out-of-bounds array access?

Yes, static analysis tools like Coverity, Sparse, and Coccinelle can detect many out-of-bounds access patterns, especially when array sizes are known at compile time.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #28

Related Articles

high

How CORS credential reflection happens in Hono middleware and how to fix it

A high-severity CORS misconfiguration in Hono's middleware (CVE-2026-54290) allowed any origin to be reflected with credentials when the `origin` option defaulted to wildcard. This vulnerability in the studio frontend could enable attackers to steal authenticated user data through cross-origin requests. The fix upgrades Hono from 4.12.21 to 4.12.25, which properly handles CORS origin validation.

high

How integer overflow in malloc happens in C libregexp and how to fix it

A high-severity integer overflow vulnerability was discovered in QuickJS's libregexp.c where multiplication to compute allocation size could wrap around, causing a heap overflow. The fix replaces the unsafe `malloc(sizeof(capture[0]) * lre_get_alloc_count(bc))` pattern with `calloc(lre_get_alloc_count(bc), sizeof(capture[0]))`, which safely handles the multiplication internally and prevents exploitation.

critical

How buffer overflow via sprintf() happens in C++ settings parsing and how to fix it

A critical buffer overflow vulnerability was discovered in `app/src/main/cpp/samp/settings.cpp` where `sprintf()` writes to a fixed 127-byte buffer (`char buff[0x7F]`) without bounds checking. If the `g_pszStorage` global variable contains a string longer than ~107 bytes, the formatted output exceeds the buffer, enabling stack corruption. The fix replaces `sprintf()` with `snprintf()` using `sizeof(buff)` to guarantee writes never exceed the declared buffer length.

medium

How integer overflow in bounds checking happens in C and how to fix it

A critical integer overflow vulnerability was discovered in the W_Read function of DOOM/w_file.c that allowed attackers to bypass bounds checking by crafting WAD files with malicious offset values near UINT_MAX. The fix implements a two-step validation approach that first checks if the offset exceeds the file length, then safely calculates the remaining bytes without risk of overflow.

critical

How buffer overflow in strcat() happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in the `daemonize()` function of `tpl.c`, where command-line arguments are concatenated into a fixed-size 8192-byte buffer using `strcat()` without any bounds checking. An attacker who controls command-line arguments can overflow this buffer to corrupt adjacent memory and potentially achieve arbitrary code execution. The fix adds a buffer-length check before each concatenation to ensure writes never exceed the declared buffer size.

critical

How command injection happens in Node.js subprocess and how to fix it

A critical command injection vulnerability in `tools/dev/src/index.ts` allowed attackers to execute arbitrary shell commands through unsanitized subprocess arguments. The fix was simple but essential: explicitly setting `shell: false` in the `spawn()` call to prevent shell metacharacter interpretation. This vulnerability demonstrates why subprocess handling requires explicit security controls in Node.js.