public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
1eba36ed8754fbff9cdc04f61b7092dbb7a77967 blob 4192 bytes (raw)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
 
## Fuzzing passt with AFL++

### Prerequisites

- AFL++ (afl-clang-fast, afl-fuzz)
- Linux kernel with `CONFIG_USER_NS=y` (default on Fedora, Debian, Ubuntu)

### Build

```
make fuzz           # AFL++-instrumented binary (produces passt.fuzz)
```

This also builds `fuzz-server` (the host-side test peer) as a prerequisite.

This produces:
- `passt.fuzz` -- instrumented with AFL++ and AddressSanitizer
- `fuzz-server` -- host-side test peer (built automatically)

To use a specific AFL++ installation:

```
make FUZZ_CC=/path/to/afl-clang-fast fuzz
```

### Network setup

The fuzzer runs in a rootless user + network namespace with AnyIP
routing so the test server intercepts all outbound IPv4 and IPv6
TCP from passt, regardless of destination IP or port.  No root
access is needed -- `fuzz-setup.sh` uses `unshare --user --net`
to create the namespace pair, matching how pasta itself operates.

The test server listens on ports 1 through 55535 (both IPv4 and
IPv6), leaving the top 10000 ports free for passt's own
`connect()` and `bind()` calls.

The namespace exists only while the command runs; no cleanup step
is required.

### Run

passt automatically fork+execs `fuzz-server` before the AFL++
forkserver starts, then sleeps 3 seconds to let it finish binding
all ports.  There is no need to launch it separately.

Basic run:

```
fuzzing/fuzz-setup.sh -- afl-fuzz -i fuzzing/testcase_dir \
  -o fuzzing/sync_dir -- ./passt.fuzz --foreground
```

Multi-core (secondary instances share the corpus):

```
# Terminal 1 -- main instance:
fuzzing/fuzz-setup.sh -- afl-fuzz -M main \
  -i fuzzing/testcase_dir -o fuzzing/sync_dir \
  -- ./passt.fuzz --foreground

# Terminal 2 -- secondary with different power schedule:
fuzzing/fuzz-setup.sh -- afl-fuzz -S variant1 -p rare \
  -i fuzzing/testcase_dir -o fuzzing/sync_dir \
  -- ./passt.fuzz --foreground
```

### Architecture

AFL++ controls four regions of the testcase buffer:

- **(a) `ev`** -- array of epoll events injected into passt's
  main loop alongside real kernel events
- **(b) `buf`** -- raw tap-side packets (full L2 frames, headers
  included). AFL++ controls everything: Ethernet, IP, TCP/UDP
  headers, destination addresses, payload
- **(c) `test_buf`** -- payload the test server sends to passt
  on accepted connections
- **(d) `sockopt_buf`** -- TCP_INFO data returned to passt by the
  `getsockopt()` wrapper in fuzz.c

The testcase header carries explicit lengths for each region:
`n_events` (u32), `tap_len` (u16), `testbuf_len` (u16), and
`sockopt_len` (u16).  Both passt and `fuzz-server` call
`fuzz_parse_layout()` on the same header, which clamps each
declared length to the available space and to per-region maximums,
so the two sides always agree on offsets.  AFL++ controls the
split directly through mutation of these header fields.

Example flow for a single event:

1. AFL++ writes an EPOLLIN event with type EPOLL_TYPE_TAP_PASST
   in `ev`, raw packet data in `buf`, and payload in `test_buf`
2. passt reads the event from `ev`, reads data from `buf`, and
   hands it to tap_handler()
3. The data happens to have Ethernet, IP, and TCP headers with
   the SYN flag set (AFL++ discovered this format). passt calls
   connect() to the destination in the packet
4. AnyIP routing makes the destination local and the test server,
   which listens on ports 1-55535 (IPv4+IPv6), accepts the connection
5. The test server sends the contents of `test_buf` to passt
6. A real epoll_wait() fires EPOLLOUT for passt (not from `ev`)
7. passt marks the connection established and inserts it in the
   flow table
8. passt reads data from the test server and generates TCP data
   back to the "guest"

### Seed inputs

`testcase_dir/empty.bin` provides a minimal starting point.
AFL++ discovers packet formats through mutation.

### Reproducing crashes

Replay a crash input (`fuzz-server` is fork+exec'd by passt
automatically):

```
fuzzing/fuzz-setup.sh -- ./passt.fuzz --foreground < \
  fuzzing/sync_dir/default/crashes/id:000000,...
```

Minimize a crash input:

```
fuzzing/fuzz-setup.sh -- afl-tmin \
  -i fuzzing/sync_dir/default/crashes/id:000000,... \
  -o crash_minimized -- ./passt.fuzz --foreground
```
debug log:

solving 1eba36ed ...
found 1eba36ed in https://archives.passt.top/passt-dev/20260928051727.2251281-8-anskuma@redhat.com/

applying [1/1] https://archives.passt.top/passt-dev/20260928051727.2251281-8-anskuma@redhat.com/
diff --git a/fuzzing/README.fuzzing.md b/fuzzing/README.fuzzing.md
new file mode 100644
index 00000000..1eba36ed

Checking patch fuzzing/README.fuzzing.md...
Applied patch fuzzing/README.fuzzing.md cleanly.

index at:
100644 1eba36ed8754fbff9cdc04f61b7092dbb7a77967	fuzzing/README.fuzzing.md

Code repositories for project(s) associated with this public inbox

	https://passt.top/passt

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for IMAP folder(s).