Files
skills/docs/foundation/bitpacker/bitpacker-validation/src/safety/safety-checks.md
2026-05-29 08:20:56 +01:00

4.9 KiB

Safety Checks

Purpose

Production-time defensive checks against pathological inputs to the bitpacker — schemas with infinite recursion, arrays with millions of elements, objects with thousands of keys, floats that are NaN or Infinity. The SafetyChecker class wraps every public-boundary pack/unpack call in a lightweight check that fails fast on the limits it was constructed with, before the bitpacker hot path consumes excessive memory or compute. Safety is opt-in (each SafetyEnabledSchema carries an embedded _safety block) and selective (the SafetyPerformanceOptions lets a consumer disable individual checks for benchmarked-acceptable workloads).

The module also ships the RecursionGuard factory that the BitPacker.unpack path uses to enforce maxRecursionDepth. Without the guard, an attacker-controlled schema with a self-referential _ref could blow the JS stack via O(n) recursion on a large input. The guard's withGuard(fn) form auto-cleans up on exception so a deeply-nested unpack failure doesn't leave the depth counter wedged.

Features

  • SafetyChecker — main class, constructed with SafetyLimits + optional SafetyPerformanceOptions
  • createRecursionGuard(checker) — factory for a RecursionGuard with enter/exit/withGuard
  • validatePackedSize(bytes, checker) / estimatePackedSize(schema, data) — pre-flight size checks
  • SafetyError — thrown on any limit breach, with a stable code field
  • Six published codes: BUFFER_OVERFLOW, RECURSION_OVERFLOW, ARRAY_TOO_LARGE, OBJECT_TOO_LARGE, INVALID_FLOAT, CUSTOM_VALIDATION_FAILED
  • DEFAULT_SAFETY_LIMITS — { maxBufferSize: 10MB, maxRecursionDepth: 100, maxArrayLength: 10_000, maxObjectKeys: 1_000, allowInfiniteFloats: false, allowNaNFloats: false }

Usage: Basic Example

import { SafetyChecker, DEFAULT_SAFETY_LIMITS } from "@suaveplan/bitpacker-validation";

const checker = new SafetyChecker(DEFAULT_SAFETY_LIMITS);
checker.checkArray(new Array(5000)); // ok
checker.checkArray(new Array(50_000)); // throws SafetyError(ARRAY_TOO_LARGE)

Usage: Advanced Example

import {
    SafetyChecker,
    SafetyError,
    createRecursionGuard,
} from "@suaveplan/bitpacker-validation";

const checker = new SafetyChecker({
    maxBufferSize: 1024 * 1024,
    maxRecursionDepth: 32,
    maxArrayLength: 1000,
    maxObjectKeys: 100,
    allowInfiniteFloats: false,
    allowNaNFloats: false,
});

const guard = createRecursionGuard(checker);

function unpackSafely(schema: unknown, data: unknown): unknown {
    return guard.withGuard(() => {
        try {
            return performUnpack(schema, data);
        } catch (err) {
            if (err instanceof SafetyError && err.code === "RECURSION_OVERFLOW") {
                // …emit metric, fall back to a depth-limited representation…
            }
            throw err;
        }
    });
}

API Reference

class SafetyChecker

Constructor: new SafetyChecker(limits?, performance?). limits defaults to DEFAULT_SAFETY_LIMITS. performance defaults to {} (every check enabled).

checkArray(arr: unknown[]), checkObject(obj: object), checkBuffer(bytes: Uint8Array), checkFloat(value: number)

Each method throws SafetyError with the corresponding code on limit breach.

getMetrics(): SafetyMetrics

Returns the per-violation counters and the timestamp of the last violation.

function createRecursionGuard(checker: SafetyChecker): RecursionGuard

Returns a guard with enter(), exit(), and withGuard(fn). withGuard auto-cleans on exception.

class SafetyError extends Error

code: string. Six published codes (see Features).

function estimatePackedSize(schema, data) / validatePackedSize(bytes, checker)

Pre-flight estimation and post-pack validation. The estimator is cheap; use it before checker.checkBuffer to short-circuit clearly-oversize packs.

Implementation Notes

  • Custom validators run after built-ins. A SchemaSafetyLimits.customValidators entry resolves to a registered CustomSafetyValidator and runs after the built-in checks pass. A custom validator may throw SafetyError(CUSTOM_VALIDATION_FAILED) to surface its own constraint name.
  • RecursionGuard.withGuard is exception-safe. The implementation uses try { … } finally { exit() } so the depth counter always decrements, even if the wrapped function throws. Calling enter without a matching exit would leak depth and cause spurious RECURSION_OVERFLOW later.
  • checkFloat is opt-in via allowInfiniteFloats / allowNaNFloats. Both default to false because most game and protocol schemas have no legitimate use for Infinity or NaN. Consumers that legitimately need them set the flags to true and accept the risk.
  • Metrics are aggregate, not per-call. SafetyMetrics accumulates violation counts across the checker's lifetime. Callers who want per-call metrics must reset the checker between calls or accumulate at their own layer.