Nim Fuzzing logo

Nim Fuzzing

Community
planetis-m
nim-fuzzing

Set up and run libFuzzer-based fuzz targets for Nim code, including harness wiring, compilation flags, corpus management, structure-aware mutators, and crash triage. Use when adding fuzzing to a Nim project, building a fuzz harness for a parser/protocol/format handler, or reproducing and minimizing a fuzzer-found crash.

Overview

Publisherplanetis-m
Repositoryskills
Skill namenim-fuzzing
Stars
66
Forks
7
Bundled files
3
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 3 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by planetis-m on GitHub. Read the source before you install it.

Installation

Install the Nim Fuzzing AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/planetis-m/skills/tree/master/nim-fuzzing
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/planetis-m/skills.git /tmp/skills
mkdir -p .claude/skills
cp -r /tmp/skills/nim-fuzzing .claude/skills/nim-fuzzing
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Nim Fuzzing in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Nim Fuzzing on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Nim Fuzzing is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Nim Fuzzing

This skill covers wiring code to libFuzzer — harness structure, compilation, corpus, mutators, and crash triage. It does NOT cover writing the code under test.

Rules

Harness structure: the two required procs

Every Nim fuzz target needs these two exported procs:

nim
proc initialize(): cint {.cdecl, exportc: "LLVMFuzzerInitialize".} =
  {.emit: "N_CDECL(void, NimMain)(void); NimMain();".}

proc testOneInput(data: ptr UncheckedArray[byte], len: int): cint {.
    cdecl, exportc: "LLVMFuzzerTestOneInput", raises: [].} =
  result = 0
  # exercise code under test with data[0..<len]

initialize is called once at startup. It bootstraps Nim's runtime via NimMain(). Without it the target will segfault before any inputs are tested.

testOneInput is called by libFuzzer with a raw byte buffer. It must return 0. Use raises: [] — any uncaught exception will crash the fuzzer process, which is treated as a finding.

Minimum working example

nim
proc fuzzMe(data: openArray[byte]): bool =
  result = data.len >= 3 and
    data[0].char == 'F' and
    data[1].char == 'U' and
    data[2].char == 'Z' and
    data[3].char == 'Z' # out-of-bounds when data.len == 3

proc initialize(): cint {.cdecl, exportc: "LLVMFuzzerInitialize".} =
  {.emit: "N_CDECL(void, NimMain)(void); NimMain();".}

proc testOneInput(data: ptr UncheckedArray[byte], len: int): cint {.
    cdecl, exportc: "LLVMFuzzerTestOneInput", raises: [].} =
  result = 0
  discard fuzzMe(data.toOpenArray(0, len-1))

Data extraction patterns

How to convert ptr UncheckedArray[byte] + len to Nim types:

Target formatExtraction code
raw byte bufferdata.toOpenArray(0, len-1)
stringvar s = newString(len); if len > 0: copyMem(addr s[0], data, len)
seq[T] via copyMemlet n = len div sizeof(T); var s = newSeq[T](n); if n > 0: copyMem(addr s[0], data, n * sizeof(T))
plain fixed-layout typeif len >= sizeof(T): var value: T; copyMem(addr value, data, sizeof(T))

For the copyMem patterns, the fuzzer-provided buffer is a flat byte array. Check the length before taking element addresses or copying bytes.

Error handling in the harness

Compile fuzz targets with --panics:on. This turns Defect subtypes (IndexDefect, OverflowDefect, AssertionDefect, etc.) into non-catchable panics that immediately crash the process — which libFuzzer interprets as a finding. You don't need a manual except Defect block.

testOneInput must have raises: []. Catch only specific expected error types (e.g., except ValueError) and let everything else crash. Never use except Exception — it catches Defect even with panics off, masking real bugs. Bare except is equivalent to except CatchableError and does not catch Defect.

nim
proc testOneInput(data: ptr UncheckedArray[byte], len: int): cint {.
    cdecl, exportc: "LLVMFuzzerTestOneInput", raises: [].} =
  result = 0
  if len > 0:
    var input = newString(len)
    copyMem(addr input[0], data, len)
    try:
      parseMyFormat(input)
    except ValueError:
      discard          # expected rejection
  # Defects crash the process — no catch needed with --panics:on

Compilation

Build config file (recommended)

Name the config file <harnessname>.nims (e.g. my_fuzzer.nims for my_fuzzer.nim) or config.nims.

nim
--cc: clang
--panics: on
--define: noSignalHandler
--define: useMalloc
--noMain: on
--passC: "-fsanitize=fuzzer,address,undefined"
--passL: "-fsanitize=fuzzer,address,undefined"
--debugger: native
FlagPurpose
--cc: clanglibFuzzer requires Clang.
--panics: onDefects crash the process immediately.
--noMain: onlibFuzzer provides its own main.
--define: noSignalHandlerPrevents Nim's signal handler from masking crashes before ASan.
--define: useMallocUses C malloc so ASan tracks all allocations.
--passC:"-fsanitize=fuzzer,address,undefined"Enables libFuzzer + ASan + UBSan.
--passL:"-fsanitize=fuzzer,address,undefined"Links the sanitizer runtimes.
--debugger: nativeEmbeds DWARF debug info in crash reports.

Corpus management

libFuzzer needs a corpus directory. Start with at least one seed input:

bash
mkdir corpus
echo "GET / HTTP/1.1" > corpus/seed_01

The fuzzer stores new coverage-discovering inputs into the corpus automatically. When restarting, pass the corpus directory to retain progress:

bash
./my_fuzzer corpus/

Seed quality

Provide 1–5 small, valid inputs. Invalid seeds waste cycles. For protocol fuzzers, seed with minimal valid examples. For binary formats, use small well-formed files. Seeds should exercise the main code path, not degenerate cases.

Running the fuzzer

Basic run:

bash
./my_fuzzer corpus/

Useful flags (passed as CLI args to the compiled binary, not to nim c):

FlagEffect
-fork=NRun N worker processes. Omit while debugging.
-ignore_crashes=1In fork mode, keep fuzzing after a crash.
-max_len=NCap input size at N bytes.
-runs=NExecute exactly N runs then exit. Use for CI/regression.
-close_fd_mask=1Close stdout; useful when running many workers.
-max_total_time=NRun for N seconds then exit.
-dict=DICT_FILEUse a dictionary of keywords/tokens to guide mutations.

Example:

bash
./my_fuzzer -fork=4 -ignore_crashes=1 -max_len=65536 corpus/

Standalone mode: reproduce crashes without libFuzzer

Build a standalone binary that replays inputs from files instead of linking libFuzzer. Add this at the bottom of the harness file:

nim
when defined(fuzzStandalone):
  import std/[cmdline, syncio]
  stderr.write "StandaloneFuzzTarget: running " & $paramCount() & " inputs\n"
  for i in 1..paramCount():
    var buf = readFile(paramStr(i))
    discard testOneInput(cast[ptr UncheckedArray[byte]](cstring(buf)), buf.len)

Compile:

bash
nim c -d:fuzzStandalone my_fuzzer.nim

Run against a crash input:

bash
./my_fuzzer crash_input

The standalone binary reads each command-line argument as a file path, feeds its contents to testOneInput, and prints progress to stderr. Use this to reproduce findings without the full fuzzer engine.

Structure-aware fuzzing with custom mutators

For structured formats (protocols, serialized types, typed data), raw byte mutation is inefficient — most mutations produce invalid inputs rejected early. Add customMutator and optionally customCrossOver to mutate at the domain level.

customMutator signature

nim
proc customMutator(data: ptr UncheckedArray[byte], len, maxLen: int,
    seed: int64): int {.
    cdecl, exportc: "LLVMFuzzerCustomMutator", raises: [].}

Called by libFuzzer to mutate data in-place. Must:

  • Read the typed structure from data[0..<len]
  • Apply a type-aware mutation (change/add/delete element, shuffle, flip bit)
  • Write the result back into data[0..<maxLen]
  • Return the new byte length (≤ maxLen)

Use the seed for deterministic randomness. On failure, return the original len.

Defining customMutator replaces libFuzzer's built-in mutations. Include operations that change, add, and remove data.

customCrossOver signature

nim
proc customCrossOver(data1: ptr UncheckedArray[byte], len1: int,
    data2: ptr UncheckedArray[byte], len2: int,
    res: ptr UncheckedArray[byte], maxResLen: int,
    seed: int64): int {.
    cdecl, exportc: "LLVMFuzzerCustomCrossOver", raises: [].}

Combines two inputs into res. Same constraints as customMutator.

When to add custom mutators

Add a custom mutator when:

  • The format has a clear structural boundary (fields, chunks, typed elements)
  • Raw byte mutation mostly produces invalid inputs
  • Coverage stalls despite a good seed corpus

Start without one. Add only when the fuzzer fails to make progress.

See references/structure_aware_fuzzing.md for a worked example.

Crash triage workflow

  1. Save the artifact. libFuzzer writes crash inputs as files named crash-<hash>, slow-unit-<hash>, etc. Copy them to a permanent location.

  2. Reproduce with the standalone binary. Compile with -d:fuzzStandalone and feed the crash input. Confirm the crash is deterministic.

  3. Minimize the input. Run the fuzzer with -minimize_crash=1:

    bash
    ./my_fuzzer -minimize_crash=1 crash_input

    This produces a smaller, still-crashing input.

  4. Isolate with sanitizers. Rebuild with only one sanitizer at a time to understand the bug type:

    bash
    # ASan only
    nim c --cc:clang -d:noSignalHandler -d:useMalloc --noMain:on \
      --passC:"-fsanitize=fuzzer,address" \
      --passL:"-fsanitize=fuzzer,address" -g my_fuzzer.nim
    
    # UBSan only
    nim c --cc:clang -d:noSignalHandler -d:useMalloc --noMain:on \
      --passC:"-fsanitize=fuzzer,undefined" \
      --passL:"-fsanitize=fuzzer,undefined" -g my_fuzzer.nim
  5. Write a minimal Nim reproducer. Extract the failing operation into a standalone .nim file with a doAssert-based test. Keep the original crash input and sanitizer report as evidence.

  6. Classify findings using the framework from the nim-defect-analysis skill. A fuzzer finding with an artifact and sanitizer report is CONFIRMED.

Coverage reporting

Build with Clang source-based coverage:

bash
nim c --cc:clang --panics:on --noMain:on \
  -d:noSignalHandler -d:useMalloc \
  --passC:"-fsanitize=fuzzer -fprofile-instr-generate -fcoverage-mapping" \
  --passL:"-fsanitize=fuzzer -fprofile-instr-generate -fcoverage-mapping" \
  my_fuzzer.nim

LLVM_PROFILE_FILE="fuzz.profraw" ./my_fuzzer -runs=10000 corpus/
llvm-profdata merge -sparse fuzz.profraw -o fuzz.profdata
llvm-cov show ./my_fuzzer -instr-profile=fuzz.profdata --format=html \
  --output-dir=coverage_report

Workflow

  1. Identify the fuzz target. Pick a function or module that parses, deserializes, or processes untrusted byte input. It must be callable from a single entry point.

  2. Set up project files. Create the harness .nim file with initialize and testOneInput. Create a <harnessname>.nims config with the fuzzer flags. Optionally add the fuzzStandalone replay block at the bottom of the harness.

  3. Create a seed corpus. Provide 1–5 minimal valid inputs in corpus/.

  4. Build and smoke-test. Compile with the fuzzer config. Run with -runs=1 to verify the harness starts and processes a seed.

  5. Start fuzzing. Run with -fork=N for parallel workers. Let it run. Check ls corpus/ periodically to see new coverage-discovering inputs.

  6. Inspect for slow units. Files named slow-unit-* indicate inputs that take unusually long. Review whether the code has algorithmic complexity issues.

  7. Triage crashes. For each crash artifact, follow the crash triage steps above: confirm, minimize, isolate sanitizer, write reproducer.

  8. Iterate. If coverage stalls and the format is structured, add a custom mutator. If the harness rejects too many inputs, relax validation in the fuzz path.

Common Mistakes

MistakeWhy it is wrong
Omitting initialize / NimMain()The Nim runtime is not bootstrapped. The target segfaults before any input is tested.
Forgetting raises: [] on testOneInputExceptions propagate to C, producing confusing crash reports. With --panics:on, Defects crash cleanly, but CatchableErrors still need raises: [].
Using assert instead of doAssert in the code under testassert is compiled out in -d:danger. The fuzzer won't find the bug.
Compiling with gcc instead of clanglibFuzzer is Clang-only.
Running ASan without -d:useMallocNim's default allocator is not intercepted. ASan misses heap bugs.
Running ASan without -d:noSignalHandlerNim's signal handler masks crashes before ASan reports.
Using --passC without --passL for sanitizersThe sanitizer runtime must be linked. Both flags are required.
Compiling without --panics:onDefects may be caught and masked. With panics enabled, any Defect crashes the process.
Using invalid seedsThe fuzzer wastes cycles re-discovering valid structure before finding interesting mutations.
Writing a custom mutator prematurelylibFuzzer's built-in mutators handle many formats well. Add custom mutators only after coverage stalls.
Not bounding input size with -max_lenUnbounded inputs can cause memory exhaustion or slow runs.
Catching except Exception in the harnessexcept Exception catches Defect subclasses including IndexDefect and OverflowDefect, masking real bugs. Bare except (same as except CatchableError) does not catch Defect. Catch only specific expected error types like ValueError. Compile with --panics:on instead of manually catching Defect.

References

  • references/simple_byte_target.md — A raw-byte harness with build configuration, seed, and run command.
  • references/structure_aware_fuzzing.md — A length-prefixed custom mutator for stalled coverage.

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Nim Fuzzing AI skill do?

Set up and run libFuzzer-based fuzz targets for Nim code, including harness wiring, compilation flags, corpus management, structure-aware mutators, and crash triage. Use when adding fuzzing to a Nim project, building a fuzz harness for a parser/protocol/format handler, or reproducing and minimizing a fuzzer-found crash.

Why use Nim Fuzzing on TypingMind?

Because you install it once and use it with any model. Nim Fuzzing is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Nim Fuzzing in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/planetis-m/skills/tree/master/nim-fuzzing. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Nim Fuzzing?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Nim Fuzzing?

As many as you like. As long as a model supports skills, you can use Nim Fuzzing with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Nim Fuzzing AI skill free?

It is published on GitHub by planetis-m. Check the repository for licensing terms. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇