Fluent Bit accepted one more network read after its parser ran out of room. On the right build, that was enough to take control of the process.
In Fluent Bit v5.0.5, the Forward input asked MessagePack to grow an unpacking buffer. MessagePack could answer false: the allocation failed, the buffer did not grow, and there was no room for the next read. Fluent Bit ignored that answer and copied network-controlled bytes anyway.
I now have six clean executions on a pinned ARM64 glibc target: four primary runs and two independent clean-room reproductions. The accepted runs use no preload shim, forced allocation failure, /proc read, debugger, target log, target-binary read, or supplied runtime address. A pointer comes back over the Forward connection. The strongest primary run and both clean-room reproductions sent the final token to a listener in another container.
Natural reserve failure → network copy past the buffer → finalizer hijack → address leak → second natural failure → command execution.
There is an important boundary. In the clean successful harness, the sender and Fluent Bit share a container and the sender connects over loopback. The callback crosses into a separate container and network namespace. A fully separated sender reached the leak and the final overwrite in diagnostic runs, but did not return a clean execution token. The exploit inputs are network-only; a clean Internet-style remote-sender replay remains unfinished.
I am not publishing the exploit program, packet recipe, heap offsets, or control-flow sequence. The source flaw, proof boundary, fix, and evidence are enough to evaluate the finding without handing out a runnable chain.
The parser kept going after reserve failed
Fluent Bit's Forward input accepts MessagePack records over TCP. The vulnerable path sits in receiver_to_unpacker(), where the plugin moves received bytes into a MessagePack unpacker in small chunks.
The v5.0.5 caller effectively did this:
if (msgpack_unpacker_buffer_capacity(unpacker) < request_size) {
msgpack_unpacker_reserve_buffer(unpacker, request_size);
}
assert(msgpack_unpacker_buffer_capacity(unpacker) >= request_size);
received = receiver_recv(receiver,
msgpack_unpacker_buffer(unpacker),
request_size);
msgpack_unpacker_buffer_consumed(unpacker, received);
The assertion looks reassuring until the binary is built for release. With NDEBUG, it disappears. The one runtime result that could stop the copy is the Boolean returned by msgpack_unpacker_reserve_buffer(), and the caller discarded it.
MessagePack's expansion routine returns false while leaving the old buffer in place when allocation fails. Fluent Bit then writes into that old buffer as though growth succeeded. The bookkeeping call that follows subtracts the received length from the stale free-space count, which can wrap to a huge unsigned value and keep the bad copies going.
The clean bug classification is an unchecked return value leading to a heap out-of-bounds write.
The neighboring object contained function pointers
MessagePack zones own allocations created while decoding an object. Some objects need cleanup callbacks, so a zone keeps a finalizer array. Each entry contains a function pointer and a data pointer. During destruction, msgpack_zone_destroy() walks backward through the array and calls func(data).
On the tested glibc build, records can grow that finalizer array next to the unpacker buffer. When the later reserve fails, bytes from the Forward stream cross the end of the old buffer and reach live function pointers. Cleanup then supplies the indirect call.
The first network stage changes only part of one callback pointer and reuses an existing Fluent Bit send path. That sends one in-process code pointer to a socket the sender already owns. Because the build is known, that single pointer is enough to recover the randomized Fluent Bit base for that run.
The harness then drains the first pressure set, confirms that the worker processed those closes, and recreates the useful heap shape. A second natural reserve failure reaches the finalizer array again. This time the corrupted callback enters code already present in the binary, uses attacker-supplied context, and runs a shell command that returns a random token.
The shell replaces the Fluent Bit process and exits successfully after the callback. The token is the execution proof; a crash or missing process is not counted as success.
How the lab assists came out
The early proof used two lab assists: a preload shim selected the failed allocation, and the sender read process mappings for addresses. Those assists answered whether the overwrite could reach a callable finalizer. They were removed from the accepted chain.
The finished harness creates a narrow, repeatable memory-pressure window with ordinary Forward connections and a fixed address-space limit already applied to the victim. The first corrupted finalizer discloses the randomized code address over TCP. The second uses that address. The attacker never reads the victim's process files, executable, stdout, or stderr during the run.
This still depends on a known binary layout. The chain carries version-specific offsets from the leaked pointer to code and writable data in that exact build. It does not discover an unknown Fluent Bit binary over the network.
The public description stops here. Reproducing the high-level result does not require publishing the socket counts, payload lengths, offsets, or gadget sequence that make it operational.
What the proof actually covers
| Check | Result |
|---|---|
| Confirmed Six clean executions | Four primary runs and two clean-room runs returned different random tokens under fresh ASLR. |
| Confirmed Network address leak | The sender received a live code pointer over Forward TCP and derived the randomized image base from the known build layout. |
| Confirmed External callback | A listener in another container and network namespace received the command's token from the exploited process. |
| Confirmed Patched negative | The same first-stage geometry against a build that checks the reserve result returned no pointer, left Fluent Bit alive, and accepted a normal record afterward. |
| Boundary Separate remote sender | Separated sender runs reached the intended leak and overwrite in diagnostics, but did not return a clean command token. |
That is enough to say the source-build configuration supports network-driven code execution. It is not enough to claim a universal unauthenticated Internet RCE across Fluent Bit packages.
The clean target is unusually specific: Fluent Bit v5.0.5, Linux ARM64 PIE, glibc 2.41, jemalloc disabled, a known build layout, high file-descriptor capacity, a precise process address-space limit, and the supplied threaded Forward configuration. Changing those details changes heap behavior.
The fully separated sender result matters because transport timing changed where the natural allocation failure landed. That looks like an engineering problem rather than a contradiction of the primitive, but the article keeps it in the unfinished column until a clean token comes back.
The official image is outside the demonstrated chain
The source tree's CMake option defaults jemalloc to off. Someone who clones the repository and builds with ordinary release flags can therefore get the platform allocator. Fluent Bit's official Dockerfile enables jemalloc, and its upstream packaging driver does too.
That allocator split is decisive for this exploit. The glibc layout placed unlike allocations beside each other. Jemalloc segregated those sizes, so the finalizer array was not the object after the unpacker buffer. The official cr.fluentbit.io/fluent/fluent-bit:5.0.5 image contains the source bug, but the demonstrated RCE does not transfer to it.
The proven population is therefore narrow: custom or source builds that use glibc instead of jemalloc, enable a reachable Forward input, and reproduce the required layout and memory conditions. Some distribution packages may also use a system allocator, but I have not runtime-proven the chain on any of them.
There is no defensible affected-user count. Public pull totals mostly measure official jemalloc images, while private source builds leave no useful census. I found zero confirmed production deployments matching the full exploit environment. Any numeric estimate would stack guesses about version, allocator, architecture, configuration, reachability, and resource limits.
“Secure Forward authentication” means the optional Forward-protocol handshake. With a shared key or configured users, Fluent Bit sends a challenge and validates the client's response before ordinary log records reach this parser. TLS is a separate transport setting. The successful exploit configuration did not enable that handshake, so any client able to reach its listener could send the records.
I would not attach a blanket 9.8 score to this evidence. The consequence is full code execution, but the demonstrated chain has demanding build, memory, and topology requirements. A score that labels the attack low-complexity across the product would hide the facts that matter most.
What about denial of service?
The successful command replaces the Fluent Bit process, and the process exits after sending the token. That is a real availability effect on the exploited instance. The glibc pressure work also produced crashes while the chain was being tuned.
I did not establish a repeatable standalone denial of service against the official jemalloc image. One cgroup-pressure run ended in a kernel OOM kill; heavier later runs survived, and the patched version survived the exploit-shaped control. An OOM kill caused by consuming all memory is ordinary resource exhaustion unless the vulnerable state is shown to make it meaningfully easier or different.
“DDoS” would be the wrong label. I did not demonstrate a distributed attack, a durable outage, a supervisor or Kubernetes crash loop, or a fleet-wide failure. The supported availability claim is single-process termination as a consequence of the proven RCE chain.
Three repository advisories, and this was not one
As of October 16, 2026, Fluent Bit's GitHub repository lists three published security advisories:
| Published | Repository advisory |
|---|---|
| May 2024 | GHSA-5rjf-prwh-pp7q — Trace API memory corruption, rated Critical. |
| February 2025 | GHSA-pj57-h8rx-jw5m — Prometheus Remote Write input denial of service, rated High. |
| September 2026 | GHSA-jrp8-r9hx-gf73 — a separate out_forward Secure Forward PONG stack overflow, rated Critical. |
Three repository advisories do not mean Fluent Bit has had only three security bugs. The project has published other security-fix summaries, and its 2025 CNCF-sponsored audit reported eleven fixed issues. The repository advisory feed is simply much shorter than the security history.
This Forward reserve bug was fixed in a public pull request and release notes. I found no matching GHSA, CVE, or reporter credit. The September 2026 out_forward advisory is a different vulnerability with a different direction, trigger, and exploit.
That distinction matters to downstream maintainers. Advisory feeds drive scanners, backport decisions, and incident review. A small bug-fix entry tells an operator to upgrade; a security advisory tells them why the old build deserves urgent attention and which deployments need investigation.
The fix stops the invalid copy
Fluent Bit merged PR #11809, “in_forward: handle unpacker allocation failure,” on May 16, 2026. The fix checks unpacker creation and the Boolean result from msgpack_unpacker_reserve_buffer(). If reserve fails, the connection leaves the parser before another receive.
The patch shipped in Fluent Bit v5.0.6 and was backported to v4.2.5. The flawed receiver pattern dates to 2016, although ancestry does not prove the same exploit works across every older release, allocator, and package.
The patched negative control is the cleanest result in the set. With the same memory cap, socket shape, and first-stage payload, the patched binary returned no address, stayed alive, and acknowledged a normal Forward record afterward.
Operators should run v5.0.6, v4.2.5, or a later supported release. Custom builders should take the parser fix even when their current allocator frustrates this specific chain. Forward listeners that cross a trust boundary should also use network controls and the protocol's shared-key or user handshake.
The entire bug sits behind one rule: if the buffer cannot grow, do not read into it.