Skip to content

in_kubernetes_events: fix OOB reads from non-NUL-terminated msgpack strings - #12180

Open
zanarellidev wants to merge 1 commit into
fluent:masterfrom
zanarellidev:fix/k8s-events-msgpack-string-oob-read
Open

in_kubernetes_events: fix OOB reads from non-NUL-terminated msgpack strings#12180
zanarellidev wants to merge 1 commit into
fluent:masterfrom
zanarellidev:fix/k8s-events-msgpack-string-oob-read

Conversation

@zanarellidev

@zanarellidev zanarellidev commented Jul 31, 2026

Copy link
Copy Markdown

Summary

record_get_field_uint64() and record_get_field_time() in plugins/in_kubernetes_events/kubernetes_events.c call strtoul() / flb_strptime() directly on msgpack_object.via.str.ptr. msgpack strings are raw, length-prefixed bytes pointing directly into the decode buffer — they are not NUL-terminated — so these C-string functions can read past the field's true boundary. record_get_field_ptr()'s key match via strncmp(k->via.str.ptr, fieldname, strlen(fieldname)) had the same class of issue: no exact-length check, so a key that's a prefix of fieldname could false-match, and comparison length wasn't bounded by the key's own size.

A spec-compliant Kubernetes Event field (e.g. resourceVersion as a digit-only JSON string, per the Kubernetes API docs) placed near the edge of the decode buffer is enough to trigger an out-of-bounds read. Reproduced locally with a harness that calls the real flb_pack_json() on {"resourceVersion":"999999"}, places the resulting buffer against a PROT_NONE guard page, and calls the real (recompiled) plugin function on it — confirmed via lldb: EXC_BAD_ACCESS inside strtoul_l, called from record_get_field_uint64 at the line doing the unbounded strtoul call.

This is the same bug class fixed same-day for the sibling out_stackdriver plugin (#12022, backported in #12170), and the same class that produced a real CVE in this project before (GHSA-5rjf-prwh-pp7q).

A prior contributor flagged the same underlying issue in #12073 but self-closed it without a fix landing; the vulnerable code is still present at HEAD. Filing this with a fix and full explanation/repro so it doesn't fall through again.

Fix

Same pattern the maintainers already established for the sibling out_stackdriver fix: copy the field into a bounded, NUL-terminated stack buffer before parsing (strtoul, flb_strptime), and require an exact length match before the key strncmp.

Test plan

  • Full internal ctest suite (90 tests) passes clean after the fix.
  • in_kubernetes_events runtime test suite (flb-rt-in_kubernetes_events) passes clean after the fix.
  • Reproduced the crash on unmodified code with a guard-page harness (lldb-confirmed EXC_BAD_ACCESS in strtoul_l); same harness runs clean (resource_version=999999, exit 0) after the fix.

Happy to share the repro harness if useful for a permanent regression test — it needs an mmap'd guard page, which doesn't fit cleanly into the existing runtime-test infra (same constraint the maintainers' own #12022 fix had, which also didn't add a crash-reproducing test).

Summary by CodeRabbit

  • Bug Fixes
    • Improved Kubernetes event parsing for more accurate field and map key matching.
    • Added validation for timestamp and numeric values, rejecting empty or oversized inputs.
    • Prevented malformed or partial values from being interpreted incorrectly.

…trings

record_get_field_uint64() and record_get_field_time() called
strtoul()/flb_strptime() directly on msgpack_object.via.str.ptr.
msgpack strings are raw, length-prefixed bytes into the decode
buffer, not NUL-terminated, so these C-string functions could read
past the field's true boundary. record_get_field_ptr()'s strncmp()
key match had the same latent issue (a key that is a prefix of
fieldname could false-match, and a short key could still be read
past its bounds by strncmp with a longer fieldname length).

A spec-compliant Kubernetes Event field (e.g. resourceVersion as a
digit-only JSON string) placed at the edge of the decode buffer is
enough to trigger an out-of-bounds read; confirmed via a guard-page
harness that reproduces EXC_BAD_ACCESS inside strtoul_l, called from
record_get_field_uint64.

This is the same bug class fixed same-day for the sibling
out_stackdriver plugin (fluent#12022, backported in fluent#12170), and the same
class that produced GHSA-5rjf-prwh-pp7q in this project before.
Applies the same fix pattern here: copy the field into a bounded,
NUL-terminated stack buffer before parsing, and require an exact
length match before the key strncmp.

A prior contributor flagged the same underlying issue in fluent#12073, but
it was self-closed without a fix landing; the vulnerable code is
still present at HEAD.
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Kubernetes event MessagePack parsing now requires exact field-name lengths and safely handles timestamp and numeric string values through bounded buffers with validation for empty, oversized, and partially parsed inputs.

Changes

Kubernetes event parsing

Layer / File(s) Summary
Exact MessagePack field matching
plugins/in_kubernetes_events/kubernetes_events.c
Field lookup requires MessagePack keys to match the requested field name length before comparing contents.
Bounded timestamp and numeric parsing
plugins/in_kubernetes_events/kubernetes_events.c
Timestamp and unsigned integer strings are copied into bounded NUL-terminated buffers; empty, oversized, and partially parsed values are rejected.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: edsiper

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary fix for out-of-bounds reads from non-NUL-terminated MessagePack strings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/in_kubernetes_events/kubernetes_events.c`:
- Around line 250-251: Update item_get_timestamp() to treat
record_get_field_time() results other than 0 as failures, including -2 for empty
or oversized timestamp values. Ensure each lastTimestamp, firstTimestamp, and
metadata.creationTimestamp path only returns success when ret == 0, allowing
fallback timestamps to be attempted after malformed values.
- Line 256: Update the timestamp parsing around flb_strptime() to capture its
returned end pointer and require both successful parsing and *end == '\0'. Apply
this validation consistently to lastTimestamp, firstTimestamp, and
metadata.creationTimestamp so trailing characters are rejected.
- Around line 287-295: Update record_get_field_uint64() to parse the copied
string with an explicit unsigned, digits-only conversion instead of accepting
strtoul’s signed or overflowing results. Clear errno before conversion, reject
any non-digit input including signs, reject ERANGE and values exceeding
UINT64_MAX, and preserve the existing invalid-field return behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: df686c53-d865-4bd7-8b94-188fa68f3a35

📥 Commits

Reviewing files that changed from the base of the PR and between f724311 and d96d74e.

📒 Files selected for processing (1)
  • plugins/in_kubernetes_events/kubernetes_events.c

Comment on lines +250 to +251
if (v->via.str.size == 0 || v->via.str.size >= sizeof(buf)) {
return -2;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Propagate malformed timestamps as failures.

record_get_field_time() returns -2 for empty or oversized values, but item_get_timestamp() treats any result other than -1 as success at Line 320, Line 325, and Line 335. An invalid lastTimestamp can therefore suppress fallback to firstTimestamp or metadata.creationTimestamp. Accept only ret == 0 downstream, or normalize parse failures to the caller’s failure sentinel.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/in_kubernetes_events/kubernetes_events.c` around lines 250 - 251,
Update item_get_timestamp() to treat record_get_field_time() results other than
0 as failures, including -2 for empty or oversized timestamp values. Ensure each
lastTimestamp, firstTimestamp, and metadata.creationTimestamp path only returns
success when ret == 0, allowing fallback timestamps to be attempted after
malformed values.

memcpy(buf, v->via.str.ptr, v->via.str.size);
buf[v->via.str.size] = '\0';

if (flb_strptime(buf, "%Y-%m-%dT%H:%M:%SZ", &tm) == NULL) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/sh
set -eu

rg -n -C 8 '\bflb_strptime\s*\(' .
rg -n -C 6 'record_get_field_time|item_get_timestamp' \
  plugins/in_kubernetes_events tests/runtime/in_kubernetes_events.c

Repository: fluent/fluent-bit

Length of output: 38889


🏁 Script executed:

#!/bin/sh
set -eu

echo "== kubernetes_events.c parse function =="
sed -n '220,270p' plugins/in_kubernetes_events/kubernetes_events.c

echo "== flb_strptime return usage in flb_parser.c =="
sed -n '1985,2022p' src/flb_parser.c

echo "== kubernetes_events runtime tests mentioning timestamps =="
rg -n -C 4 'timestamp|creationTimestamp|lastTimestamp|firstTimestamp|20.*T[0-9]|invalid' tests/runtime/in_kubernetes_events.c plugins/in_kubernetes_tests || true

echo "== deterministic model of current behavior vs proposed end check =="
python3 - <<'PY'
from datetime import datetime
samples = [
    ("2023-01-15T12:34:56Z", True),
    ("2023-01-15T12:34:56Z extra", False),
    ("", False),
]
for s, exact_ok in samples:
    prefix = "2023-01-15T12:34:56Z"
    # Mimic: parsing succeeds if prefix matches ISO timestamp pattern exactly
    # current NULL-only check accepts valid prefix.
    parsed_ok = s.startswith(prefix)
    end_consumed = len(s) == len(prefix)
    print(f"{s!r}: parsed_ok={parsed_ok}, end_at_null={end_consumed}, current_accepts={parsed_ok}, proposed_accepts={parsed_ok and end_consumed}")
PY

Repository: fluent/fluent-bit

Length of output: 6433


Require flb_strptime() to consume the entire timestamp buffer.

The NULL check accepts valid ISO timestamp prefixes with trailing content, so lastTimestamp, firstTimestamp, or metadata.creationTimestamp could be parsed even when the string contains malformed/garbage suffixes. Capture the return pointer and require *end == '\0'.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/in_kubernetes_events/kubernetes_events.c` at line 256, Update the
timestamp parsing around flb_strptime() to capture its returned end pointer and
require both successful parsing and *end == '\0'. Apply this validation
consistently to lastTimestamp, firstTimestamp, and metadata.creationTimestamp so
trailing characters are rejected.

Comment on lines +287 to +295
len = v->via.str.size;
if (len == 0 || len > sizeof(buf) - 1) {
return -1;
}
memcpy(buf, v->via.str.ptr, len);
buf[len] = '\0';

*val = strtoul(buf, &end, 10);
if (end == NULL || end == buf || *end != '\0') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/sh
set -eu

rg -n -C 8 \
  '\b(record_get_field_uint64|strtoul|strtoull|strtoumax|ERANGE)\b' .

Repository: fluent/fluent-bit

Length of output: 50374


🌐 Web query:

C strtoul overflow ERANGE unsigned long documentation

💡 Result:

When using the C standard library function strtoul, an overflow occurs if the converted value is outside the range of representable values for an unsigned long [1][2]. When this range error occurs: 1. The function returns ULONG_MAX [3][4][5]. 2. The global variable errno is set to ERANGE [3][1][2]. Because ULONG_MAX is a valid return value for successful conversions, you cannot rely solely on the return value to detect an error [1][5]. To correctly check for overflow: 1. Set errno to 0 before calling strtoul [1][2][5]. 2. Perform the conversion [1][2]. 3. Check if errno is equal to ERANGE after the call [3][4][5]. If the input string begins with a minus sign, strtoul performs the conversion as if the value were positive and then negates the result within the unsigned type [4][5]. However, if the absolute value (the non-negated value) is so large that it would overflow even before negation, the function will still trigger the ERANGE error and return ULONG_MAX [6][5].

Citations:


🏁 Script executed:

#!/bin/sh
set -eu

printf '--- kubernetes_events.c relevant function ---\n'
sed -n '266,306p' plugins/in_kubernetes_events/kubernetes_events.c | cat -n

printf '\n--- behavioral probe if compiler is available ---\n'
if command -v cc >/dev/null 2>&1; then
  tmpdir="$(mktemp -d)"
  cat > "$tmpdir/strtoul_probe.c" <<'C'
`#define` _GNU_SOURCE
`#include` <stdio.h>
`#include` <stdlib.h>
`#include` <string.h>
`#include` <errno.h>
`#include` <limits.h>

int main(void) {
  char buf[32];
  char *end;
  unsigned long ul;
  int tests;

  tests = snprintf(buf, sizeof(buf), "%lu", ULONG_MAX);
  ul = (unsigned long) 0xdeadbeefdeafbeef; errno = 0; end = buf + strlen(buf) + 1;
  ul = strtoul(buf, &end, 10);
  printf("test_unchanged_buffer_returned_ULONG_MAX=%u\n", ul == ULONG_MAX);
  printf("test_unchanged_buffer_errno_ERANGE=%u\n", (int)(errno == ERANGE));
  printf("test_unchanged_buffer_consumed_chars=%ld\n", (long)(end - buf));

  tests = snprintf(buf, sizeof(buf), "%lu", ULONG_MAX + 1ULL);
  ul = (unsigned long) 0xdeadbeefdeafbeef; errno = 0; end = buf + strlen(buf) + 1;
  ul = strtoul(buf, &end, 10);
  printf("test_oversized_returned_ULONG_MAX=%u\n", ul == ULONG_MAX);
  printf("test_oversized_errno_ERANGE=%u\n", (int)(errno == ERANGE));
  printf("test_oversized_consumed_chars=%ld\n", (long)(end - buf));

  memset(buf, 0, sizeof(buf));
  ul = (unsigned long) 0xdeadbeefdeafbeef; errno = 0; end = buf + strlen(buf) + 1;
  ul = strtoul("-1", &end, 10);
  printf("test_minus_one_ret=%08lx end=%ld\n", ul, (long)(end - "-1"));
  return 0;
}
C
  if cc -O2 -o "$tmpdir/strtoul_probe" "$tmpdir/strtoul_probe.c" 2>/tmp/strtoul_compile.log; then
    "$tmpdir/strtoul_probe"
  else
    printf 'compile_failed\n'
    cat /tmp/strtoul_compile.log
  fi
  rm -rf "$tmpdir"
else
  printf 'cc not available\n'
fi

Repository: fluent/fluent-bit

Length of output: 246


🏁 Script executed:

#!/bin/sh
set -eu

printf '%s\n' '--- kubernetes_events.c relevant function ---'
sed -n '266,306p' plugins/in_kubernetes_events/kubernetes_events.c | cat -n

printf '%s\n' ''
printf '%s\n' '--- behavioral probe if compiler is available ---'
if command -v cc >/dev/null 2>&1; then
  tmpdir="$(mktemp -d)"
  cat > "$tmpdir/strtoul_probe.c" <<'C'
`#define` _GNU_SOURCE
`#include` <stdio.h>
`#include` <stdlib.h>
`#include` <string.h>
`#include` <errno.h>
`#include` <limits.h>

int main(void) {
  char buf[32];
  char *end;
  unsigned long ul;
  int tests;

  tests = snprintf(buf, sizeof(buf), "%lu", ULONG_MAX);
  ul = (unsigned long) 0xdeadbeefdeafbeef; errno = 0; end = buf + strlen(buf) + 1;
  ul = strtoul(buf, &end, 10);
  printf("test_unchanged_buffer_returned_ULONG_MAX=%u\n", ul == ULONG_MAX);
  printf("test_unchanged_buffer_errno_ERANGE=%u\n", (int)(errno == ERANGE));
  printf("test_unchanged_buffer_consumed_chars=%ld\n", (long)(end - buf));

  tests = snprintf(buf, sizeof(buf), "%lu", ULONG_MAX + 1ULL);
  ul = (unsigned long) 0xdeadbeefdeafbeef; errno = 0; end = buf + strlen(buf) + 1;
  ul = strtoul(buf, &end, 10);
  printf("test_oversized_returned_ULONG_MAX=%u\n", ul == ULONG_MAX);
  printf("test_oversized_errno_ERANGE=%u\n", (int)(errno == ERANGE));
  printf("test_oversized_consumed_chars=%ld\n", (long)(end - buf));

  memset(buf, 0, sizeof(buf));
  ul = (unsigned long) 0xdeadbeefdeafbeef; errno = 0; end = buf + strlen(buf) + 1;
  ul = strtoul("-1", &end, 10);
  printf("test_minus_one_ret=%08lx end=%ld\n", ul, (long)(end - "-1"));
  return 0;
}
C
  if cc -O2 -o "$tmpdir/strtoul_probe" "$tmpdir/strtoul_probe.c" 2>/tmp/strtoul_compile.log; then
    "$tmpdir/strtoul_probe"
  else
    printf '%s\n' 'compile_failed'
    cat /tmp/strtoul_compile.log
  fi
  rm -rf "$tmpdir"
else
  printf '%s\n' 'cc not available'
fi

Repository: fluent/fluent-bit

Length of output: 1834


🌐 Web query:

C strtoull base 10 optional sign documentation ERANGE

💡 Result:

The C standard library function strtoull (and its related functions like strtoul) performs integer conversion on a string and is defined to handle optional signs and potential range errors as follows: Optional Signs The input string may begin with an arbitrary amount of whitespace (as determined by isspace) followed by an optional plus (+) or minus (-) sign [1][2]. This applies regardless of the specified base (including base 10) [3][4]. If a minus sign is present, the resulting value is negated [3][5][4]. Since the return type of strtoull is unsigned, this negation follows the rules of unsigned integer arithmetic (often referred to as modular arithmetic or unsigned wraparound) [6]. Range Errors (ERANGE) If the correct value of the conversion falls outside the range of representable values for the unsigned long long type, the function returns ULLONG_MAX and sets errno to ERANGE [1][7]. Usage Note Because ULLONG_MAX is a valid return value on success (it is the maximum representable value), a program should reset errno to 0 before calling strtoull and check it after the call to reliably distinguish between a successful conversion to ULLONG_MAX and a range error [7][8]. Summary of Behavior 1. Whitespace is skipped [1][4]. 2. An optional sign (+ or -) is parsed [3][5]. 3. The remaining characters are converted based on the provided base (or auto-detected if base is 0) [1][2]. 4. If the final value is out of range, ULLONG_MAX is returned and ERANGE is set [1][8]. Top Results: [1], [2], [5], [7], [8]

Citations:


Reject signed or overflowing numeric fields.

record_get_field_uint64() accepts 31-character strings, stores the result of strtoul() in a uint64_t, and never checks errno == ERANGE. This allows out-of-range values to saturate to ULONG_MAX and signs like -1 to be accepted. Use an explicit unsigned parser that accepts digits only, clears/checks errno, and rejects values larger than UINT64_MAX.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/in_kubernetes_events/kubernetes_events.c` around lines 287 - 295,
Update record_get_field_uint64() to parse the copied string with an explicit
unsigned, digits-only conversion instead of accepting strtoul’s signed or
overflowing results. Clear errno before conversion, reject any non-digit input
including signs, reject ERANGE and values exceeding UINT64_MAX, and preserve the
existing invalid-field return behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant