mirror of
https://github.com/sol1/rustguac.git
synced 2026-09-10 01:26:06 +00:00
Add guacd parser fuzz harness and document fuzzing findings
libFuzzer+ASan+UBSan harness for guac_parser_append() — the C state machine that parses all Guacamole wire-format input in guacd. 3.2M iterations found no memory corruption; one non-exploitable signed integer overflow (UBSan) in the length prefix accumulator noted in FINDINGS.md. Also adds FINDINGS.md for the Rust protocol parser fuzzer documenting the UTF-8 boundary panic fix from v0.1.3. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fuzz_parser
|
||||
fuzz_parser_afl
|
||||
fuzz_parser_standalone
|
||||
corpus/
|
||||
findings/
|
||||
crash-*
|
||||
oom-*
|
||||
timeout-*
|
||||
@@ -0,0 +1,58 @@
|
||||
# guacd Parser Fuzzing Findings
|
||||
|
||||
Harness targets `guac_parser_append()` from guacamole-server's libguac —
|
||||
the core state machine that parses all Guacamole wire-format data received
|
||||
from clients.
|
||||
|
||||
## Setup
|
||||
|
||||
- **Fuzzer**: libFuzzer (clang 19) with ASan + UBSan
|
||||
- **Target function**: `guac_parser_append()` in `src/libguac/parser.c`
|
||||
- **Dependencies compiled inline**: `parser.c`, `unicode.c` (no autoconf needed)
|
||||
|
||||
Build: `./build.sh` (requires clang)
|
||||
|
||||
## Results
|
||||
|
||||
**Run date**: 2026-02-07
|
||||
**Iterations**: ~3.2 million (5 minutes)
|
||||
**Exec/s**: ~10,800
|
||||
**Coverage**: 138 edges (saturated — parser state machine is compact)
|
||||
|
||||
### No crashes or memory errors
|
||||
|
||||
ASan found no heap buffer overflows, use-after-free, or other memory
|
||||
corruption issues in 3.2M iterations across varying chunk sizes.
|
||||
|
||||
### Signed integer overflow in length prefix parser (UBSan, non-exploitable)
|
||||
|
||||
**Location**: `parser.c:83`
|
||||
**Severity**: Informational (undefined behavior, not exploitable)
|
||||
|
||||
The length prefix accumulator:
|
||||
```c
|
||||
parsed_length = parsed_length*10 + c - '0';
|
||||
```
|
||||
uses `int` arithmetic and can overflow with a long digit string (e.g.,
|
||||
`222222222...`) before the subsequent bounds check:
|
||||
```c
|
||||
if (parsed_length > GUAC_INSTRUCTION_MAX_LENGTH) {
|
||||
parser->state = GUAC_PARSE_ERROR;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
In practice, the `GUAC_INSTRUCTION_MAX_LENGTH` (8192) check catches
|
||||
malicious lengths after the overflow wraps, and the parser transitions to
|
||||
`GUAC_PARSE_ERROR`. The overflow is technically undefined behavior per the
|
||||
C standard but has no security impact — the parser rejects the input
|
||||
regardless.
|
||||
|
||||
**Upstream**: guacamole-server (Apache). Not filed — low impact, and the
|
||||
existing bounds check is sufficient in practice.
|
||||
|
||||
## Rust protocol parser fuzzing
|
||||
|
||||
See `../fuzz/` for the Rust-side protocol parser fuzzer (cargo-fuzz).
|
||||
That fuzzer found and fixed a UTF-8 char boundary panic in
|
||||
`src/protocol.rs` — see commit history for v0.1.3.
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Build the guacd parser fuzzer.
|
||||
#
|
||||
# Usage:
|
||||
# ./build.sh # build with libFuzzer (default)
|
||||
# ./build.sh afl # build with AFL++
|
||||
# ./build.sh standalone # build a standalone test binary (no fuzzer)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
GUAC_SRC="${SCRIPT_DIR}/../../guacamole-server/src/libguac"
|
||||
|
||||
if [ ! -f "${GUAC_SRC}/parser.c" ]; then
|
||||
echo "ERROR: guacamole-server source not found at ${GUAC_SRC}" >&2
|
||||
echo "Expected: ../guacamole-server relative to rustguac root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MODE="${1:-libfuzzer}"
|
||||
SANITIZERS="-fsanitize=address,undefined"
|
||||
# Include our stubs first (-I.) so config.h and guacamole/socket.h
|
||||
# resolve to our minimal stubs before the real guacamole-server headers.
|
||||
INCLUDES="-I${SCRIPT_DIR} -I${GUAC_SRC}"
|
||||
CFLAGS="-g -O1 ${SANITIZERS} ${INCLUDES}"
|
||||
|
||||
SOURCES=(
|
||||
"${SCRIPT_DIR}/fuzz_parser.c"
|
||||
"${GUAC_SRC}/parser.c"
|
||||
"${GUAC_SRC}/unicode.c"
|
||||
)
|
||||
|
||||
case "${MODE}" in
|
||||
libfuzzer)
|
||||
echo "Building with libFuzzer..."
|
||||
CC="${CC:-clang}"
|
||||
$CC ${CFLAGS} -fsanitize=fuzzer "${SOURCES[@]}" -o "${SCRIPT_DIR}/fuzz_parser"
|
||||
echo "Built: ${SCRIPT_DIR}/fuzz_parser"
|
||||
echo "Run: ${SCRIPT_DIR}/fuzz_parser ${SCRIPT_DIR}/corpus/"
|
||||
;;
|
||||
afl)
|
||||
echo "Building with AFL++..."
|
||||
CC="${CC:-afl-clang-fast}"
|
||||
$CC ${CFLAGS} -DAFL_MODE "${SOURCES[@]}" -o "${SCRIPT_DIR}/fuzz_parser_afl"
|
||||
echo "Built: ${SCRIPT_DIR}/fuzz_parser_afl"
|
||||
echo "Run: afl-fuzz -i ${SCRIPT_DIR}/corpus/ -o ${SCRIPT_DIR}/findings/ -- ${SCRIPT_DIR}/fuzz_parser_afl"
|
||||
;;
|
||||
standalone)
|
||||
# Build a standalone binary that reads from stdin — useful for
|
||||
# reproducing crashes without a fuzzer installed.
|
||||
echo "Building standalone test binary..."
|
||||
CC="${CC:-clang}"
|
||||
cat > /tmp/fuzz_standalone_main.c <<'MAIN_EOF'
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
|
||||
int main(int argc, char **argv) {
|
||||
for (int i = 1; i < argc; i++) {
|
||||
FILE *f = fopen(argv[i], "rb");
|
||||
if (!f) { perror(argv[i]); continue; }
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
uint8_t *buf = malloc(sz);
|
||||
fread(buf, 1, sz, f);
|
||||
fclose(f);
|
||||
printf("Testing %s (%ld bytes)... ", argv[i], sz);
|
||||
LLVMFuzzerTestOneInput(buf, sz);
|
||||
printf("OK\n");
|
||||
free(buf);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
MAIN_EOF
|
||||
$CC ${CFLAGS} /tmp/fuzz_standalone_main.c "${SOURCES[@]}" \
|
||||
-o "${SCRIPT_DIR}/fuzz_parser_standalone"
|
||||
rm /tmp/fuzz_standalone_main.c
|
||||
echo "Built: ${SCRIPT_DIR}/fuzz_parser_standalone"
|
||||
echo "Run: ${SCRIPT_DIR}/fuzz_parser_standalone corpus/*.raw"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [libfuzzer|afl|standalone]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,4 @@
|
||||
/* Empty config.h stub for fuzzing — no autoconf defines needed */
|
||||
#ifndef FUZZ_CONFIG_H
|
||||
#define FUZZ_CONFIG_H
|
||||
#endif
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* AFL++/libFuzzer harness for guacd's Guacamole protocol parser.
|
||||
*
|
||||
* Fuzzes guac_parser_append() — the core state machine that parses
|
||||
* Guacamole wire format data. This is the function that processes
|
||||
* all input from rustguac before any protocol handling occurs.
|
||||
*
|
||||
* Build with build.sh or manually:
|
||||
* libFuzzer: clang -fsanitize=fuzzer,address,undefined \
|
||||
* -I. -I$GUAC/src/libguac \
|
||||
* -o fuzz_parser fuzz_parser.c $GUAC/src/libguac/unicode.c \
|
||||
* $GUAC/src/libguac/parser.c
|
||||
*
|
||||
* AFL++: afl-clang-fast -fsanitize=address,undefined \
|
||||
* -I. -I$GUAC/src/libguac \
|
||||
* -o fuzz_parser fuzz_parser.c $GUAC/src/libguac/unicode.c \
|
||||
* $GUAC/src/libguac/parser.c
|
||||
*
|
||||
* Our config.h and guacamole/socket.h stubs in this directory override
|
||||
* the real ones (which need autoconf / pull in the full socket API).
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── Stubs for libguac internals ────────────────────────────────── */
|
||||
|
||||
/* guac_error / guac_error_message are macros expanding to
|
||||
* (*__guac_error()) / (*__guac_error_message()). Provide the backing
|
||||
* thread-local storage and accessor functions. */
|
||||
#include "guacamole/error-types.h"
|
||||
|
||||
static __thread guac_status _guac_error_value = GUAC_STATUS_SUCCESS;
|
||||
static __thread const char* _guac_error_message_value = "";
|
||||
|
||||
guac_status* __guac_error(void) {
|
||||
return &_guac_error_value;
|
||||
}
|
||||
|
||||
const char** __guac_error_message(void) {
|
||||
return &_guac_error_message_value;
|
||||
}
|
||||
|
||||
/* Overflow-checked multiply (PRIV_guac_mem_ckd_mul) */
|
||||
int PRIV_guac_mem_ckd_mul(size_t* result, size_t factor_count,
|
||||
const size_t* factors) {
|
||||
if (factor_count == 0) return 1;
|
||||
size_t size = factors[0];
|
||||
for (size_t i = 1; i < factor_count; i++) {
|
||||
if (factors[i] && size > SIZE_MAX / factors[i]) return 1;
|
||||
size *= factors[i];
|
||||
}
|
||||
*result = size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* guac_mem_alloc — the macro expands to PRIV_guac_mem_alloc() */
|
||||
void* PRIV_guac_mem_alloc(size_t factor_count, const size_t* factors) {
|
||||
size_t size;
|
||||
if (PRIV_guac_mem_ckd_mul(&size, factor_count, factors)) return NULL;
|
||||
if (size == 0) size = 1;
|
||||
return malloc(size);
|
||||
}
|
||||
|
||||
/* guac_mem_free */
|
||||
void PRIV_guac_mem_free(void* ptr) {
|
||||
free(ptr);
|
||||
}
|
||||
|
||||
/* Stubs for socket functions referenced by guac_parser_read() —
|
||||
* we don't fuzz that path but the linker needs symbols. */
|
||||
typedef struct guac_socket guac_socket;
|
||||
int guac_socket_select(guac_socket* socket, int usec_timeout) {
|
||||
(void)socket; (void)usec_timeout;
|
||||
return -1;
|
||||
}
|
||||
int guac_socket_read(guac_socket* socket, void* buf, int count) {
|
||||
(void)socket; (void)buf; (void)count;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ── Pull in the real parser API ─────────────────────────────────
|
||||
* We include the headers (not the .c files) — the .c files are
|
||||
* compiled separately and linked by the build script. */
|
||||
#include "guacamole/parser.h"
|
||||
|
||||
/* ── Fuzz target ── */
|
||||
|
||||
#ifdef __AFL_FUZZ_TESTCASE_LEN
|
||||
/* AFL++ persistent mode */
|
||||
__AFL_FUZZ_INIT();
|
||||
#endif
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
if (size == 0) return 0;
|
||||
|
||||
/* Work on a mutable copy — guac_parser_append() modifies the buffer
|
||||
* (it writes NUL terminators at element boundaries). */
|
||||
char *buf = malloc(size);
|
||||
if (!buf) return 0;
|
||||
memcpy(buf, data, size);
|
||||
|
||||
guac_parser* parser = guac_parser_alloc();
|
||||
if (!parser) { free(buf); return 0; }
|
||||
|
||||
/* Feed the data in varying chunk sizes to exercise boundary handling */
|
||||
size_t pos = 0;
|
||||
size_t chunk = 1;
|
||||
while (pos < size) {
|
||||
size_t remaining = size - pos;
|
||||
size_t len = chunk < remaining ? chunk : remaining;
|
||||
|
||||
int parsed = guac_parser_append(parser, buf + pos, (int)len);
|
||||
|
||||
if (parser->state == GUAC_PARSE_ERROR) {
|
||||
/* Reset and continue to test recovery */
|
||||
guac_parser_free(parser);
|
||||
parser = guac_parser_alloc();
|
||||
if (!parser) { free(buf); return 0; }
|
||||
pos += len;
|
||||
} else if (parser->state == GUAC_PARSE_COMPLETE) {
|
||||
/* Successfully parsed an instruction — access fields to
|
||||
* trigger any potential out-of-bounds reads */
|
||||
volatile const char* op = parser->opcode;
|
||||
(void)op;
|
||||
for (int i = 0; i < parser->argc; i++) {
|
||||
volatile const char* arg = parser->argv[i];
|
||||
(void)arg;
|
||||
}
|
||||
/* Reset for next instruction */
|
||||
guac_parser_free(parser);
|
||||
parser = guac_parser_alloc();
|
||||
if (!parser) { free(buf); return 0; }
|
||||
pos += len;
|
||||
} else {
|
||||
pos += (parsed > 0) ? (size_t)parsed : len;
|
||||
}
|
||||
|
||||
/* Vary chunk size: 1, 2, 4, 8, ..., 64, then back to 1 */
|
||||
chunk = (chunk >= 64) ? 1 : chunk * 2;
|
||||
}
|
||||
|
||||
guac_parser_free(parser);
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifndef __AFL_FUZZ_TESTCASE_LEN
|
||||
/* When not using AFL, this is a libFuzzer target (no main needed) */
|
||||
#else
|
||||
/* AFL persistent mode main */
|
||||
int main(void) {
|
||||
__AFL_INIT();
|
||||
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
|
||||
while (__AFL_LOOP(100000)) {
|
||||
int len = __AFL_FUZZ_TESTCASE_LEN;
|
||||
LLVMFuzzerTestOneInput(buf, len);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* Minimal socket.h stub for fuzzing — only the forward typedef is needed.
|
||||
* parser.h includes socket-types.h which defines guac_socket, but parser.c
|
||||
* also includes this full header. We stub it to avoid pulling in client-types.h
|
||||
* and the rest of the socket API.
|
||||
*/
|
||||
#ifndef _GUAC_SOCKET_H
|
||||
#define _GUAC_SOCKET_H
|
||||
#include "socket-types.h"
|
||||
#endif
|
||||
Generated
+3716
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
# Rust Protocol Parser Fuzzing Findings
|
||||
|
||||
Fuzz targets for `src/protocol.rs` — rustguac's Guacamole wire-format
|
||||
parser and streaming instruction parser.
|
||||
|
||||
## Setup
|
||||
|
||||
- **Fuzzer**: cargo-fuzz (libFuzzer) with nightly Rust
|
||||
- **Targets**: `protocol_parse` (single instruction), `protocol_stream` (streaming)
|
||||
|
||||
Run: `cargo +nightly fuzz run protocol_parse` / `cargo +nightly fuzz run protocol_stream`
|
||||
|
||||
## Results
|
||||
|
||||
**Run date**: 2026-02-07
|
||||
**Iterations**: ~52.8 million total (49.5M parse + 3.3M stream, 5 min each)
|
||||
|
||||
### Fixed: UTF-8 char boundary panic
|
||||
|
||||
**Crash input**: `4.smze,1.\xc7\xbb\x00`
|
||||
**Severity**: Denial of service (panic/unwrap)
|
||||
|
||||
`Instruction::parse()` sliced a string at a byte offset derived from
|
||||
the length prefix without checking `is_char_boundary()`. When the
|
||||
length pointed into the middle of a multi-byte UTF-8 character (e.g.,
|
||||
the 2-byte sequence `\xc7\xbb`), Rust panicked.
|
||||
|
||||
**Fix**: Added `remaining.is_char_boundary(len)` check before slicing,
|
||||
returning `ParseError::Truncated` for invalid boundaries. Fixed in v0.1.3.
|
||||
|
||||
### No other issues
|
||||
|
||||
After the fix, both fuzz targets ran 52.8M iterations with zero
|
||||
additional crashes or panics.
|
||||
|
||||
## guacd parser fuzzing
|
||||
|
||||
See `../fuzz-guacd/` for the C-side guacd protocol parser fuzzer.
|
||||
Reference in New Issue
Block a user