anticsdecoded|Portfolio
SEC|2026
/audits / ONTOLOGY.go
cat /audits/ONTOLOGY.go
NODE: PRIVATE

L1 smart-contract platform (NeoVM)

// engagement
protocolOntology
categoryL1 smart-contract platform (NeoVM)
kindprivate · Private
date2026-07
duration1 week
languageGo
chainsOntology
nSLOC1,480
// findingsNODE: 1 FND
critical0
high1
medium0
low0
info0
gas0
// summary

Private disclosure to the Ontology team (research@ont.io) for the ontology Go full node, covering an uncontrolled-recursion crash in NeoVM native-argument serialization: a circular-reference detector that inspects only each container's first child lets a later-child cycle slip through and drive BuildParamToNative into unbounded recursion, reachable pre-authentication through the sendrawtransaction pre-execution path.

// methodology
  • Manual review
  • PoC development
  • Live-node testing
// scope
pathnsloc
vm/neovm/types/neovm_value.go560
smartcontract/service/neovm/native.go90
smartcontract/service/neovm/neovm_service.go320
http/jsonrpc/interfaces.go300
txnpool/proc/txnpool_actor.go210
total1,480
// findings
H-01highfixed

Self-referential native-invoke argument slips past the first-child-only cycle detector and crashes a node with a fatal stack overflow

`CircularRefAndDepthDetection` records each container's backing pointer but `return`s from inside its loop after examining only child index 0, so a self-referential array shaped as `[false, self]` — cycle hidden in a later child — is reported cycle-free. `BuildParamToNative`, the native-invoke argument serializer (which, unlike `System.Runtime.Serialize`, has no sink-size cap), then re-enters the public detector per child, misses the cycle every time, and recurses into the self-reference until the Go runtime aborts with `fatal error: stack overflow`. It runs during transaction pre-execution on the unauthenticated `sendrawtransaction` handler — both the explicit pre-exec branch and the default txpool route, before signature, balance, or gas checks — so a 222-byte transaction from a zero-balance signer deterministically kills any reachable RPC node (exit code 2); being a Go runtime throw, the handler's `recover()` cannot contain it, and every validator that executes the tx crashes identically. Fixed by restoring whole-graph traversal (every child, one shared visited-set, across array/struct/map) and adding a MAX_BYTEARRAY_SIZE sink cap to the native serializer.