Skip to content

test(fuzz): add nesting-depth stack-safety target#78

Merged
membphis merged 2 commits into
mainfrom
codex/issue-74-depth-fuzz
May 30, 2026
Merged

test(fuzz): add nesting-depth stack-safety target#78
membphis merged 2 commits into
mainfrom
codex/issue-74-depth-fuzz

Conversation

@membphis
Copy link
Copy Markdown
Collaborator

@membphis membphis commented May 30, 2026

Summary

  • add a non-differential fuzz_depth target for deeply nested arrays/objects
  • pin qjson depth boundaries at the default 1024 and clamped 4096 limits, including FFI parse error propagation
  • exercise accepted boundary inputs through the FFI cursor API and include the new target in CI/docs

Test Plan

  • cargo +nightly fuzz run fuzz_depth -- -max_total_time=60
  • cargo +nightly fuzz run fuzz_parse_eager -- -runs=0
  • cargo test --release
  • cargo clippy --release --all-targets -- -D warnings
  • cargo clippy --manifest-path fuzz/Cargo.toml --bins -- -D warnings
  • git diff --check

Closes #74.

Summary by CodeRabbit

  • Tests

    • Added new fuzz testing for JSON nesting depth validation and edge cases to improve code robustness.
  • Chores

    • Updated CI workflow to run comprehensive fuzz tests.
    • Updated fuzzing documentation with new test guidance.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

Warning

Review limit reached

@membphis, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 44 minutes and 57 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0f59a217-9800-40a2-96be-f9a95879c33e

📥 Commits

Reviewing files that changed from the base of the PR and between f1718f4 and e170c0c.

📒 Files selected for processing (1)
  • fuzz/Cargo.toml
📝 Walkthrough

Walkthrough

This PR introduces fuzz_depth, a new libFuzzer target that validates QJSON's nesting depth boundary behavior at both default (1024) and maximum (4096) limits. The harness generates nested JSON, parses via Rust and FFI APIs, asserts depth error consistency, optionally mutates the input, and exercises cursor traversal on accepted documents.

Changes

Nesting Depth Fuzz Target

Layer / File(s) Summary
Fuzz target registration and CI integration
fuzz/Cargo.toml, .github/workflows/ci.yml
Binary entry for fuzz_depth is added to Cargo, and CI workflow updated to run both fuzz_parse_eager and fuzz_depth with 60-second time limits.
Nesting depth fuzz harness implementation
fuzz/fuzz_targets/fuzz_depth.rs
Harness generates nested JSON (array/object shapes selectable from input), parses via both eager and lazy Rust modes and FFI parser, validates depth boundary (reject N+1, accept N), detects boundary consistency across implementations, optionally mutates JSON structure, and for shallow accepted documents walks the cursor API while asserting byte-span invariants.
Documentation and usage guidance
CONTRIBUTING.md
Fuzzing instructions expanded to document and invoke fuzz_depth with matching time limits in both PR-length and pre-release regression guides.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • #63 — The main infrastructure issue that scaffolds cargo-fuzz setup, introduces the first differential fuzz target (fuzz_parse_eager), and this PR extends that infrastructure with a non-differential depth-focused target.
  • #72, #73 — Related fuzz targets that exercise cursor/walker API paths and traversal logic.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
E2e Test Quality Review ⚠️ Warning The fuzz_depth test missing N-1 boundary assertion. Review comment flags that assert_boundary() tests only N and N+1, not just-below case, leaving this to fuzzing instead of deterministic assertion. Add explicit below_limit test with saturating_sub(1) assertion before at_limit case per the review comment's proposed diff.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a new nesting-depth fuzz target for stack-safety testing, which is the primary objective of this pull request.
Linked Issues check ✅ Passed The PR comprehensively addresses all coding requirements from issue #74: adds fuzz_depth.rs, generates deeply-nested inputs, asserts parse rejections beyond max_depth, covers default (1024) and ceiling (4096) boundaries, and exercises FFI cursor API.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #74: new fuzz target implementation, CI workflow integration, and documentation updates. No unrelated modifications detected.
Security Check ✅ Passed PR contains only fuzzing test code with no production security concerns; security check designed for API Gateway platform production code is not applicable to test harnesses.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issue-74-depth-fuzz

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 81-84: Add an explicit permissions block to the fuzz job to scope
GITHUB_TOKEN to least privilege: in the job that contains the "Run fuzz targets"
step (the fuzz job), add a permissions: block with contents: read and actions:
write at the job level (not the step level) so actions/cache@v4 can restore/save
caches without granting broader default scopes.

In `@fuzz/fuzz_targets/fuzz_depth.rs`:
- Around line 25-35: run_boundary_assertions currently asserts only at N and N+1
for each depth constant; add explicit N-1 checks so the just-below cases are
deterministically covered. For both places that call assert_boundary (the block
using QJSON_DEFAULT_MAX_DEPTH/QJSON_MAX_MAX_DEPTH and the later similar block
around lines 49-54), add assertions like assert_boundary(mode,
QJSON_DEFAULT_MAX_DEPTH - 1, QJSON_DEFAULT_MAX_DEPTH, ShapeMode::Array) and
assert_boundary(mode, QJSON_MAX_MAX_DEPTH - 1, QJSON_MAX_MAX_DEPTH,
ShapeMode::Object) (and the Mixed variant for the u32::MAX case where
applicable), using the same mode iteration and preserving ShapeMode variants so
the N-1 boundary is explicitly tested for each mode.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 99219920-4da7-4114-8bde-f96b02f38d56

📥 Commits

Reviewing files that changed from the base of the PR and between 5c4c3c6 and f1718f4.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • CONTRIBUTING.md
  • fuzz/Cargo.toml
  • fuzz/fuzz_targets/fuzz_depth.rs

Comment thread .github/workflows/ci.yml
Comment on lines +81 to +84
- name: Run fuzz targets
run: |
cargo +nightly fuzz run fuzz_parse_eager -- -max_total_time=60
cargo +nightly fuzz run fuzz_depth -- -max_total_time=60
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the minimum GitHub Actions permissionsblock needed for a workflow that usesactions/checkout@v4, actions/cache@v4, and only runs cargo commands without writing to the repository?

💡 Result:

To run a GitHub Actions workflow that utilizes actions/checkout@v4 and actions/cache@v4 for cargo commands without writing to the repository, you should define the following minimal permissions block: permissions: actions: read contents: read Explanation: - contents: read: Required by actions/checkout@v4 to fetch your repository's code [1][2][3]. - actions: read: Required by actions/cache@v4 to interact with the GitHub Actions cache service [4]. Note that while some historical contexts or specific implementations might suggest write access for caching, the base requirement for simply using the cache service is read access; however, if your workflow is intended to save (write) caches, you must grant actions: write [4]. Since you specified that the workflow only runs cargo commands and does not write to the repository, you should use the block above to follow the principle of least privilege. If your workflow needs to save caches (which is standard practice for performance), use actions: write instead [4]. permissions: actions: write contents: read Setting permissions explicitly is a best practice, as it overrides the default read-write permissions, preventing the GITHUB_TOKEN from having unnecessary access to your repository [5][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section around the fuzz steps
echo "=== ci.yml lines 60-120 ==="
nl -ba .github/workflows/ci.yml | sed -n '60,120p'

# Check for any top-level permissions block
echo
echo "=== permissions blocks in ci.yml (all) ==="
rg -n "^\s*permissions:\s*$|^\s*permissions:\s*$|permissions:" .github/workflows/ci.yml || true

# Also check for other workflow files defining permissions (quick scan)
echo
echo "=== permissions blocks across all workflows ==="
ls -1 .github/workflows || true
rg -n "^\s*permissions:" .github/workflows || true

Repository: api7/lua-qjson

Length of output: 147


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== ci.yml lines 70-95 (context) ==="
sed -n '70,95p' .github/workflows/ci.yml || true

echo
echo "=== permissions blocks in .github/workflows/ci.yml ==="
rg -n "^\s*permissions:" .github/workflows/ci.yml || true

echo
echo "=== permissions blocks across all workflows ==="
rg -n "^\s*permissions:" .github/workflows/*.yml .github/workflows/*.yaml 2>/dev/null || true

Repository: api7/lua-qjson

Length of output: 1021


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Show ci.yml header (first 60 lines) ==="
sed -n '1,60p' .github/workflows/ci.yml || true

echo
echo "=== Find actions/cache usage in ci.yml ==="
rg -n "actions/cache@" .github/workflows/ci.yml || true

echo
echo "=== Show cache step sections around matches ==="
rg -n "actions/cache@" .github/workflows/ci.yml | cut -d: -f1 | while read -r line; do
  start=$((line-10))
  end=$((line+20))
  echo "--- around line $line ($start-$end) ---"
  sed -n "${start},${end}p" .github/workflows/ci.yml
  echo
done

echo
echo "=== Check if ci.yml sets top-level workflow permissions (any other yml keys) ==="
# just in case: look for common patterns
rg -n "permissions:|GITHUB_TOKEN|token:" .github/workflows/ci.yml || true

Repository: api7/lua-qjson

Length of output: 4832


🌐 Web query:

actions/cache@v4 required permissions GITHUB_TOKEN actions: read write contents read

💡 Result:

For actions/cache@v4, the GITHUB_TOKEN needs these job-level permissions (or equivalents): permissions: actions: write contents: read Why: actions/cache (v4) saves/restores caches using GitHub’s cache service APIs; saving requires the actions scope to be write-capable, while restoring only requires reading repository contents. A documented real-world fix for the common “Resource not accessible by integration” error explicitly identifies permissions: { actions: write } as the missing permission for a cache-related action failure, with contents: read also needed to access repository code inputs in the workflow. [1][2] Note: GitHub also supports using workflow/job permissions to control least-privilege access for GITHUB_TOKEN. [2]

Citations:


Scope the workflow token explicitly.

.github/workflows/ci.yml has no permissions: block, so the GITHUB_TOKEN for the fuzz job inherits default (broader) scopes. Since this job uses actions/cache@v4 (restores + saves caches), set least-privilege permissions explicitly:

permissions:
  contents: read
  actions: write
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 51-84: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 81 - 84, Add an explicit permissions
block to the fuzz job to scope GITHUB_TOKEN to least privilege: in the job that
contains the "Run fuzz targets" step (the fuzz job), add a permissions: block
with contents: read and actions: write at the job level (not the step level) so
actions/cache@v4 can restore/save caches without granting broader default
scopes.

Comment on lines +25 to +35
fn run_boundary_assertions() {
for mode in [QJSON_MODE_EAGER, QJSON_MODE_LAZY] {
assert_boundary(mode, 0, QJSON_DEFAULT_MAX_DEPTH, ShapeMode::Array);
assert_boundary(
mode,
QJSON_MAX_MAX_DEPTH,
QJSON_MAX_MAX_DEPTH,
ShapeMode::Object,
);
assert_boundary(mode, u32::MAX, QJSON_MAX_MAX_DEPTH, ShapeMode::Mixed);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Assert the N-1 boundary cases explicitly.

The deterministic boundary check currently pins only N and N+1. That leaves the acceptance criteria's just-below cases for 1024 and 4096 to chance through fuzz input selection instead of asserting them directly.

Proposed change
 fn assert_boundary(
     mode: u32,
     requested_max_depth: u32,
     effective_max_depth: u32,
     shape: ShapeMode,
 ) {
     let opts = Options {
         mode,
         max_depth: requested_max_depth,
     };
 
+    let below_limit_depth = effective_max_depth.saturating_sub(1);
+    let below_limit = nested_json(below_limit_depth, shape, &[0]);
+    assert_parse_ok(&below_limit, &opts);
+    walk_accepted_doc(&below_limit, &opts, shape.path_steps(below_limit_depth));
+
     let at_limit = nested_json(effective_max_depth, shape, &[0]);
     assert_parse_ok(&at_limit, &opts);
     walk_accepted_doc(&at_limit, &opts, shape.path_steps(effective_max_depth));
 
     let over_limit = nested_json(effective_max_depth + 1, shape, &[0]);
     assert_nesting_error(&over_limit, &opts);
 }

Also applies to: 49-54

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@fuzz/fuzz_targets/fuzz_depth.rs` around lines 25 - 35,
run_boundary_assertions currently asserts only at N and N+1 for each depth
constant; add explicit N-1 checks so the just-below cases are deterministically
covered. For both places that call assert_boundary (the block using
QJSON_DEFAULT_MAX_DEPTH/QJSON_MAX_MAX_DEPTH and the later similar block around
lines 49-54), add assertions like assert_boundary(mode, QJSON_DEFAULT_MAX_DEPTH
- 1, QJSON_DEFAULT_MAX_DEPTH, ShapeMode::Array) and assert_boundary(mode,
QJSON_MAX_MAX_DEPTH - 1, QJSON_MAX_MAX_DEPTH, ShapeMode::Object) (and the Mixed
variant for the u32::MAX case where applicable), using the same mode iteration
and preserving ShapeMode variants so the N-1 boundary is explicitly tested for
each mode.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test(fuzz): v3 nesting-depth / stack-safety target

1 participant