From 5ceec108d8bd2c231864de5c8e47e98e267ce778 Mon Sep 17 00:00:00 2001 From: louis Date: Tue, 2 Jun 2026 15:00:40 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(audit):=20add=20wave=202=20=E2=80=94?= =?UTF-8?q?=20VM=20fix=20regression=20audits=20(6=20scripts)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers six GnoVM fixes from the last quarter not yet tested on-chain: - audit_cross_realm_p_arithmetic.sh — 9e56b0c77 (/p/-type arithmetic cross-realm) - audit_preprocess_alloc_caps.sh — c98a2cdca (per-tx allocator cap) - audit_panic_log_dos.sh — 4bb497abe (bounded panic-Log rendering) - audit_map_key_gas.sh — 720af8bcd (gas on ComputeMapKey) - audit_nil_func_call.sh — a7e4c34b0 (nil func call → Gno panic) - audit_type_assert_nil.sh — 6dad8e39d (nil interface type assertion → Gno panic) All scripts use maketx run (no cur realm parameter needed — PR #5669). Node-crash tests (nil_func_call, type_assert_nil) include a liveness check after the rejected tx to confirm the node is still responsive. --- .../audit/audit_cross_realm_p_arithmetic.sh | 119 ++++++++++++++++++ .../samourai-crew/audit/audit_map_key_gas.sh | 79 ++++++++++++ .../audit/audit_nil_func_call.sh | 80 ++++++++++++ .../audit/audit_panic_log_dos.sh | 85 +++++++++++++ .../audit/audit_preprocess_alloc_caps.sh | 53 ++++++++ .../audit/audit_type_assert_nil.sh | 80 ++++++++++++ tests/samourai-crew/run_tests.sh | 6 + 7 files changed, 502 insertions(+) create mode 100755 tests/samourai-crew/audit/audit_cross_realm_p_arithmetic.sh create mode 100755 tests/samourai-crew/audit/audit_map_key_gas.sh create mode 100755 tests/samourai-crew/audit/audit_nil_func_call.sh create mode 100755 tests/samourai-crew/audit/audit_panic_log_dos.sh create mode 100755 tests/samourai-crew/audit/audit_preprocess_alloc_caps.sh create mode 100755 tests/samourai-crew/audit/audit_type_assert_nil.sh diff --git a/tests/samourai-crew/audit/audit_cross_realm_p_arithmetic.sh b/tests/samourai-crew/audit/audit_cross_realm_p_arithmetic.sh new file mode 100755 index 0000000..f95fe1c --- /dev/null +++ b/tests/samourai-crew/audit/audit_cross_realm_p_arithmetic.sh @@ -0,0 +1,119 @@ +#!/bin/sh +# Targets: fix(gnovm): cross-realm /p/-type arithmetic +# Commit: 9e56b0c77 +# Vector: arithmetic on a /p/-defined type passed across realm boundaries could +# produce incorrect results. A Counter defined in /p/ is incremented by realm A, +# passed (via a run script) to realm B which increments it again, then read back +# from realm A. Result must equal 2. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +SUFFIX=$(date +%s) +PKGPATH_P="gno.land/p/${KEY_ADDR}/audit/pcounter${SUFFIX}" +PKGPATH_R="gno.land/r/${KEY_ADDR}/audit/arithmetic${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "9e56b0c77 — Cross-realm /p/-type arithmetic" +echo " Counter pkg : $PKGPATH_P" +echo " Realm : $PKGPATH_R" + +mkdir -p "$TMPDIR/p" "$TMPDIR/r" + +# --- deploy /p/ Counter type --- +cat > "$TMPDIR/p/pcounter.gno" << EOF +package pcounter + +type Counter struct{ Value int } + +func Inc(c *Counter) { c.Value++ } +EOF + +cat > "$TMPDIR/p/gnomod.toml" << EOF +module = "${PKGPATH_P}" +gno = "0.9" +EOF + +echo -n " Deploying /p/ Counter... " +DEPLOY_P=$(echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$PKGPATH_P" -pkgdir "$TMPDIR/p" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$DEPLOY_P" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$DEPLOY_P"; exit 1 +fi + +# --- deploy /r/ realm that holds a Counter --- +cat > "$TMPDIR/r/arithmetic.gno" << EOF +package arithmetic + +import counter "${PKGPATH_P}" + +var c = &counter.Counter{Value: 0} + +func Inc() { + counter.Inc(c) +} + +func GetValue() int { return c.Value } +EOF + +cat > "$TMPDIR/r/gnomod.toml" << EOF +module = "${PKGPATH_R}" +gno = "0.9" +EOF + +echo -n " Deploying realm... " +DEPLOY_R=$(echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$PKGPATH_R" -pkgdir "$TMPDIR/r" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$DEPLOY_R" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$DEPLOY_R"; exit 1 +fi + +# --- increment twice via maketx run --- +cat > "$TMPDIR/inc.gno" << EOF +package main + +import r "${PKGPATH_R}" + +func main() { + r.Inc() + r.Inc() +} +EOF + +echo -n " Incrementing counter twice... " +INC=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/inc.gno" 2>&1) +if echo "$INC" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$INC"; exit 1 +fi + +# --- verify result --- +echo -n " Querying counter value (expect 2)... " +RESULT=$(gnokey query "vm/qeval" \ + -data "${PKGPATH_R}.GetValue()" \ + -remote "$RPC" 2>&1) + +if echo "$RESULT" | grep -qE '\(2 int\)|"2"'; then + echo "✅ PATCHED — cross-realm /p/ arithmetic correct (value=2)" +elif echo "$RESULT" | grep -qE '\(0 int\)|"0"'; then + echo "❌ VULNERABLE — counter still 0 after two increments (cross-realm arithmetic broken)" + exit 1 +else + echo "⚠️ UNKNOWN OUTPUT"; echo "$RESULT"; exit 1 +fi diff --git a/tests/samourai-crew/audit/audit_map_key_gas.sh b/tests/samourai-crew/audit/audit_map_key_gas.sh new file mode 100755 index 0000000..d80ed54 --- /dev/null +++ b/tests/samourai-crew/audit/audit_map_key_gas.sh @@ -0,0 +1,79 @@ +#!/bin/sh +# Targets: fix: consume gas on ComputeMapKey +# Commit: 720af8bcd +# Vector: map key computation (insertions, lookups with complex struct keys) +# did not consume gas proportionally. A map with deeply nested struct keys +# allowed a DoS with unbounded CPU at constant gas cost. +# Test 1: insert 10_000 complex-key entries with low gas → must OOG (PATCHED). +# Test 2: insert 10 simple-key entries with ample gas → must succeed. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "720af8bcd — Gas consumption on ComputeMapKey" + +# --- test 1: many complex struct keys, low gas → expect OOG --- +cat > "$TMPDIR/mapbig.gno" << EOF +package main + +type Key struct { + A, B, C, D, E string +} + +func main() { + m := make(map[Key]int) + for i := 0; i < 10000; i++ { + m[Key{"aaa", "bbb", "ccc", "ddd", "eee"}] = i + } +} +EOF + +echo -n " Test 1: 10_000 complex map keys, gas=100_000 (expect OOG)... " +BIG=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 100000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/mapbig.gno" 2>&1) + +if echo "$BIG" | grep -qiE "out of gas|OOG|gas"; then + echo "OOG (expected)" +elif echo "$BIG" | grep -q "OK!"; then + echo "❌ VULNERABLE — 10_000 complex map insertions succeeded with only 100_000 gas" + exit 1 +else + # Any rejection is acceptable — might be a gas format error or similar + echo "rejected ($(echo "$BIG" | head -1))" +fi + +# --- test 2: 10 simple keys, ample gas → expect success --- +cat > "$TMPDIR/mapsmall.gno" << EOF +package main + +func main() { + m := make(map[string]int) + for i := 0; i < 10; i++ { + m["key"] = i + } +} +EOF + +echo -n " Test 2: 10 simple map keys, gas=1_000_000 (expect OK)... " +SMALL=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 1000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/mapsmall.gno" 2>&1) + +if echo "$SMALL" | grep -q "OK!"; then + echo "✅ PATCHED — complex map keys metered (OOG on large), simple map works normally" +else + echo "FAILED (simple map rejected unexpectedly)" + echo "$SMALL" + exit 1 +fi diff --git a/tests/samourai-crew/audit/audit_nil_func_call.sh b/tests/samourai-crew/audit/audit_nil_func_call.sh new file mode 100755 index 0000000..0e374d4 --- /dev/null +++ b/tests/samourai-crew/audit/audit_nil_func_call.sh @@ -0,0 +1,80 @@ +#!/bin/sh +# Targets: fix(gnovm): generate proper Gno panic for nil function calls +# Commit: a7e4c34b0 +# Vector: calling a nil function variable (var f func(); f()) caused a node crash +# (Go-level panic escaping the VM) instead of a proper Gno panic. After the fix, +# the VM catches the nil call and returns a transaction-level Gno panic, leaving +# the node intact. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +SUFFIX=$(date +%s) +PKGPATH="gno.land/r/${KEY_ADDR}/audit/nilfunc${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "a7e4c34b0 — Nil function call → proper Gno panic (not node crash)" +echo " Package: $PKGPATH" + +# --- deploy realm with a nil function variable --- +cat > "$TMPDIR/nilfunc.gno" << EOF +package nilfunc + +var f func() + +func CallNil() { + f() +} +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying realm... " +DEPLOY=$(echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$PKGPATH" -pkgdir "$TMPDIR" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$DEPLOY" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$DEPLOY"; exit 1 +fi + +# --- call the nil function --- +cat > "$TMPDIR/callnil.gno" << EOF +package main + +import v "${PKGPATH}" + +func main() { v.CallNil() } +EOF + +echo -n " Calling nil function... " +CALL=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/callnil.gno" 2>&1) +echo "$(echo "$CALL" | grep -oE 'OK!|error|panic|nil' | head -1)" + +# TX must be rejected (nil call should panic) +if echo "$CALL" | grep -q "OK!"; then + echo "❌ VULNERABLE — nil function call succeeded (unexpected)" + exit 1 +fi + +# --- verify node is still alive --- +echo -n " Checking node liveness... " +if gnokey query "bank/balances/${KEY_ADDR}" -remote "$RPC" > /dev/null 2>&1; then + echo "✅ PATCHED — nil function call rejected gracefully, node still responsive" +else + echo "❌ VULNERABLE — node unresponsive after nil function call (crash)" + exit 1 +fi diff --git a/tests/samourai-crew/audit/audit_panic_log_dos.sh b/tests/samourai-crew/audit/audit_panic_log_dos.sh new file mode 100755 index 0000000..a8f485d --- /dev/null +++ b/tests/samourai-crew/audit/audit_panic_log_dos.sh @@ -0,0 +1,85 @@ +#!/bin/sh +# Targets: fix(vm): bound panic-Log rendering to prevent unmetered long running txs +# Commit: 4bb497abe +# Vector: a panic with a very long message triggered unmetered rendering in the VM, +# allowing a DoS via transactions that would block the node regardless of gas. +# After the fix, panic message rendering is bounded by gas. +# Test: send a tx that panics with a large message using a bounded timeout; +# if the node hangs past 30s the rendering is unmetered. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +SUFFIX=$(date +%s) +PKGPATH="gno.land/r/${KEY_ADDR}/audit/panicdos${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "4bb497abe — Panic-Log DoS (unmetered panic message rendering)" +echo " Package: $PKGPATH" + +# --- deploy realm with a large-panic function --- +cat > "$TMPDIR/panicdos.gno" << EOF +package panicdos + +import "strings" + +func Boom() { + panic(strings.Repeat("A", 100_000)) +} +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying realm... " +DEPLOY=$(echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$PKGPATH" -pkgdir "$TMPDIR" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$DEPLOY" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$DEPLOY"; exit 1 +fi + +# --- call Boom() with a 30s timeout --- +# PATCHED: call returns quickly (OOG or panic error) within the timeout. +# VULNERABLE: call hangs past 30s (unmetered rendering blocks the node). +cat > "$TMPDIR/boom.gno" << EOF +package main + +import v "${PKGPATH}" + +func main() { v.Boom() } +EOF + +echo -n " Calling Boom() with 30s timeout... " +CALL=$(timeout 30 sh -c "echo '$PASSWORD' | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 2000000 \ + -broadcast -chainid \"$CHAINID\" -remote \"$RPC\" \ + -insecure-password-stdin \ + -home \"$GNOKEY_HOME\" \ + \"$KEY\" \"$TMPDIR/boom.gno\"" 2>&1) +TIMEOUT_STATUS=$? + +if [ "$TIMEOUT_STATUS" -eq 124 ]; then + echo "❌ VULNERABLE — call hung past 30s (unmetered panic rendering)" + exit 1 +fi + +# Call returned within timeout — should be a tx rejection (panic or OOG) +echo "returned ($(echo "$CALL" | grep -oE 'OK!|error|panic' | head -1))" + +# Verify node is still responsive +echo -n " Checking node liveness... " +if gnokey query "bank/balances/${KEY_ADDR}" -remote "$RPC" > /dev/null 2>&1; then + echo "✅ PATCHED — Boom() returned within 30s and node is still responsive" +else + echo "❌ Node unresponsive after panic tx" + exit 1 +fi diff --git a/tests/samourai-crew/audit/audit_preprocess_alloc_caps.sh b/tests/samourai-crew/audit/audit_preprocess_alloc_caps.sh new file mode 100755 index 0000000..5f04db7 --- /dev/null +++ b/tests/samourai-crew/audit/audit_preprocess_alloc_caps.sh @@ -0,0 +1,53 @@ +#!/bin/sh +# Targets: fix(gnolang): preprocess hardening — per-tx allocator + caps +# Commit: c98a2cdca +# Vector: without a per-tx allocator cap, a single transaction could allocate +# an arbitrary amount of memory before GC kicked in, bypassing gas limits. +# After the fix, allocation beyond the per-tx cap is rejected regardless of +# gas-wanted. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "c98a2cdca — Per-tx allocator cap" + +# --- attempt massive single-expression allocation via maketx run --- +# 100MB in one make() — should hit the per-tx allocator cap before gas runs out. +cat > "$TMPDIR/alloc.gno" << EOF +package main + +func main() { + _ = make([]byte, 100_000_000) +} +EOF + +echo -n " Allocating 100MB in one tx (gas-wanted=50_000_000)... " +ALLOC=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 50000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/alloc.gno" 2>&1) + +# PATCHED: rejected — allocator cap, OOG, or memory limit error +if echo "$ALLOC" | grep -qiE "out of gas|alloc|memory|limit|exceeded|OOG"; then + echo "✅ PATCHED — massive allocation rejected by per-tx allocator cap" + exit 0 +fi + +if echo "$ALLOC" | grep -q "OK!"; then + echo "❌ VULNERABLE — 100MB allocation succeeded (no per-tx cap)" + exit 1 +fi + +# Transaction rejected for an unexpected reason — check if it's still a cap-related message +echo "REJECTED (unexpected error)" +echo "$ALLOC" +# A non-OK rejection that isn't clearly memory-related is ambiguous. +# Treat as unknown rather than failing the audit — the node did reject it. +echo "⚠️ TX rejected but reason unclear — manual review needed" +exit 1 diff --git a/tests/samourai-crew/audit/audit_type_assert_nil.sh b/tests/samourai-crew/audit/audit_type_assert_nil.sh new file mode 100755 index 0000000..2548ec3 --- /dev/null +++ b/tests/samourai-crew/audit/audit_type_assert_nil.sh @@ -0,0 +1,80 @@ +#!/bin/sh +# Targets: fix(gnovm): add nil checks for unsafe .V type assertions +# Commit: 6dad8e39d +# Vector: an unsafe type assertion (x.(*T)) on a nil interface value caused a +# Go-level panic escaping the VM instead of a proper Gno panic. After the fix, +# the VM catches the nil interface and returns a transaction-level Gno panic, +# leaving the node intact. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +SUFFIX=$(date +%s) +PKGPATH="gno.land/r/${KEY_ADDR}/audit/typeassert${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "6dad8e39d — Nil interface type assertion → proper Gno panic (not node crash)" +echo " Package: $PKGPATH" + +# --- deploy realm with nil interface type assertion --- +cat > "$TMPDIR/typeassert.gno" << EOF +package typeassert + +var i interface{} = nil + +func Assert() { + _ = i.(*int) +} +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying realm... " +DEPLOY=$(echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$PKGPATH" -pkgdir "$TMPDIR" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$DEPLOY" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$DEPLOY"; exit 1 +fi + +# --- trigger the nil assertion --- +cat > "$TMPDIR/assert.gno" << EOF +package main + +import v "${PKGPATH}" + +func main() { v.Assert() } +EOF + +echo -n " Calling Assert() on nil interface... " +CALL=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/assert.gno" 2>&1) +echo "$(echo "$CALL" | grep -oE 'OK!|error|panic|nil' | head -1)" + +# TX must be rejected (nil type assertion should panic) +if echo "$CALL" | grep -q "OK!"; then + echo "❌ VULNERABLE — nil type assertion succeeded (unexpected)" + exit 1 +fi + +# --- verify node is still alive --- +echo -n " Checking node liveness... " +if gnokey query "bank/balances/${KEY_ADDR}" -remote "$RPC" > /dev/null 2>&1; then + echo "✅ PATCHED — nil type assertion rejected gracefully, node still responsive" +else + echo "❌ VULNERABLE — node unresponsive after nil type assertion (crash)" + exit 1 +fi diff --git a/tests/samourai-crew/run_tests.sh b/tests/samourai-crew/run_tests.sh index beda8cc..3a735ea 100755 --- a/tests/samourai-crew/run_tests.sh +++ b/tests/samourai-crew/run_tests.sh @@ -175,6 +175,12 @@ if [ "$MODE" = "one-shot" ] || [ "$MODE" = "all" ]; then run_test "audit_var_init_order" /tests/audit/audit_var_init_order.sh run_test "audit_cross_realm_recover" /tests/audit/audit_cross_realm_recover.sh \ "broader pattern not yet fixed, see f87249327" + run_test "audit_cross_realm_p_arithmetic" /tests/audit/audit_cross_realm_p_arithmetic.sh + run_test "audit_preprocess_alloc_caps" /tests/audit/audit_preprocess_alloc_caps.sh + run_test "audit_panic_log_dos" /tests/audit/audit_panic_log_dos.sh + run_test "audit_map_key_gas" /tests/audit/audit_map_key_gas.sh + run_test "audit_nil_func_call" /tests/audit/audit_nil_func_call.sh + run_test "audit_type_assert_nil" /tests/audit/audit_type_assert_nil.sh echo "" echo "=== E2E Tests (one-shot) ===" From 51a81df18cca4b93417e6caa7c3d72261e9a37b1 Mon Sep 17 00:00:00 2001 From: louis Date: Tue, 2 Jun 2026 15:06:54 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix(audit):=20rework=20preprocess=5Falloc?= =?UTF-8?q?=5Fcaps=20=E2=80=94=20test=20embed-chain=20depth=20cap=20not=20?= =?UTF-8?q?runtime=20alloc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix (c98a2cdca) targets the preprocessor (MsgAddPackage type-checking phase), not runtime allocation. make([]byte, 100M) tests the wrong code path. New approach: deploy a 10-level struct embed chain (> cap of 8) — preprocessor must reject it. 6-level embed (< cap) must still deploy successfully. --- .../audit/audit_preprocess_alloc_caps.sh | 127 +++++++++++++----- 1 file changed, 97 insertions(+), 30 deletions(-) diff --git a/tests/samourai-crew/audit/audit_preprocess_alloc_caps.sh b/tests/samourai-crew/audit/audit_preprocess_alloc_caps.sh index 5f04db7..97d1a9a 100755 --- a/tests/samourai-crew/audit/audit_preprocess_alloc_caps.sh +++ b/tests/samourai-crew/audit/audit_preprocess_alloc_caps.sh @@ -1,53 +1,120 @@ #!/bin/sh # Targets: fix(gnolang): preprocess hardening — per-tx allocator + caps # Commit: c98a2cdca -# Vector: without a per-tx allocator cap, a single transaction could allocate -# an arbitrary amount of memory before GC kicked in, bypassing gas limits. -# After the fix, allocation beyond the per-tx cap is rejected regardless of -# gas-wanted. +# Vector: during MsgAddPackage/MsgCall/MsgRun, the preprocessor (type-checking +# phase) ran with no allocator and no depth caps. Adversarial input with deeply +# nested composite types could spin the preprocessor for seconds and allocate +# hundreds of MB before any gas was charged — a pure DoS at tx submission time. +# Fix adds: composite-type nesting depth cap (8), embed-chain depth cap (8), +# MaxInterfaceMethods (128), MaxStructFields (128), per-tx preprocess allocator. +# +# Test 1: deploy a package with 10 levels of struct embedding (> cap of 8) +# → MsgAddPackage must be rejected quickly (PATCHED) +# Test 2: deploy a package with 6 levels of struct embedding (< cap of 8) +# → MsgAddPackage must succeed (sanity check) SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # shellcheck source=common.sh . "$SCRIPT_DIR/common.sh" +SUFFIX=$(date +%s) +PKGPATH_DEEP="gno.land/p/${KEY_ADDR}/audit/deepembed${SUFFIX}" +PKGPATH_SHALLOW="gno.land/p/${KEY_ADDR}/audit/shallowembed${SUFFIX}" TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT -echo "c98a2cdca — Per-tx allocator cap" +echo "c98a2cdca — Preprocess depth caps (embed chain, composite type nesting)" -# --- attempt massive single-expression allocation via maketx run --- -# 100MB in one make() — should hit the per-tx allocator cap before gas runs out. -cat > "$TMPDIR/alloc.gno" << EOF -package main +mkdir -p "$TMPDIR/deep" "$TMPDIR/shallow" -func main() { - _ = make([]byte, 100_000_000) -} +# --- test 1: 10 levels of struct embedding — exceeds cap of 8 --- +cat > "$TMPDIR/deep/deepembed.gno" << EOF +package deepembed + +type E0 struct{} +type E1 struct{ E0 } +type E2 struct{ E1 } +type E3 struct{ E2 } +type E4 struct{ E3 } +type E5 struct{ E4 } +type E6 struct{ E5 } +type E7 struct{ E6 } +type E8 struct{ E7 } +type E9 struct{ E8 } +type Deep struct{ E9 } +EOF + +cat > "$TMPDIR/deep/gnomod.toml" << EOF +module = "${PKGPATH_DEEP}" +gno = "0.9" EOF -echo -n " Allocating 100MB in one tx (gas-wanted=50_000_000)... " -ALLOC=$(echo "$PASSWORD" | gnokey maketx run \ - -gas-fee 1000000ugnot -gas-wanted 50000000 \ +echo -n " Test 1: deploying 10-level embed chain (expect rejection)... " +DEEP=$(timeout 30 sh -c "echo '$PASSWORD' | gnokey maketx addpkg \ + -pkgpath '$PKGPATH_DEEP' -pkgdir '$TMPDIR/deep' \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid '$CHAINID' -remote '$RPC' \ + -insecure-password-stdin \ + -home '$GNOKEY_HOME' \ + '$KEY'" 2>&1) +TIMEOUT_STATUS=$? + +if [ "$TIMEOUT_STATUS" -eq 124 ]; then + echo "❌ VULNERABLE — preprocessor hung past 30s (unmetered deep embed)" + exit 1 +fi + +if echo "$DEEP" | grep -q "OK!"; then + echo "ACCEPTED" + echo "⚠️ 10-level embed deployed successfully — cap may not be active on this network" + echo " (commit c98a2cdca may not be in the running binary)" + # Don't exit 1 — mark as inconclusive, not a clear vulnerability + DEEP_RESULT="inconclusive" +else + echo "rejected ($(echo "$DEEP" | grep -oiE 'depth|cap|exceed|limit|embed|error' | head -1))" + DEEP_RESULT="patched" +fi + +# --- test 2: 6 levels of struct embedding — within cap --- +cat > "$TMPDIR/shallow/shallowembed.gno" << EOF +package shallowembed + +type S0 struct{} +type S1 struct{ S0 } +type S2 struct{ S1 } +type S3 struct{ S2 } +type S4 struct{ S3 } +type S5 struct{ S4 } +type Shallow struct{ S5 } +EOF + +cat > "$TMPDIR/shallow/gnomod.toml" << EOF +module = "${PKGPATH_SHALLOW}" +gno = "0.9" +EOF + +echo -n " Test 2: deploying 6-level embed chain (expect success)... " +SHALLOW=$(echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$PKGPATH_SHALLOW" -pkgdir "$TMPDIR/shallow" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ -broadcast -chainid "$CHAINID" -remote "$RPC" \ -insecure-password-stdin \ -home "$GNOKEY_HOME" \ - "$KEY" "$TMPDIR/alloc.gno" 2>&1) + "$KEY" 2>&1) -# PATCHED: rejected — allocator cap, OOG, or memory limit error -if echo "$ALLOC" | grep -qiE "out of gas|alloc|memory|limit|exceeded|OOG"; then - echo "✅ PATCHED — massive allocation rejected by per-tx allocator cap" - exit 0 +if echo "$SHALLOW" | grep -q "OK!"; then + echo "OK" +else + echo "FAILED (6-level embed rejected — unexpected)" + echo "$SHALLOW" + exit 1 fi -if echo "$ALLOC" | grep -q "OK!"; then - echo "❌ VULNERABLE — 100MB allocation succeeded (no per-tx cap)" +# --- verdict --- +if [ "$DEEP_RESULT" = "patched" ]; then + echo "✅ PATCHED — deep embed rejected, shallow embed accepted (preprocessor caps active)" +else + echo "⚠️ INCONCLUSIVE — deep embed was accepted; commit c98a2cdca may not be in this binary" + echo " Both tests ran without hanging — preprocessor is at least not DoS-vulnerable" exit 1 fi - -# Transaction rejected for an unexpected reason — check if it's still a cap-related message -echo "REJECTED (unexpected error)" -echo "$ALLOC" -# A non-OK rejection that isn't clearly memory-related is ambiguous. -# Treat as unknown rather than failing the audit — the node did reject it. -echo "⚠️ TX rejected but reason unclear — manual review needed" -exit 1