Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Kinetic-KID: bounded.rs

Crate: kinetic-kid Stage: 06 Reading Time: 15 minutes Depends On: serde (external)


What Is This?

This module provides custom Serde deserialization helpers designed to strictly limit the maximum number of items in a deserialized array or vector. Rather than relying on standard Serde routines that happily ingest arrays of unlimited length, this code enforces a hard, unyielding cap at the exact moment of processing. It acts as an essential security gatekeeper during payload parsing in the Kinetic network. If a network peer tries to send a payload that exceeds these explicitly defined array lengths, the parser rejects it instantly instead of allocating unbounded memory. This file contains the primitive types and logic required to seamlessly integrate these bounded checks into standard Rust structs without writing custom parsers from scratch for every new network message struct.


Why Kinetic Needs This

In a decentralized network like Kinetic, we process un-trusted data sent from random peers over the open, unauthenticated internet. A classic attack vector against network nodes in systems programming is a “memory exhaustion attack” (often referred to as a memory bomb or resource exhaustion DoS). If we define a struct with a standard Vec<T>, a malicious actor could deliberately construct a payload with hundreds of millions of empty or tiny elements. Standard Serde arrays try to allocate memory to match the size of the incoming data before completely failing. If an attacker sends a JSON or binary document containing a massive array, Serde might attempt to allocate a multi-gigabyte vector on the heap right away.

Warning

This can easily crash our node by triggering a fatal Out Of Memory (OOM) panic in Rust, causing a catastrophic Denial of Service (DoS) across the network.

We absolutely must defend the node against this vector, as crashing nodes could partition the network. By wrapping list deserialization in explicit bounded caps, we guarantee our parsing logic aborts the exact moment an attacker exceeds the configured threshold. This approach prevents memory allocation attacks at the serialization boundary, ensuring Kinetic remains robust, predictable, and highly resilient under hostile network conditions.


How It Works

The standard deserialize provided by Serde is fully generic, meaning it has no inherent sense of maximum array bounds. To modify this core behavior, we implement our own deserialization logic using the Visitor trait.

Note

See RUST_CONCEPTS.md for an explanation of the serde::de::Visitor trait.

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Lines 9 to 59
}

The custom behavior is driven by the internal BoundedVecVisitor<T> struct. When Serde detects a sequence, the visitor starts by defensively checking the size_hint() provided by the incoming sequence inside the visit_seq method.

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Lines 37 to 41
}

If the underlying data format provides an upfront size (like knowing the array length in advance because of a binary prefix) and that size exceeds our hard max, we immediately return an invalid_length error.

Important

We do not allocate a single byte of heap memory for the vector yet. This is the optimal fast path for rejection, saving CPU cycles.

However, some formats (like streaming JSON) do not provide a size upfront. In those cases, we must allocate a vector to start storing the parsed elements, but we do so defensively and carefully.

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Line 43
}

The capacity allocation is strictly limited to either the size hint or the max allowed limit, preventing us from pre-allocating an excessively large heap buffer. Then we begin to iterate through the sequence using seq.next_element()?, parsing and instantiating one element at a time.

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Lines 46 to 55
}

For every single element popped off the wire and deserialized, we check if our running count has reached the max threshold. If it hits the ceiling, we immediately abort the while loop and return a custom Serde error indicating the boundary was crossed. This drops the partially built vector, freeing the memory instantly, and terminates the connection context via the bubbled-up Error. If the sequence ends before hitting the max, the returned vector is safely bounded and passed back into the struct being populated.


Key Pieces

BoundedVecVisitor<T>

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Lines 9-21
}

This is the core struct driving the defensive parsing strategy. It holds a max value of type usize that determines the absolute ceiling for the array length.

Note

See RUST_CONCEPTS.md for PhantomData (used here to bind the generic type T without actually allocating it).

visit_seq

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Lines 33-59
}

This is a required method on the Visitor trait where the actual bounded iteration logic lives. It is extremely important because this is the exact boundary layer where un-trusted network bytes are converted into Rust heap allocations. It correctly handles both early-abort checks using size hints, and mid-stream aborts if the incoming data stream deliberately hides its true length.

deserialize_max_20

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Lines 66-72
}

A public helper function that sets up a visitor with a hard limit of exactly 20 elements. This is used across Kinetic when we know an array should be relatively small, such as a list of recent cryptographic signatures or short peer discovery lists.

deserialize_max_50

#![allow(unused)]
fn main() {
// See: kinetic-kid/src/bounded.rs — Lines 79-85
}

Similar to the above, but sets a hard limit of exactly 50 elements. Used for slightly larger payload arrays, such as transaction batches or mempool gossips, while still maintaining a strict memory bounds check against runaway allocations.


How This Connects to the Rest of Kinetic

FORWARD DEPENDENCY: You will see these exact helper functions heavily utilized across Kinetic’s core payload structures in other modules. Any struct that needs to be deserialized from a network payload and contains a Vec<T> should be explicitly annotated with #[serde(deserialize_with = "deserialize_max_20")] (or deserialize_max_50). Without this explicit annotation, the struct would silently fall back to Serde’s default, unbounded Vec deserialization, immediately re-opening the memory exhaustion attack vector.

CROSS-CRATE: This specific file acts as a foundational security primitive for the larger kinetic-network crate. When the peer-to-peer layer ingests raw bytes from a TCP/UDP socket, these bounds act as the absolute first line of defense, ensuring that maliciously crafted messages are dropped entirely without affecting the node runtime’s memory footprint or stability.


Quick Reference

ConceptDetail
ProblemNode crashing due to OOM attacks via unbounded JSON/binary arrays in network payloads.
SolutionImplement a custom BoundedVecVisitor that stops parsing the exact moment the length limit is hit.
UsageApply #[serde(deserialize_with = "deserialize_max_20")] to Vec struct fields in Kinetic payloads.
Fail StateReturns a Serde error which bubbles up to the network layer to drop the bad peer’s message immediately.
Key ConceptPhantomData is used to artificially carry type information about T without actually storing the type.

Open Questions / Things to Revisit

  • Currently, we only have hardcoded limits of 20 and 50 exposed as public helpers. Should we implement a Rust macro to generate generic bounded functions (e.g. 100, 500), or is this rigid approach safer for auditing? Having a small, fixed set of limits makes it easier to verify that developers aren’t accidentally allowing arrays of size 10,000 somewhere in the network crate.
  • Is there a way to computationally penalize peers (e.g., lower their reputation score) at the network layer if they trigger the bound limit early in the connection phase to save bandwidth? We need to ensure we don’t accidentally penalize honest peers running outdated client software.
  • Does the serde_json memory footprint still scale poorly with highly nested object structures (like { "a": { "b": { ... } } }) even if the child arrays themselves are strictly bounded? We might need a structural depth check in the future to defend against stack-overflow attacks.
  • If we ever migrate from JSON payloads to a purely binary format like Bincode, do we still need this specific visitor, or does the binary format naturally prevent this type of resource exhaustion? Bincode often allocates based on size prefixes, so this vulnerability might actually be worse in binary if not handled correctly.
  • Should we expose the BoundedVecVisitor directly so that other developers can use it in custom implementations, or keep it strictly encapsulated behind the deserialize_max_* macros?