cybergo is a security-focused fork of the Go toolchain. In a very simple phrasing, cybergo is a copy of the Go compiler that finds bugs. For now, it focuses on three things:
- Integrating go-panikint: instrumentation that panics on integer overflow/underflow (and optionally on truncating integer conversions).
- Integrating LibAFL fuzzer: run Go fuzzing harnesses with LibAFL for better fuzzing performances.
- Panicking on user-provided function call: catching targeted bugs when certains functions are called (eg.,
myapp.(*Logger).Error).
It especially has two objectives:
- Being easy to use and UX-friendly (we're tired of complex tools),
- Helping to find bugs in Go codebases via built-in security implementations.
cd src && ./make.bash # This produces `./bin/go`. See `GOFLAGS` below.This work is inspired from the previously developed go-panikint. It adds overflow/underflow detection for integer arithmetic operations and (optionnally) type truncation detection for integer conversions. When overflow or truncation is detected, a panic with a detailed error message is triggered, including the specific operation type and integer types involved.
Arithmetic operations: Handles addition +, subtraction -, multiplication *, and division / for both signed and unsigned integer types. For signed integers, covers int8, int16, int32. For unsigned integers, covers uint8, uint16, uint32, uint64. The division case specifically detects the MIN_INT / -1 overflow condition for signed integers. int64 and uintptr are not checked for arithmetic operations.
Type truncation detection: Detects potentially lossy integer type conversions. Covers all integer types: int8, int16, int32, int64, uint8, uint16, uint32, uint64. Excludes uintptr due to platform-dependent usage. This is disabled by default.
Overflow detection is enabled by default. To disable it, add GOFLAGS='-gcflags=-overflowdetect=false' before your ./make.bash. You can also enable truncation issues checker with: -gcflags=-truncationdetect=true
This feature patches the compiler SSA generation so that integer arithmetic operations and integer conversions get extra runtime checks that call into the runtime to panic with a detailed error message when a bug is detected. Checks are applied using source-location-based filtering so user code is instrumented while standard library files and dependencies (module cache and vendor/) are skipped.
You can read the associated blog post about it here.
Add a marker on the same line as the operation or the line immediately above to suppress a specific report:
- Overflow/underflow:
overflow_false_positive - Truncation:
truncation_false_positive
Example:
// overflow_false_positive
intentionalOverflow := a + b
// truncation_false_positive
x := uint8(big)
sum2 := a + b // overflow_false_positive
x2 := uint8(big) // truncation_false_positiveSometimes this might not work, that's because Go is in-lining the function. If // overflow_false_poistive isn't enough, add //go:noinline before the signature of your function.
When fuzzing targets, we may be interested in triggering a panic when certain functions are called. For example, some software may emit log.error messages instead of panicking, even though such conditions often indicate states that security researchers would want to detect during fuzzing.
However, these errors are usually handled internally (e.g., through retry or pause mechanisms, or by printing messages to logs), which makes them largely invisible to fuzzers. The objective of this feature is to address this issue.
Compile cybergo, then use the --panic-on flag.
./bin/go test -fuzz=FuzzHarness --use-libafl --panic-on="test_go_panicon.(*Logger).Warning,test_go_panicon.(*Logger).Error"The example above would panic when either (*Logger).Warning or (*Logger).Error is called (comma-separated list).
How panic on selected functions feature works
┌───────────────────────────────────────────────────────────────────────────┐
│ 1) cybergo `go test` │
│ - parses + validates `-panic-on=...` against packages being built │
│ - forwards patterns to the compiler via `-panic-on-call=...` │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 2) `cmd/compile` │
│ - prevents inlining of matching calls so the call stays visible │
│ - SSA pass inserts a call to `runtime.panicOnCall(...)` │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 3) `runtime.panicOnCall` │
│ - panics with: "panic-on-call: func-name" │
└───────────────────────────────────────────────────────────────────────────┘
In practice, this makes any matched call site behave like a crash/panic for fuzzers (note: only static call sites can be trapped).
LibAFL performs way better than the traditional Go fuzzer. Using the --use-libafl flag runs standard Go fuzz tests (go test -fuzz=...) with LibAFL. The runner is implemented in golibafl/. Without --use-libafl, the fuzzer behaves like upstream Go. More documentation in this Markdown file.
You can also pass an optional config. file for LibAFL, see here.
./bin/go test -fuzz=FuzzHarness --use-libafl --libafl-config=path/to/libafl.jsonc # optionnal --libafl-configHow Go + LibAFL are wired together
┌───────────────────────────────────────────────────────────────────────────┐
│ 1) cybergo `go test` │
│ - captures `testing.F.Fuzz(...)` callback │
│ - generates extra source file: `_libaflmain.go` │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 2) Generated bridge: `_libaflmain.go` │
│ - provides libFuzzer-style C ABI entrypoints: │
│ LLVMFuzzerInitialize │
│ LLVMFuzzerTestOneInput │
│ - adapts bytes -> Go types -> calls the captured fuzz callback │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 3) `libharness.a` (static archive on disk) contains: │
│ - compiled objects for all test package (+ dependencies) │
│ - generated `_testmain.go` + `_libaflmain.go` │
│ - LLVMFuzzerInitialize │
│ - LLVMFuzzerTestOneInput │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 4) `golibafl/` (Rust + LibAFL) │
│ env: HARNESS_LIB=/path/to/libharness.a │
│ fuzz loop: mutate input -> LLVMFuzzerTestOneInput(data) -> observe │
└───────────────────────────────────────────────────────────────────────────┘
In --use-libafl mode, cybergo builds libharness.a and the Rust golibafl runner drives it in-process via the libFuzzer entrypoints.
Let's talk about the motivation behind using LibAFL. Fuzzing with go test -fuzz is far behind the state-of-the-art fuzzing techniques. A good example for this is AFL++'s CMPLOG/Redqueen. Those features allow fuzzers to solve certain constraints. Let's assume the following snippet
if input == "IMARANDOMSTRINGJUSTCMPLOGMEMAN" {
panic("this string is illegal")
}SOTA fuzzers like AFL++ or LibAFL would find the panic instantly in that case. However, Go native fuzzer wouldn't. That is a massive gap that restrains coverage exploration by a lot.
The benchmark below show those limits. Note that those benchmarks can be reproduced and improved via the cybergo-bench-libafl repository.
The chart below is the evolution of the number of lines covered while fuzzing Google's UUID using LibAFL vs go native fuzzer.
The chart below is the evolution of the number of lines covered while fuzzing go-ethereum using LibAFL vs go native fuzzer.
You can test it on some fuzzing harnesses in test/cybergo/examples/.
cd test/cybergo/examples/reverse
../../../../bin/go test -fuzz=FuzzReverse --use-libaflCredits to Bruno Produit and Nills Ollrogge for their work on golibafl.