Security
AI assistant
Build this with Claude Code, Cursor or Copilot
Copy a Tessero-tuned prompt for Security, grounded in 16 real API signatures , into your IDE's AI. No chatbot, just exact context.
Threat model
Tessero is a computation library. It opens no sockets, spawns no processes
(apart from tessero build, which runs make), reads files only when you call
Npy::load/loadZ, and evaluates no code from data. The relevant risks are:
- Resource exhaustion from attacker-controlled sizes: huge arrays, huge LP/MDP models, many small requests that each allocate.
- Memory safety in the C kernel and the extension: out-of-bounds reads or writes from malformed input.
- FFI exposure when
ffi.enable=1is used in web SAPIs. - Unsafe deserialisation of arrays from caches, queues or files.
1. Resource exhaustion
- Always set a native memory budget in web-facing processes
(
tessero.memory_budget,TESSERO_MEMORY_BUDGET). Without one, a single request can allocate until the OOM killer ends the worker. Outside the CLI the FFI package now starts with a default budget of max(256 MiB,memory_limit) when none is set (memory_limit = -1keeps it unlimited). Opt out withTESSERO_MEMORY_BUDGET=0(or-1) in the process environment, orTessero::setMemoryBudget(0)for the current request. - Validate sizes before building arrays.
NumericArray(maxElements: …)rejects oversized input before any allocation. Its check is linear in the input size and allocates nothing. WithoutmaxElementsit usesconfig('tessero.max_elements')(TESSERO_MAX_ELEMENTS, default 1,000,000), and it refuses input nested deeper than 32 levels. - Bound solver work for user-supplied models:
maxiter,nodeLimit,maxIter, and cap the number of states, constraints and variables at the API boundary. A dense LP tableau grows as m·(n+m). Npy::loadreads the whole file into PHP memory (somemory_limitapplies), then checks the header's declared shape against the data length before building the array. Check file sizes before loading untrusted files.- File readers treat their input as hostile (
src/Io): compressed data is inflated only up to$maxBytes(default 256 MiB per.npzarchive or MAT/IDL file, checked from the zip directory before decompressing; at most 4096 archive members), header counts are checked against the bytes actually present before any loop or allocation, and the pure-PHP readers (MAT, netCDF, Matrix Market) decode at most$maxElementsvalues (default 64 Mi).readsavrefuses record chains that do not move forward.hb_readandSparseNpz::loadvalidate the CSR/CSC structure before returning it. Raise the limits per call only for files you trust; never take them from request input.
2. Memory safety
- Every entry point validates shapes, dtypes and index ranges before C code runs. Kernels receive only well-formed metadata.
- Solvers validate their inputs: LP/MILP reject NaN/INF data; MDP input must
be finite and well-formed CSR (
indptrmonotone, ending atnnz; indices in range), with rows summing to 1 unless the caller passesvalidate: false. unserialize(),fromBytes()and.npyloading check, before any allocation, that every dimension is non-negative, that the element count cannot overflow, and that the byte length matches dtype × shape. A crafted payload raises an exception; it can never describe more memory than it carries.- The C test suite, the extension's phpt suite and the parity suite run under AddressSanitizer and UndefinedBehaviorSanitizer in CI. The kernel's integer arithmetic wraps by definition (NumPy semantics) and is never undefined behaviour.
- Coverage-guided fuzzing (libFuzzer, in CI) covers every parser of untrusted
text or bytes: the slice parser, the
.npyheader parser, memory-map argument planning and the JSON writer. NumPy-generated slice fixtures check the results as well. - The [memory-safety review(https://github.com/DMJ-CV-91913/tessero/blob/main/docs/project/memory-safety-review.md) lists the safeguards, the defects fixed and the residual risks.
3. FFI exposure
ffi.enable=1 in a web SAPI lets any PHP code in the application call
arbitrary C functions, which turns any code-injection bug into native code
execution. Use:
- the extension (no FFI needed), or
ffi.enable=preloadwith Tessero's preload script. Only preloaded code can use FFI, and the only libraries bound are libtessero and BLAS. The script writes its temporary header with an exclusive, private file (tempnam, 0600) and deletes it after loading, so another local user cannot redirect which library is loaded. The FFI package also refuses a libtessero binary whose version differs from the PHP code.
Keep ffi.enable=1 to the CLI.
Under ffi.enable=preload, the preloaded Tessero classes are the gate: any
request code that can call Tessero\Native\Library::ffi() or
Tessero\Native\Blas::ffi() gets the bound libraries' full C surface. These
are @internal; PHP cannot make them unreachable (Reflection reaches private
members as well), so treat code injection in the application as native code
execution even with preload. The free-any-address helpers that existed
(Buffer::wrap(), Buffer::adopt(), Library::ptr()) were removed: a
Buffer can now only be created over memory Tessero allocated itself.
Which libraries are loaded
TESSERO_LIBandTESSERO_BLASare read from the process environment only (getenv(..., true): never from SAPI or request variables). Each must be an absolute path to an existing file, and neither the file nor its directory may be group- or world-writable (chmod go-w); otherwise loading fails with an explicitLibraryUnavailableerror instead of falling back.- A relative or bare library name would be resolved against the working
directory, which under php-cgi is the web root. On Windows a bare
libopenblas.dllis therefore searched only in the CLI; web SAPIs load BLAS fromTESSERO_BLAS(absolute) or the absolute system paths. - The per-user SciPy wheel (
$HOME/.local/lib/python3*/site-packages/scipy.libs) is searched only in the CLI: under a web SAPI$HOMEis often writable by the web server. - libtessero exports its
tsr_*symbols globally. If another extension in the same process links a different libtessero (for example a system copy in/usr/local/lib), the dynamic linker can bind the FFI-loaded library's internal calls to that copy, so allocations are counted, and budgets enforced, by the other copy. Do not load two libtessero builds into one PHP process.
4. Memory-mapped files
NDArray::memmap(),openMemmap()andload($path, $mode), on both backends, resolve paths throughopen_basedirand refuse stream wrappers.Npy::save/load/saveZ/loadZandSparseNpz::save/loadrefuse stream wrappers too (http://,phar://,php://filter,data:, ...). Paths must never come from request input; if a user chooses a file, map their choice to a path through an allow-list, as you would forfopen().- Mode
'w+'refuses a path that is a symbolic link. The check and the kernel'sopen()are separate steps, so in a directory other accounts can write a link planted in between still wins: create maps only in directories owned by the application. - Mode
'r'maps pages read-only in addition to the library's own checks (every write path, includingout:arguments andshuffle), so a read-only array cannot be written even through a bug. - If a mapped file is truncated by another process, touching the missing pages raises SIGBUS and kills the worker. Map only files your application controls, and never map files that untrusted parties can modify.
- Mapped bytes are outside
memory_budget. Cap file sizes you accept for mapping, as you would cap request sizes.
5. Deserialisation
serialize()d arrays contain only dtype, shape and raw bytes, and__unserializevalidates them. They are safe to store in caches and queues you control. As with anyunserialize(), never unserialise data from untrusted sources: other classes' gadgets are the risk, not Tessero's.- The
binaryEloquent cast stores{dtype, shape, b64}JSON and validates the decoded length. .npyheaders are parsed with a strict grammar. They are never evaluated, and object/pickle dtypes are rejected.
Supply chain
- Prebuilt binaries are produced by the CI
nativejob from tagged sources. Release notes list SHA-256 checksums (see [Releasing(https://github.com/DMJ-CV-91913/tessero/blob/main/docs/project/releasing.md)). - To avoid prebuilt binaries entirely, build
libtesserofrom source (tessero build) and setTESSERO_LIB, or use the extension built from source. - The ziggurat tables are NumPy's (BSD-3-Clause). There are no other third-party sources in the kernel.
Reporting a vulnerability
Follow SECURITY.md at the repository root: report privately, never in a
public issue.