├── .github └── workflows │ ├── release.yml │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── configs └── archlinux.config ├── corpus ├── bko-154021-invalid-drop-level.raw.zst ├── bko-154961-heap-overflow-chunk-items.raw.zst ├── bko-155181-bad-backref.raw.zst ├── bko-156731.raw.zst ├── bko-161811.raw.zst └── bko-200403.raw.zst ├── docs ├── architecture.md └── mscgen │ ├── architecture.mscgen │ └── architecture.png ├── scripts ├── contiguous_zeroes.py ├── docker │ ├── config_kernel.sh │ └── entry.sh └── validate_crashes.sh ├── src ├── imgcompress │ ├── Cargo.toml │ └── src │ │ ├── bin │ │ └── main.rs │ │ ├── btrfs.rs │ │ ├── chunk_tree.rs │ │ ├── lib.rs │ │ ├── structs.rs │ │ └── tree.rs ├── manager │ ├── __init__.py │ └── manager.py ├── mutator │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── runner │ ├── Cargo.toml │ └── src │ ├── constants.rs │ ├── forkserver.rs │ ├── kcov.rs │ ├── main.rs │ └── mount.rs └── x.py /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | name: Upload zstd compressed docker image tarball 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | - name: Install deps 16 | run: sudo apt-get install btrfs-progs podman python3 17 | - name: Install python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Symlink python3 22 | run: "sudo ln -s $(which python) /bin/python3" 23 | - name: Install pexpect 24 | run: "pip install --user pexpect" 25 | - name: Free disk space 26 | run: | 27 | echo "Before disk space clean" 28 | df -h 29 | sudo swapoff -a 30 | sudo rm -f /swapfile 31 | sudo apt-get clean 32 | docker rmi $(docker image ls -aq) 33 | df -h 34 | - name: Build docker image 35 | run: | 36 | ./x.py build 37 | ./x.py --local build-tar --zstd fs.tzst 38 | - name: Create release 39 | id: create_release 40 | uses: actions/create-release@v1 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | with: 44 | tag_name: ${{ github.ref }} 45 | release_name: Release ${{ github.ref }} 46 | draft: false 47 | prerelease: false 48 | - name: Upload Release Asset 49 | id: upload-release-asset 50 | uses: actions/upload-release-asset@v1 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | with: 54 | upload_url: ${{ steps.create_release.outputs.upload_url }} 55 | asset_path: ./fs.tzst 56 | asset_name: fs.tzst 57 | asset_content_type: application/zstd 58 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Install deps 15 | run: sudo apt-get install -y btrfs-progs 16 | - name: Run build 17 | run: cargo build --verbose 18 | - name: Run tests 19 | run: cargo test --verbose 20 | - name: Run rustfmt 21 | run: cargo fmt -- --check 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | __pycache__/ 9 | 10 | _state 11 | _confirmed 12 | fs/ 13 | 14 | .#* 15 | .tags* 16 | .gdb_history 17 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "ansi_term" 5 | version = "0.11.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 8 | dependencies = [ 9 | "winapi", 10 | ] 11 | 12 | [[package]] 13 | name = "anyhow" 14 | version = "1.0.32" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" 17 | 18 | [[package]] 19 | name = "atty" 20 | version = "0.2.14" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 23 | dependencies = [ 24 | "hermit-abi", 25 | "libc", 26 | "winapi", 27 | ] 28 | 29 | [[package]] 30 | name = "autocfg" 31 | version = "1.0.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 34 | 35 | [[package]] 36 | name = "bitflags" 37 | version = "1.2.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 40 | 41 | [[package]] 42 | name = "byteorder" 43 | version = "1.3.4" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 46 | 47 | [[package]] 48 | name = "cc" 49 | version = "1.0.60" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "ef611cc68ff783f18535d77ddd080185275713d852c4f5cbb6122c462a7a825c" 52 | dependencies = [ 53 | "jobserver", 54 | ] 55 | 56 | [[package]] 57 | name = "cfg-if" 58 | version = "0.1.10" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 61 | 62 | [[package]] 63 | name = "clap" 64 | version = "2.33.3" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 67 | dependencies = [ 68 | "ansi_term", 69 | "atty", 70 | "bitflags", 71 | "strsim", 72 | "textwrap", 73 | "unicode-width", 74 | "vec_map", 75 | ] 76 | 77 | [[package]] 78 | name = "crc32c" 79 | version = "0.5.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "f6419af41d57055d753ec718ab9318e08d35378f0094b3ae4779ae15857951aa" 82 | 83 | [[package]] 84 | name = "either" 85 | version = "1.6.1" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 88 | 89 | [[package]] 90 | name = "errno" 91 | version = "0.2.6" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "6eab5ee3df98a279d9b316b1af6ac95422127b1290317e6d18c1743c99418b01" 94 | dependencies = [ 95 | "errno-dragonfly", 96 | "libc", 97 | "winapi", 98 | ] 99 | 100 | [[package]] 101 | name = "errno-dragonfly" 102 | version = "0.1.1" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "14ca354e36190500e1e1fb267c647932382b54053c50b14970856c0b00a35067" 105 | dependencies = [ 106 | "gcc", 107 | "libc", 108 | ] 109 | 110 | [[package]] 111 | name = "fuchsia-cprng" 112 | version = "0.1.1" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 115 | 116 | [[package]] 117 | name = "fuzzmutator" 118 | version = "0.2.1" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "84d8be77392916254d4755d1ebdc40312e31fa9957c69c5270ac05a426f4c4d6" 121 | dependencies = [ 122 | "rand 0.4.6", 123 | ] 124 | 125 | [[package]] 126 | name = "gcc" 127 | version = "0.3.55" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" 130 | 131 | [[package]] 132 | name = "getrandom" 133 | version = "0.1.15" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" 136 | dependencies = [ 137 | "cfg-if", 138 | "libc", 139 | "wasi", 140 | ] 141 | 142 | [[package]] 143 | name = "glob" 144 | version = "0.3.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 147 | 148 | [[package]] 149 | name = "heck" 150 | version = "0.3.1" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 153 | dependencies = [ 154 | "unicode-segmentation", 155 | ] 156 | 157 | [[package]] 158 | name = "hermit-abi" 159 | version = "0.1.16" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "4c30f6d0bc6b00693347368a67d41b58f2fb851215ff1da49e90fe2c5c667151" 162 | dependencies = [ 163 | "libc", 164 | ] 165 | 166 | [[package]] 167 | name = "imgcompress" 168 | version = "0.1.0" 169 | dependencies = [ 170 | "anyhow", 171 | "crc32c", 172 | "rmp-serde", 173 | "serde", 174 | "structopt", 175 | "tempfile", 176 | "zstd", 177 | ] 178 | 179 | [[package]] 180 | name = "itertools" 181 | version = "0.9.0" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" 184 | dependencies = [ 185 | "either", 186 | ] 187 | 188 | [[package]] 189 | name = "jobserver" 190 | version = "0.1.21" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" 193 | dependencies = [ 194 | "libc", 195 | ] 196 | 197 | [[package]] 198 | name = "lazy_static" 199 | version = "1.4.0" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 202 | 203 | [[package]] 204 | name = "libc" 205 | version = "0.2.77" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235" 208 | 209 | [[package]] 210 | name = "loopdev" 211 | version = "0.2.1" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "ac9e35cfb6646d67059f2ca8913a90e6c60633053c103df423975297f33d6fcc" 214 | dependencies = [ 215 | "errno", 216 | "libc", 217 | ] 218 | 219 | [[package]] 220 | name = "mutator" 221 | version = "0.1.0" 222 | dependencies = [ 223 | "anyhow", 224 | "fuzzmutator", 225 | "imgcompress", 226 | "libc", 227 | "rmp-serde", 228 | "serde", 229 | ] 230 | 231 | [[package]] 232 | name = "nix" 233 | version = "0.18.0" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" 236 | dependencies = [ 237 | "bitflags", 238 | "cc", 239 | "cfg-if", 240 | "libc", 241 | ] 242 | 243 | [[package]] 244 | name = "num-traits" 245 | version = "0.2.12" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" 248 | dependencies = [ 249 | "autocfg", 250 | ] 251 | 252 | [[package]] 253 | name = "ppv-lite86" 254 | version = "0.2.9" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" 257 | 258 | [[package]] 259 | name = "proc-macro-error" 260 | version = "1.0.4" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 263 | dependencies = [ 264 | "proc-macro-error-attr", 265 | "proc-macro2", 266 | "quote", 267 | "syn", 268 | "version_check", 269 | ] 270 | 271 | [[package]] 272 | name = "proc-macro-error-attr" 273 | version = "1.0.4" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 276 | dependencies = [ 277 | "proc-macro2", 278 | "quote", 279 | "version_check", 280 | ] 281 | 282 | [[package]] 283 | name = "proc-macro2" 284 | version = "1.0.21" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "36e28516df94f3dd551a587da5357459d9b36d945a7c37c3557928c1c2ff2a2c" 287 | dependencies = [ 288 | "unicode-xid", 289 | ] 290 | 291 | [[package]] 292 | name = "quote" 293 | version = "1.0.7" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" 296 | dependencies = [ 297 | "proc-macro2", 298 | ] 299 | 300 | [[package]] 301 | name = "rand" 302 | version = "0.4.6" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 305 | dependencies = [ 306 | "fuchsia-cprng", 307 | "libc", 308 | "rand_core 0.3.1", 309 | "rdrand", 310 | "winapi", 311 | ] 312 | 313 | [[package]] 314 | name = "rand" 315 | version = "0.7.3" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 318 | dependencies = [ 319 | "getrandom", 320 | "libc", 321 | "rand_chacha", 322 | "rand_core 0.5.1", 323 | "rand_hc", 324 | ] 325 | 326 | [[package]] 327 | name = "rand_chacha" 328 | version = "0.2.2" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 331 | dependencies = [ 332 | "ppv-lite86", 333 | "rand_core 0.5.1", 334 | ] 335 | 336 | [[package]] 337 | name = "rand_core" 338 | version = "0.3.1" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 341 | dependencies = [ 342 | "rand_core 0.4.2", 343 | ] 344 | 345 | [[package]] 346 | name = "rand_core" 347 | version = "0.4.2" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 350 | 351 | [[package]] 352 | name = "rand_core" 353 | version = "0.5.1" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 356 | dependencies = [ 357 | "getrandom", 358 | ] 359 | 360 | [[package]] 361 | name = "rand_hc" 362 | version = "0.2.0" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 365 | dependencies = [ 366 | "rand_core 0.5.1", 367 | ] 368 | 369 | [[package]] 370 | name = "rdrand" 371 | version = "0.4.0" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 374 | dependencies = [ 375 | "rand_core 0.3.1", 376 | ] 377 | 378 | [[package]] 379 | name = "redox_syscall" 380 | version = "0.1.57" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 383 | 384 | [[package]] 385 | name = "remove_dir_all" 386 | version = "0.5.3" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 389 | dependencies = [ 390 | "winapi", 391 | ] 392 | 393 | [[package]] 394 | name = "rmp" 395 | version = "0.8.9" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "0f10b46df14cf1ee1ac7baa4d2fbc2c52c0622a4b82fa8740e37bc452ac0184f" 398 | dependencies = [ 399 | "byteorder", 400 | "num-traits", 401 | ] 402 | 403 | [[package]] 404 | name = "rmp-serde" 405 | version = "0.14.4" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "4ce7d70c926fe472aed493b902010bccc17fa9f7284145cb8772fd22fdb052d8" 408 | dependencies = [ 409 | "byteorder", 410 | "rmp", 411 | "serde", 412 | ] 413 | 414 | [[package]] 415 | name = "runner" 416 | version = "0.1.0" 417 | dependencies = [ 418 | "anyhow", 419 | "imgcompress", 420 | "libc", 421 | "loopdev", 422 | "nix", 423 | "rmp-serde", 424 | "static_assertions", 425 | "structopt", 426 | "sys-mount", 427 | "tempfile", 428 | ] 429 | 430 | [[package]] 431 | name = "serde" 432 | version = "1.0.116" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5" 435 | dependencies = [ 436 | "serde_derive", 437 | ] 438 | 439 | [[package]] 440 | name = "serde_derive" 441 | version = "1.0.116" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8" 444 | dependencies = [ 445 | "proc-macro2", 446 | "quote", 447 | "syn", 448 | ] 449 | 450 | [[package]] 451 | name = "static_assertions" 452 | version = "1.1.0" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 455 | 456 | [[package]] 457 | name = "strsim" 458 | version = "0.8.0" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 461 | 462 | [[package]] 463 | name = "structopt" 464 | version = "0.3.18" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "a33f6461027d7f08a13715659b2948e1602c31a3756aeae9378bfe7518c72e82" 467 | dependencies = [ 468 | "clap", 469 | "lazy_static", 470 | "structopt-derive", 471 | ] 472 | 473 | [[package]] 474 | name = "structopt-derive" 475 | version = "0.4.11" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "c92e775028122a4b3dd55d58f14fc5120289c69bee99df1d117ae30f84b225c9" 478 | dependencies = [ 479 | "heck", 480 | "proc-macro-error", 481 | "proc-macro2", 482 | "quote", 483 | "syn", 484 | ] 485 | 486 | [[package]] 487 | name = "syn" 488 | version = "1.0.41" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "6690e3e9f692504b941dc6c3b188fd28df054f7fb8469ab40680df52fdcc842b" 491 | dependencies = [ 492 | "proc-macro2", 493 | "quote", 494 | "unicode-xid", 495 | ] 496 | 497 | [[package]] 498 | name = "sys-mount" 499 | version = "1.2.1" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "62f5703caf67c45ad3450104001b4620a605e9def0cef13dde3c9add23f73cee" 502 | dependencies = [ 503 | "bitflags", 504 | "libc", 505 | "loopdev", 506 | ] 507 | 508 | [[package]] 509 | name = "tempfile" 510 | version = "3.1.0" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" 513 | dependencies = [ 514 | "cfg-if", 515 | "libc", 516 | "rand 0.7.3", 517 | "redox_syscall", 518 | "remove_dir_all", 519 | "winapi", 520 | ] 521 | 522 | [[package]] 523 | name = "textwrap" 524 | version = "0.11.0" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 527 | dependencies = [ 528 | "unicode-width", 529 | ] 530 | 531 | [[package]] 532 | name = "unicode-segmentation" 533 | version = "1.6.0" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 536 | 537 | [[package]] 538 | name = "unicode-width" 539 | version = "0.1.8" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 542 | 543 | [[package]] 544 | name = "unicode-xid" 545 | version = "0.2.1" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 548 | 549 | [[package]] 550 | name = "vec_map" 551 | version = "0.8.2" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 554 | 555 | [[package]] 556 | name = "version_check" 557 | version = "0.9.2" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" 560 | 561 | [[package]] 562 | name = "wasi" 563 | version = "0.9.0+wasi-snapshot-preview1" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 566 | 567 | [[package]] 568 | name = "winapi" 569 | version = "0.3.9" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 572 | dependencies = [ 573 | "winapi-i686-pc-windows-gnu", 574 | "winapi-x86_64-pc-windows-gnu", 575 | ] 576 | 577 | [[package]] 578 | name = "winapi-i686-pc-windows-gnu" 579 | version = "0.4.0" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 582 | 583 | [[package]] 584 | name = "winapi-x86_64-pc-windows-gnu" 585 | version = "0.4.0" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 588 | 589 | [[package]] 590 | name = "zstd" 591 | version = "0.5.3+zstd.1.4.5" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "01b32eaf771efa709e8308605bbf9319bf485dc1503179ec0469b611937c0cd8" 594 | dependencies = [ 595 | "zstd-safe", 596 | ] 597 | 598 | [[package]] 599 | name = "zstd-safe" 600 | version = "2.0.5+zstd.1.4.5" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "1cfb642e0d27f64729a639c52db457e0ae906e7bc6f5fe8f5c453230400f1055" 603 | dependencies = [ 604 | "libc", 605 | "zstd-sys", 606 | ] 607 | 608 | [[package]] 609 | name = "zstd-sys" 610 | version = "1.4.17+zstd.1.4.5" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "b89249644df056b522696b1bb9e7c18c87e8ffa3e2f0dc3b0155875d6498f01b" 613 | dependencies = [ 614 | "cc", 615 | "glob", 616 | "itertools", 617 | "libc", 618 | ] 619 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "src/runner", 4 | "src/imgcompress", 5 | "src/mutator", 6 | ] 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:edge as kernel 2 | 3 | ARG KERNEL_REPO=https://github.com/torvalds/linux.git 4 | ARG KERNEL_BRANCH=master 5 | 6 | ENV KERNEL_REPO=${KERNEL_REPO} 7 | ENV KERNEL_BRANCH=${KERNEL_BRANCH} 8 | 9 | RUN apk update && apk add \ 10 | bash \ 11 | bison \ 12 | build-base \ 13 | diffutils \ 14 | elfutils-dev \ 15 | findutils \ 16 | flex \ 17 | git \ 18 | gzip \ 19 | linux-headers \ 20 | perl \ 21 | python3 \ 22 | openssl \ 23 | openssl-dev \ 24 | xz 25 | 26 | WORKDIR / 27 | 28 | RUN git clone --depth 1 ${KERNEL_REPO} linux --branch ${KERNEL_BRANCH} 29 | WORKDIR linux 30 | 31 | COPY scripts/docker/config_kernel.sh config_kernel.sh 32 | COPY configs/archlinux.config .config 33 | RUN chmod +x config_kernel.sh 34 | RUN ./config_kernel.sh 35 | 36 | RUN make bzImage -j$(nproc) 37 | 38 | # Second build stage builds statically linked btrfs-fuzz software components 39 | FROM rust:alpine as btrfsfuzz 40 | 41 | RUN apk update && apk add musl-dev 42 | 43 | WORKDIR / 44 | RUN mkdir btrfs-fuzz 45 | WORKDIR btrfs-fuzz 46 | COPY Cargo.toml Cargo.lock ./ 47 | RUN mkdir src 48 | COPY src src 49 | RUN cargo update 50 | RUN cargo build --release -p runner 51 | 52 | # Third stage builds dynamically linked btrfs-fuzz components 53 | FROM rust:latest as btrfsfuzz-dy 54 | 55 | WORKDIR / 56 | RUN mkdir btrfs-fuzz 57 | WORKDIR btrfs-fuzz 58 | COPY Cargo.toml Cargo.lock ./ 59 | RUN mkdir src 60 | COPY src src 61 | RUN cargo update 62 | RUN cargo build --release -p mutator 63 | 64 | # Final stage build copies over binaries from build stages and only installs 65 | # runtime components. 66 | FROM aflplusplus/aflplusplus:latest 67 | 68 | ARG DEBIAN_FRONTEND=noninteractive 69 | RUN apt-get update && apt-get install -y \ 70 | btrfs-progs \ 71 | busybox \ 72 | kmod \ 73 | linux-tools-generic \ 74 | less \ 75 | strace \ 76 | qemu-system-x86 77 | 78 | WORKDIR / 79 | RUN mkdir btrfs-fuzz 80 | WORKDIR btrfs-fuzz 81 | 82 | COPY scripts/docker/entry.sh entry.sh 83 | RUN chmod +x entry.sh 84 | 85 | RUN git clone https://github.com/amluto/virtme.git 86 | 87 | COPY --from=kernel /linux/arch/x86/boot/bzImage . 88 | COPY --from=kernel /linux/vmlinux . 89 | COPY --from=btrfsfuzz /btrfs-fuzz/target/release/runner . 90 | COPY --from=btrfsfuzz-dy /btrfs-fuzz/target/release/libmutator.so . 91 | 92 | ENTRYPOINT ["./entry.sh"] 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # btrfs-fuzz 2 | 3 | [![][0]][1] 4 | [![][3]][4] 5 | 6 | `btrfs-fuzz` is an unsupervised coverage guided-fuzzer tailored for [btrfs][2]. 7 | 8 | ## Dependencies 9 | 10 | `btrfs-fuzz` is mostly self-contained inside a docker image. The only things you 11 | need on your host are: 12 | 13 | * `btrfs-progs` 14 | * [`podman`][5] 15 | * python3 16 | * QEMU 17 | * Rust toolchain 18 | 19 | ## Quickstart 20 | 21 | ```shell 22 | $ git clone https://github.com/danobi/btrfs-fuzz.git 23 | $ cd btrfs-fuzz 24 | $ ./x.py build 25 | $ ./x.py seed 26 | $ ./x.py run 27 | ``` 28 | 29 | ## x.py 30 | 31 | `x.py` is the "Makefile" for this project. See `x.py --help` for full options. 32 | 33 | ## Trophies 34 | 35 | * [Kernel divide-by-zero][6] 36 | * [Kernel stack scribbling][7] 37 | 38 | 39 | [0]: https://img.shields.io/docker/cloud/build/dxuu/btrfs-fuzz 40 | [1]: https://hub.docker.com/r/dxuu/btrfs-fuzz 41 | [2]: https://en.wikipedia.org/wiki/Btrfs 42 | [3]: https://github.com/danobi/btrfs-fuzz/workflows/Rust/badge.svg 43 | [4]: https://github.com/danobi/btrfs-fuzz/actions?query=workflow%3ARust 44 | [5]: https://podman.io/ 45 | [6]: https://lore.kernel.org/linux-btrfs/20201020173745.227665-1-dxu@dxuuu.xyz/ 46 | [7]: https://lore.kernel.org/linux-btrfs/0e869ff2f4ace0acb4bcfcd9a6fcf95d95b1d85a.1605232441.git.dxu@dxuuu.xyz/ 47 | -------------------------------------------------------------------------------- /corpus/bko-154021-invalid-drop-level.raw.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danobi/btrfs-fuzz/3054faf40fae133f66bc99f53a94953af5a1e7b9/corpus/bko-154021-invalid-drop-level.raw.zst -------------------------------------------------------------------------------- /corpus/bko-154961-heap-overflow-chunk-items.raw.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danobi/btrfs-fuzz/3054faf40fae133f66bc99f53a94953af5a1e7b9/corpus/bko-154961-heap-overflow-chunk-items.raw.zst -------------------------------------------------------------------------------- /corpus/bko-155181-bad-backref.raw.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danobi/btrfs-fuzz/3054faf40fae133f66bc99f53a94953af5a1e7b9/corpus/bko-155181-bad-backref.raw.zst -------------------------------------------------------------------------------- /corpus/bko-156731.raw.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danobi/btrfs-fuzz/3054faf40fae133f66bc99f53a94953af5a1e7b9/corpus/bko-156731.raw.zst -------------------------------------------------------------------------------- /corpus/bko-161811.raw.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danobi/btrfs-fuzz/3054faf40fae133f66bc99f53a94953af5a1e7b9/corpus/bko-161811.raw.zst -------------------------------------------------------------------------------- /corpus/bko-200403.raw.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danobi/btrfs-fuzz/3054faf40fae133f66bc99f53a94953af5a1e7b9/corpus/bko-200403.raw.zst -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture notes 2 | 3 | `btrfs-fuzz` runs tests in a VM so we can detect and respond to kernel panics. 4 | `manager` is responsible for managing the VM and responding appropriately to 5 | panics. `AFL++` generates and mutates btrfs images. `runner` runs/tests each 6 | generated image inside the VM. All tests are run in the same process for better 7 | fuzzing performance. This is ok b/c btrfs has minimal shared state between 8 | mounts. `runner` is also responsible for collecting kernel code coverage and 9 | writing the results to a shared memory buffer that AFL++ reads. FS images are 10 | also compressed to improve speed. Image fixups after decompression are 11 | necessary to get deeper code path penetration (so the code doesn't bail early 12 | when it sees a mismatched checksum or an invalid superblock magic). 13 | 14 | ### Diagram 15 | 16 | The following diagram shows two test case executions: one uninteresting 17 | execution and one panic. 18 | 19 | ![](mscgen/architecture.png) 20 | -------------------------------------------------------------------------------- /docs/mscgen/architecture.mscgen: -------------------------------------------------------------------------------- 1 | // mscgen -T png architecture.mscgen 2 | 3 | msc { 4 | a [label="manager"],b [label="AFL++"], c [label="runner"]; 5 | 6 | a=>b [label="start QEMU"]; 7 | b=>c [label="start forkserver"]; 8 | b=>b [label="mutate image"]; 9 | b=>c [label="send image"]; 10 | c=>c [label="decompress image"]; 11 | c=>c [label="fixup image"]; 12 | c=>c [label="mount/umount"]; 13 | c=>b [label="report results"]; 14 | b=>b [label="mutate image"]; 15 | b=>c [label="send image"]; 16 | c=>c [label="decompress image"]; 17 | c=>c [label="fixup image"]; 18 | c=>c [label="mount/umount"]; 19 | --- [label="kernel panic"]; 20 | a=>a [label="parse crash log"]; 21 | a=>b [label="start QEMU"]; 22 | } 23 | -------------------------------------------------------------------------------- /docs/mscgen/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danobi/btrfs-fuzz/3054faf40fae133f66bc99f53a94953af5a1e7b9/docs/mscgen/architecture.png -------------------------------------------------------------------------------- /scripts/contiguous_zeroes.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | # 3 | # Calculates number of contiguous zeroes (controlled by minimum WIDTH zeroes) 4 | # in a binary file. This script is useful to determine if 0-encoding a 5 | # filesystem image is useful. 6 | 7 | import sys 8 | import enum 9 | 10 | 11 | def bytes_from_file(filename, chunksize=(16 << 10)): 12 | with open(filename, "rb") as f: 13 | while True: 14 | chunk = f.read(chunksize) 15 | if chunk: 16 | for b in chunk: 17 | yield b 18 | else: 19 | break 20 | 21 | 22 | def main(): 23 | if len(sys.argv) < 2: 24 | print("Usage: ./contiguous_zeroes.py FILE [WIDTH]=16") 25 | sys.exit(1) 26 | 27 | width = 16 28 | if len(sys.argv) == 3: 29 | width = int(sys.argv[2]) 30 | 31 | count = 0 32 | chunks = 0 33 | total_bytes = 0 34 | state = 0 35 | 36 | for b in bytes_from_file(sys.argv[1]): 37 | total_bytes += 1 38 | 39 | if b == 0: 40 | state += 1 41 | if state < width: 42 | pass 43 | elif state == width: 44 | count += width 45 | chunks += 1 46 | else: # state > width 47 | count += 1 48 | else: 49 | state = 0 50 | 51 | print(f"{count:<12}eligible zeros") 52 | print(f"{chunks:<12}chunks of zeroes") 53 | print(f"{total_bytes:<12}total bytes") 54 | print("------------------------------") 55 | 56 | metadata_bytes = 4 * chunks 57 | compressed_size = total_bytes - count + metadata_bytes 58 | compression_ratio = total_bytes / compressed_size 59 | 60 | print(f"{compressed_size >> 10:.3f}KB compressed size") 61 | print(f"{compression_ratio:.3f} compression ratio") 62 | 63 | 64 | if __name__ == "__main__": 65 | main() 66 | -------------------------------------------------------------------------------- /scripts/docker/config_kernel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Configure kernel for btrfs fuzzing 4 | # 5 | # Run this inside kernel source tree root. 6 | 7 | set -eu 8 | 9 | # Enable CONFIG_KCOV but don't instrument everything. We don't want any 10 | # noise from anything outside btrfs 11 | ./scripts/config \ 12 | -e KCOV \ 13 | -d KCOV_INSTRUMENT_ALL \ 14 | -e KCOV_ENABLE_COMPARISONS 15 | 16 | # Enable KCOV instrumentation for btrfs 17 | find fs/btrfs -name Makefile \ 18 | | xargs -L1 -I {} \ 19 | bash -c 'grep -q KCOV_INSTRUMENT {} || echo "KCOV_INSTRUMENT := y" >> {}' 20 | 21 | # Apply syzkaller recommended configs. See: 22 | # https://github.com/google/syzkaller/blob/master/docs/linux/kernel_configs.md 23 | ./scripts/config \ 24 | -e DEBUG_FS \ 25 | -e DEBUG_INFO \ 26 | -e KALLSYMS \ 27 | -e KALLSYMS_ALL \ 28 | -e NAMESPACES \ 29 | -e UTS_NS \ 30 | -e IPC_NS \ 31 | -e PID_NS \ 32 | -e NET_NS \ 33 | -e USER_NS \ 34 | -e CGROUP_PIDS \ 35 | -e MEMCG \ 36 | -e CONFIGFS_FS \ 37 | -e SECURITYFS \ 38 | -e KASAN \ 39 | -e KASAN_INLINE \ 40 | -e WARNING \ 41 | -e FAULT_INJECTION \ 42 | -e FAULT_INJECTION_DEBUG_FS \ 43 | -e FAILSLAB \ 44 | -e FAIL_PAGE_ALLOC \ 45 | -e FAIL_MAKE_REQUEST \ 46 | -e FAIL_IO_TIMEOUT \ 47 | -e FAIL_FUTEX \ 48 | -e LOCKDEP \ 49 | -e PROVE_LOCKING \ 50 | -e DEBUG_ATOMIC_SLEEP \ 51 | -e PROVE_RCU \ 52 | -e DEBUG_VM \ 53 | -e REFCOUNT_FULL \ 54 | -e FORTIFY_SOURCE \ 55 | -e HARDENED_USERCOPY \ 56 | -e LOCKUP_DETECTOR \ 57 | -e SOFTLOCKUP_DETECTOR \ 58 | -e HARDLOCKUP_DETECTOR \ 59 | -e BOOTPARAM_HARDLOCKUP_PANIC \ 60 | -e DETECT_HUNG_TASK \ 61 | -e WQ_WATCHDOG \ 62 | --set-val DEFAULT_HUNG_TASK_TIMEOUT 140 \ 63 | --set-val RCU_CPU_STALL_TIMEOUT 100 \ 64 | -e UBSAN \ 65 | -d RANDOMIZE_BASE 66 | 67 | # Apply virtme required configs 68 | ./scripts/config \ 69 | -e VIRTIO \ 70 | -e VIRTIO_PCI \ 71 | -e VIRTIO_MMIO \ 72 | -e NET \ 73 | -e NET_CORE \ 74 | -e NETDEVICES \ 75 | -e NETWORK_FILESYSTEMS \ 76 | -e INET \ 77 | -e NET_9P \ 78 | -e NET_9P_VIRTIO \ 79 | -e 9P_FS \ 80 | -e VIRTIO_NET \ 81 | -e VIRTIO_CONSOLE \ 82 | -e DEVTMPFS \ 83 | -e SCSI_VIRTIO \ 84 | -e BINFMT_SCRIPT \ 85 | -e TMPFS \ 86 | -e UNIX \ 87 | -e TTY \ 88 | -e VT \ 89 | -e UNIX98_PTYS \ 90 | -e WATCHDOG \ 91 | -e WATCHDOG_CORE \ 92 | -e I6300ESB_WDT \ 93 | -e BLOCK \ 94 | -e SCSI_gLOWLEVEL \ 95 | -e SCSI \ 96 | -e SCSI_VIRTIO \ 97 | -e BLK_DEV_SD \ 98 | -e VIRTIO_BALLOON \ 99 | -d CMDLINE_OVERRIDE \ 100 | -d UEVENT_HELPER \ 101 | -d EMBEDDED \ 102 | -d EXPERT \ 103 | -d MODULE_SIG_FORCE 104 | 105 | # Build btrfs module in-kernel 106 | ./scripts/config -e BTRFS_FS 107 | 108 | # Build loop module in-kernel 109 | ./scripts/config -e BLK_DEV_LOOP 110 | 111 | # Disable BTF to reduce build dependencies 112 | ./scripts/config -d DEBUG_INFO_BTF 113 | 114 | # Setting previous configs may result in more sub options being available, 115 | # so set all the new available ones to default as well. 116 | make olddefconfig 117 | -------------------------------------------------------------------------------- /scripts/docker/entry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Entry point for container 4 | 5 | set -eu 6 | 7 | cd /btrfs-fuzz 8 | ./virtme/virtme-run --kimg bzImage --rw --pwd --memory 1024M "$@" 9 | -------------------------------------------------------------------------------- /scripts/validate_crashes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Script to see if discovered crashes can reproduce. 4 | # 5 | # Env vars: 6 | # REMOTE: use `x.py --remote` 7 | # 8 | # Example: 9 | # REMOTE=1 ./validate_crashes.sh ./_state/output/crashes 10 | # 11 | 12 | set -eu 13 | 14 | if [[ "$#" != 1 ]]; then 15 | echo 'Usage: validate_crashes.sh DIR' >> /dev/stderr 16 | exit 1 17 | fi 18 | 19 | if [[ -v REMOTE ]]; then 20 | cmd="./x.py --remote" 21 | else 22 | cmd="./x.py" 23 | fi 24 | 25 | for f in "$1"/*; do 26 | echo "Testing ${f}" 27 | output=$($cmd repro --exit "$f") 28 | if echo "$output" | grep -q -e FAILURE -e "RIP:" -e "Call Trace:"; then 29 | echo -e "\tReproduced failure for ${f}" 30 | fi 31 | done 32 | -------------------------------------------------------------------------------- /src/imgcompress/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "imgcompress" 3 | version = "0.1.0" 4 | authors = ["Daniel Xu "] 5 | edition = "2018" 6 | 7 | [[bin]] 8 | name = "imgcompress" 9 | path = "src/bin/main.rs" 10 | 11 | [dependencies] 12 | anyhow = "1.0" 13 | crc32c = "0.5" 14 | rmp-serde = "0.14" 15 | serde = { version = "1.0", features = ["derive"] } 16 | structopt = "0.3" 17 | zstd = "0.5" 18 | 19 | [dev-dependencies] 20 | tempfile = "3.1" 21 | -------------------------------------------------------------------------------- /src/imgcompress/src/bin/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs::OpenOptions; 2 | use std::io::{Read, Write}; 3 | use std::path::PathBuf; 4 | 5 | use anyhow::Result; 6 | use rmp_serde::{decode::from_read_ref, Serializer}; 7 | use serde::Serialize; 8 | use structopt::StructOpt; 9 | 10 | #[derive(Debug, StructOpt)] 11 | #[structopt(name = "imgcompress", about = "Compress a btrfs image")] 12 | struct Opt { 13 | #[structopt(subcommand)] 14 | cmd: Command, 15 | } 16 | 17 | #[derive(Debug, StructOpt)] 18 | enum Command { 19 | /// Compress a btrfs image 20 | Compress { 21 | #[structopt(parse(from_os_str))] 22 | input: PathBuf, 23 | #[structopt(parse(from_os_str))] 24 | output: PathBuf, 25 | }, 26 | /// Decompress an imgcompress'd btrfs image 27 | Decompress { 28 | #[structopt(parse(from_os_str))] 29 | input: PathBuf, 30 | #[structopt(parse(from_os_str))] 31 | output: PathBuf, 32 | }, 33 | } 34 | 35 | fn compress(input: PathBuf, output: PathBuf) -> Result<()> { 36 | let mut input = OpenOptions::new().read(true).open(input)?; 37 | 38 | let mut input_image = Vec::new(); 39 | input.read_to_end(&mut input_image)?; 40 | let compressed_image = imgcompress::compress(&input_image)?; 41 | 42 | let output = OpenOptions::new() 43 | .create(true) 44 | .write(true) 45 | .truncate(true) 46 | .open(output)?; 47 | compressed_image.serialize(&mut Serializer::new(output))?; 48 | 49 | Ok(()) 50 | } 51 | 52 | fn decompress(input: PathBuf, output: PathBuf) -> Result<()> { 53 | let mut input = OpenOptions::new().read(true).open(input)?; 54 | 55 | let mut serialized_input = Vec::new(); 56 | input.read_to_end(&mut serialized_input)?; 57 | let deserialized_input: imgcompress::CompressedBtrfsImage = from_read_ref(&serialized_input)?; 58 | let decompressed_image = imgcompress::decompress(&deserialized_input)?; 59 | 60 | let mut output = OpenOptions::new() 61 | .create(true) 62 | .write(true) 63 | .truncate(true) 64 | .open(output)?; 65 | output.write_all(&decompressed_image)?; 66 | 67 | Ok(()) 68 | } 69 | 70 | fn main() -> Result<()> { 71 | let opts = Opt::from_args(); 72 | 73 | match opts.cmd { 74 | Command::Compress { input, output } => compress(input, output), 75 | Command::Decompress { input, output } => decompress(input, output), 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/imgcompress/src/btrfs.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::mem::size_of; 3 | 4 | use anyhow::{anyhow, bail, Context, Result}; 5 | use zstd::stream::encode_all; 6 | 7 | use crate::chunk_tree::{ChunkTreeCache, ChunkTreeKey, ChunkTreeValue}; 8 | use crate::structs::*; 9 | use crate::tree; 10 | use crate::CompressedBtrfsImage; 11 | 12 | /// Helper struct to compress a valid btrfs image. 13 | /// 14 | /// To annotate more of the image for fuzzing, just add more entries to 15 | /// `CompressedBtrfsImage::metadata` and `CompressedBtrfsImage::data` as the trees are walked. 16 | pub struct Btrfs<'a> { 17 | image: &'a [u8], 18 | superblock: &'a BtrfsSuperblock, 19 | chunk_tree_cache: ChunkTreeCache, 20 | } 21 | 22 | impl<'a> Btrfs<'a> { 23 | /// Constructor processes superblock and chunk tree. The chunk tree is used to map logical to 24 | /// physical addresses so it needs to be bootstrapped ASAP. 25 | pub fn new(img: &'a [u8]) -> Result { 26 | // Read superblock 27 | let superblock = 28 | parse_superblock(img).with_context(|| "Failed to parse superblock".to_string())?; 29 | 30 | // Bootstraup chunk tree 31 | let mut chunk_tree_cache = bootstrap_chunk_tree(superblock) 32 | .with_context(|| "Failed to boostrap chunk tree".to_string())?; 33 | 34 | // Read root chunk tree node 35 | let chunk_root = read_root_node(img, superblock.chunk_root, &chunk_tree_cache) 36 | .with_context(|| "Failed to read chunk tree root".to_string())?; 37 | 38 | // Read rest of chunk tree 39 | read_chunk_tree(img, &chunk_root, &mut chunk_tree_cache, &superblock) 40 | .with_context(|| "Failed to read chunk tree".to_string())?; 41 | 42 | Ok(Self { 43 | image: img, 44 | superblock, 45 | chunk_tree_cache, 46 | }) 47 | } 48 | 49 | /// Compress the image 50 | pub fn compress(&self) -> Result { 51 | let mut compressed = CompressedBtrfsImage::default(); 52 | 53 | // Compress and save base image 54 | compressed.base = encode_all(self.image, 0)?; 55 | 56 | // Save node size b/c the value in the superblock could get fuzzed to something else 57 | compressed.node_size = self.superblock.node_size.try_into()?; 58 | 59 | // Save all superblocks 60 | self.save_superblocks(&mut compressed)?; 61 | 62 | // Parse everything in the root tree 63 | self.parse_root_tree(&mut compressed) 64 | .with_context(|| "Failed to parse root tree".to_string())?; 65 | 66 | // The log tree seems to be maintained separately from the root tree, so parse everything 67 | // in there separately 68 | if self.superblock.log_root != 0 { 69 | self.parse_tree(self.superblock.log_root, &mut compressed)?; 70 | } 71 | 72 | Ok(compressed) 73 | } 74 | 75 | fn save_superblocks(&self, compressed: &mut CompressedBtrfsImage) -> Result<()> { 76 | // First superblock has to exist 77 | compressed.mark_as_metadata( 78 | BTRFS_SUPERBLOCK_OFFSET.try_into()?, 79 | &self.image[BTRFS_SUPERBLOCK_OFFSET..(BTRFS_SUPERBLOCK_OFFSET + BTRFS_SUPERBLOCK_SIZE)], 80 | true, 81 | )?; 82 | 83 | for offset in &[BTRFS_SUPERBLOCK_OFFSET2, BTRFS_SUPERBLOCK_OFFSET3] { 84 | let offset = *offset; 85 | if self.image.len() >= (offset + BTRFS_SUPERBLOCK_SIZE) { 86 | compressed.mark_as_metadata( 87 | offset.try_into()?, 88 | &self.image[offset..(offset + BTRFS_SUPERBLOCK_SIZE)], 89 | true, 90 | )?; 91 | } 92 | } 93 | 94 | Ok(()) 95 | } 96 | 97 | fn parse_root_tree(&self, compressed: &mut CompressedBtrfsImage) -> Result<()> { 98 | let physical = self 99 | .chunk_tree_cache 100 | .offset(self.superblock.root) 101 | .ok_or_else(|| anyhow!("Root tree root logical addr not mapped"))?; 102 | let node = read_root_node(self.image, self.superblock.root, &self.chunk_tree_cache) 103 | .with_context(|| "Failed to read root tree root".to_string())?; 104 | 105 | let header = tree::parse_btrfs_header(node)?; 106 | 107 | if header.level == 0 { 108 | // Store the header b/c it's metadata 109 | let metadata_size = 110 | size_of::() + (header.nritems as usize * size_of::()); 111 | compressed.mark_as_metadata(physical, &node[..metadata_size], true)?; 112 | 113 | // Now recursively walk the tree 114 | let items = tree::parse_btrfs_leaf(node)?; 115 | for item in items.iter().rev() { 116 | if item.key.ty != BTRFS_ROOT_ITEM_KEY { 117 | continue; 118 | } 119 | 120 | let root_item = unsafe { 121 | &*(node 122 | .as_ptr() 123 | .add(std::mem::size_of::() + item.offset as usize) 124 | as *const BtrfsRootItem) 125 | }; 126 | 127 | self.parse_tree(root_item.bytenr, compressed)?; 128 | } 129 | } else { 130 | bail!("The root tree root should only contain one level") 131 | } 132 | 133 | Ok(()) 134 | } 135 | 136 | fn parse_tree(&self, logical: u64, compressed: &mut CompressedBtrfsImage) -> Result<()> { 137 | let physical = self 138 | .chunk_tree_cache 139 | .offset(logical) 140 | .ok_or_else(|| anyhow!("Node logical addr={} not mapped", logical))?; 141 | let node = read_root_node(self.image, logical, &self.chunk_tree_cache) 142 | .with_context(|| "Failed to read node".to_string())?; 143 | 144 | // Store the header b/c it's metadata 145 | let header = tree::parse_btrfs_header(node)?; 146 | let mut metadata_size = size_of::(); 147 | 148 | if header.level == 0 { 149 | // First annotate header 150 | metadata_size += header.nritems as usize * size_of::(); 151 | compressed.mark_as_metadata(physical, &node[..metadata_size], true)?; 152 | 153 | // Now annotate payloads 154 | // 155 | // First find the "left most" payload item. Leaf payloads grow from right 156 | // to left while `BtrfsItem` structs grow left to right. 157 | let mut lowest_offset = None; 158 | for item in tree::parse_btrfs_leaf(node)? { 159 | if lowest_offset.is_none() || item.offset < lowest_offset.unwrap() { 160 | lowest_offset = Some(item.offset); 161 | } 162 | } 163 | 164 | if let Some(lowest) = lowest_offset { 165 | let physical: usize = physical.try_into()?; 166 | let lowest: usize = lowest.try_into()?; 167 | let node_size: usize = self.superblock.node_size.try_into()?; 168 | let start: usize = physical + size_of::() + lowest; 169 | let end: usize = physical + node_size; 170 | compressed.mark_as_metadata(start.try_into()?, &self.image[start..end], false)?; 171 | } 172 | } else { 173 | // We're at an internal node: there's no payload 174 | metadata_size += header.nritems as usize * size_of::(); 175 | compressed.mark_as_metadata(physical, &node[..metadata_size], true)?; 176 | 177 | // Recursively visit children 178 | let ptrs = tree::parse_btrfs_node(node)?; 179 | for ptr in ptrs { 180 | self.parse_tree(ptr.blockptr, compressed)?; 181 | } 182 | } 183 | 184 | Ok(()) 185 | } 186 | } 187 | 188 | fn parse_superblock(img: &[u8]) -> Result<&BtrfsSuperblock> { 189 | if BTRFS_SUPERBLOCK_OFFSET + BTRFS_SUPERBLOCK_SIZE > img.len() { 190 | bail!("Image to small to contain superblock"); 191 | } 192 | 193 | let superblock_ptr = img[BTRFS_SUPERBLOCK_OFFSET..].as_ptr() as *const BtrfsSuperblock; 194 | let superblock = unsafe { &*superblock_ptr }; 195 | 196 | if superblock.magic != BTRFS_SUPERBLOCK_MAGIC { 197 | bail!("Superblock magic is wrong"); 198 | } 199 | 200 | Ok(superblock) 201 | } 202 | 203 | fn bootstrap_chunk_tree(superblock: &BtrfsSuperblock) -> Result { 204 | let array_size = superblock.sys_chunk_array_size as usize; 205 | let mut offset: usize = 0; 206 | let mut chunk_tree_cache = ChunkTreeCache::default(); 207 | 208 | while offset < array_size { 209 | let key_size = std::mem::size_of::(); 210 | if offset + key_size > array_size as usize { 211 | bail!("Short key read"); 212 | } 213 | 214 | let key_slice = &superblock.sys_chunk_array[offset..]; 215 | let key = unsafe { &*(key_slice.as_ptr() as *const BtrfsKey) }; 216 | if key.ty != BTRFS_CHUNK_ITEM_KEY { 217 | bail!( 218 | "Unknown item type={} in sys_array at offset={}", 219 | key.ty, 220 | offset 221 | ); 222 | } 223 | offset += key_size; 224 | 225 | if offset + std::mem::size_of::() > array_size { 226 | bail!("short chunk item read"); 227 | } 228 | 229 | let chunk_slice = &superblock.sys_chunk_array[offset..]; 230 | let chunk = unsafe { &*(chunk_slice.as_ptr() as *const BtrfsChunk) }; 231 | if chunk.num_stripes == 0 { 232 | bail!("num_stripes cannot be 0"); 233 | } 234 | 235 | // To keep things simple, we'll only process 1 stripe, as stripes should have 236 | // identical content. The device the stripe is on will be the device passed in 237 | // via cmd line args. 238 | let num_stripes = chunk.num_stripes; // copy to prevent unaligned access 239 | if num_stripes != 1 { 240 | println!( 241 | "Warning: {} stripes detected but only processing 1", 242 | num_stripes 243 | ); 244 | } 245 | 246 | // Add chunk to cache if not already in cache 247 | let logical = key.offset; 248 | if chunk_tree_cache.offset(logical).is_none() { 249 | chunk_tree_cache.insert( 250 | ChunkTreeKey { 251 | start: logical, 252 | size: chunk.length, 253 | }, 254 | ChunkTreeValue { 255 | offset: chunk.stripe.offset, 256 | }, 257 | ); 258 | } 259 | 260 | // Despite only processing one stripe, we need to be careful to skip over the 261 | // entire chunk item. 262 | let chunk_item_size = std::mem::size_of::() 263 | + (std::mem::size_of::() * (chunk.num_stripes as usize - 1)); 264 | if offset + chunk_item_size > array_size { 265 | bail!("short chunk item + stripe read"); 266 | } 267 | offset += chunk_item_size; 268 | } 269 | 270 | Ok(chunk_tree_cache) 271 | } 272 | 273 | fn read_root_node<'a>(img: &'a [u8], logical: u64, cache: &ChunkTreeCache) -> Result<&'a [u8]> { 274 | let size: usize = cache 275 | .mapping_kv(logical) 276 | .ok_or_else(|| anyhow!("Root node logical addr not mapped"))? 277 | .0 278 | .size 279 | .try_into()?; 280 | let physical: usize = cache 281 | .offset(logical) 282 | .ok_or_else(|| anyhow!("Root node logical addr not mapped"))? 283 | .try_into()?; 284 | let end = physical + size; 285 | 286 | Ok(&img[physical..end]) 287 | } 288 | 289 | fn read_chunk_tree( 290 | img: &[u8], 291 | root: &[u8], 292 | chunk_tree_cache: &mut ChunkTreeCache, 293 | superblock: &BtrfsSuperblock, 294 | ) -> Result<()> { 295 | let header = tree::parse_btrfs_header(root)?; 296 | 297 | // Level 0 is leaf node, !0 is internal node 298 | if header.level == 0 { 299 | let items = tree::parse_btrfs_leaf(root)?; 300 | for item in items { 301 | if item.key.ty != BTRFS_CHUNK_ITEM_KEY { 302 | continue; 303 | } 304 | 305 | let chunk = unsafe { 306 | // `item.offset` is offset from data portion of `BtrfsLeaf` where associated 307 | // `BtrfsChunk` starts 308 | &*(root 309 | .as_ptr() 310 | .add(std::mem::size_of::() + item.offset as usize) 311 | as *const BtrfsChunk) 312 | }; 313 | 314 | chunk_tree_cache.insert( 315 | ChunkTreeKey { 316 | start: item.key.offset, 317 | size: chunk.length, 318 | }, 319 | ChunkTreeValue { 320 | offset: chunk.stripe.offset, 321 | }, 322 | ); 323 | } 324 | } else { 325 | let ptrs = tree::parse_btrfs_node(root)?; 326 | for ptr in ptrs { 327 | let physical: usize = chunk_tree_cache 328 | .offset(ptr.blockptr) 329 | .ok_or_else(|| anyhow!("Chunk tree node not mapped"))? 330 | .try_into()?; 331 | let end: usize = physical + superblock.node_size as usize; 332 | 333 | read_chunk_tree(img, &img[physical..end], chunk_tree_cache, superblock)?; 334 | } 335 | } 336 | 337 | Ok(()) 338 | } 339 | -------------------------------------------------------------------------------- /src/imgcompress/src/chunk_tree.rs: -------------------------------------------------------------------------------- 1 | #[derive(Default, Clone, Copy)] 2 | pub struct ChunkTreeKey { 3 | pub start: u64, 4 | pub size: u64, 5 | } 6 | 7 | #[derive(Default, Clone, Copy)] 8 | pub struct ChunkTreeValue { 9 | pub offset: u64, 10 | } 11 | 12 | #[derive(Default)] 13 | pub struct ChunkTreeCache { 14 | inner: Vec<(ChunkTreeKey, ChunkTreeValue)>, 15 | } 16 | 17 | impl ChunkTreeCache { 18 | pub fn insert(&mut self, key: ChunkTreeKey, value: ChunkTreeValue) { 19 | if self.contains_overlapping(&key) { 20 | panic!("overlapping chunk range detected"); 21 | } 22 | 23 | self.inner.push((key, value)); 24 | } 25 | 26 | pub fn mapping_kv(&self, logical: u64) -> Option<(ChunkTreeKey, ChunkTreeValue)> { 27 | for (k, v) in &self.inner { 28 | if logical >= k.start && logical < (k.start + k.size) { 29 | return Some((*k, *v)); 30 | } 31 | } 32 | 33 | None 34 | } 35 | 36 | pub fn offset(&self, logical: u64) -> Option { 37 | if let Some((k, v)) = self.mapping_kv(logical) { 38 | Some(v.offset + (logical - k.start)) 39 | } else { 40 | None 41 | } 42 | } 43 | 44 | fn contains_overlapping(&self, key: &ChunkTreeKey) -> bool { 45 | for (k, _) in &self.inner { 46 | if (key.start > k.start && key.start < (k.start + k.size)) 47 | || ((key.start + key.size) > k.start && (key.start + key.size) < (k.start + k.size)) 48 | { 49 | return true; 50 | } 51 | } 52 | 53 | false 54 | } 55 | } 56 | 57 | #[test] 58 | fn test_ctc_basic() { 59 | let mut tree = ChunkTreeCache::default(); 60 | tree.insert( 61 | ChunkTreeKey { start: 0, size: 5 }, 62 | ChunkTreeValue { offset: 123 }, 63 | ); 64 | tree.insert( 65 | ChunkTreeKey { start: 5, size: 5 }, 66 | ChunkTreeValue { offset: 234 }, 67 | ); 68 | 69 | assert_eq!(tree.offset(0), Some(123)); 70 | assert_eq!(tree.offset(1), Some(124)); 71 | assert_eq!(tree.offset(5), Some(234)); 72 | assert_eq!(tree.offset(6), Some(235)); 73 | assert_eq!(tree.offset(11), None); 74 | } 75 | 76 | #[test] 77 | fn test_ctc_random_order() { 78 | let mut tree = ChunkTreeCache::default(); 79 | tree.insert( 80 | ChunkTreeKey { start: 10, size: 3 }, 81 | ChunkTreeValue { offset: 345 }, 82 | ); 83 | tree.insert( 84 | ChunkTreeKey { start: 25, size: 5 }, 85 | ChunkTreeValue { offset: 456 }, 86 | ); 87 | tree.insert( 88 | ChunkTreeKey { start: 15, size: 5 }, 89 | ChunkTreeValue { offset: 567 }, 90 | ); 91 | tree.insert( 92 | ChunkTreeKey { start: 0, size: 5 }, 93 | ChunkTreeValue { offset: 123 }, 94 | ); 95 | tree.insert( 96 | ChunkTreeKey { start: 5, size: 5 }, 97 | ChunkTreeValue { offset: 234 }, 98 | ); 99 | 100 | assert_eq!(tree.offset(0), Some(123)); 101 | assert_eq!(tree.offset(1), Some(124)); 102 | assert_eq!(tree.offset(5), Some(234)); 103 | assert_eq!(tree.offset(6), Some(235)); 104 | assert_eq!(tree.offset(11), Some(346)); 105 | assert_eq!(tree.offset(14), None); 106 | assert_eq!(tree.offset(18), Some(570)); 107 | assert_eq!(tree.offset(20), None); 108 | assert_eq!(tree.offset(25), Some(456)); 109 | } 110 | 111 | #[test] 112 | #[should_panic] 113 | fn test_ctc_edge_overlap() { 114 | let mut tree = ChunkTreeCache::default(); 115 | tree.insert( 116 | ChunkTreeKey { start: 0, size: 5 }, 117 | ChunkTreeValue { offset: 123 }, 118 | ); 119 | tree.insert( 120 | ChunkTreeKey { start: 4, size: 5 }, 121 | ChunkTreeValue { offset: 234 }, 122 | ); 123 | 124 | // unreached 125 | assert!(false); 126 | } 127 | 128 | #[test] 129 | #[should_panic] 130 | fn test_ctc_inside_overlap() { 131 | let mut tree = ChunkTreeCache::default(); 132 | tree.insert( 133 | ChunkTreeKey { start: 0, size: 5 }, 134 | ChunkTreeValue { offset: 123 }, 135 | ); 136 | tree.insert( 137 | ChunkTreeKey { start: 1, size: 2 }, 138 | ChunkTreeValue { offset: 234 }, 139 | ); 140 | 141 | // unreached 142 | assert!(false); 143 | } 144 | -------------------------------------------------------------------------------- /src/imgcompress/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | #[cfg(test)] 3 | use std::io::{Read, Seek, SeekFrom}; 4 | #[cfg(test)] 5 | use std::process::Command; 6 | 7 | use anyhow::{bail, Result}; 8 | use crc32c::crc32c_append; 9 | use serde::{Deserialize, Serialize}; 10 | #[cfg(test)] 11 | use tempfile::NamedTempFile; 12 | use zstd::stream::decode_all; 13 | 14 | mod btrfs; 15 | mod chunk_tree; 16 | mod structs; 17 | mod tree; 18 | 19 | use btrfs::Btrfs; 20 | use structs::*; 21 | 22 | /// Metadata for a metadata extent. 23 | #[derive(Deserialize, Serialize, Default)] 24 | pub struct MetadataExtent { 25 | /// If true, this metadata extent begins with a csum field that needs fixups 26 | pub needs_csum_fixup: bool, 27 | /// Offset in the decompressed image this extent needs to be written 28 | pub offset: u64, 29 | /// Length of metadata extent 30 | pub size: u64, 31 | } 32 | 33 | #[derive(Deserialize, Serialize, Default)] 34 | pub struct CompressedBtrfsImage { 35 | /// Compressed original image. Fuzzed metadata should be laid on top of the original image. 36 | base: Vec, 37 | /// Each entry in this vector describes a metadata extent in `data`. 38 | /// 39 | /// For example, if `metadata` contained entries [(offset 0, size 10), (offset 50, size 5)], 40 | /// then `data.len()` == 15, where the first 10 bytes would go to offset 0 and the last 5 bytes 41 | /// would go to offset 50. 42 | pub metadata: Vec, 43 | pub data: Vec, 44 | /// Size of each node in the btree. Used to calculate checksum in node headers. 45 | node_size: usize, 46 | } 47 | 48 | impl CompressedBtrfsImage { 49 | /// Mark a range of data as metadata 50 | pub(crate) fn mark_as_metadata( 51 | &mut self, 52 | physical: u64, 53 | metadata: &[u8], 54 | needs_csum_fixup: bool, 55 | ) -> Result<()> { 56 | self.metadata.push(MetadataExtent { 57 | needs_csum_fixup, 58 | offset: physical, 59 | size: metadata.len().try_into()?, 60 | }); 61 | self.data.extend_from_slice(metadata); 62 | 63 | Ok(()) 64 | } 65 | } 66 | 67 | /// Compress a btrfs image 68 | pub fn compress(img: &[u8]) -> Result { 69 | let btrfs = Btrfs::new(img)?; 70 | btrfs.compress() 71 | } 72 | 73 | /// Decompressed an `imgcompress::compress`d btrfs image. 74 | /// 75 | /// Also rewrites superblock magic and checksums to be valid. 76 | pub fn decompress(compressed: &CompressedBtrfsImage) -> Result> { 77 | // Decompress the base image 78 | let mut image: Vec = decode_all(compressed.base.as_slice())?; 79 | 80 | // Now overwrite `image` with the metadata placed at their original offsets 81 | let mut data_idx = 0; 82 | for metadata in &compressed.metadata { 83 | let offset: usize = metadata.offset.try_into()?; 84 | let size: usize = metadata.size.try_into()?; 85 | 86 | let _: Vec<_> = image 87 | .splice( 88 | offset..(offset + size), 89 | compressed.data[data_idx..(data_idx + size)].iter().cloned(), 90 | ) 91 | .collect(); 92 | data_idx += size; 93 | } 94 | 95 | // Fixup the fist superblock 96 | if image.len() < (BTRFS_SUPERBLOCK_OFFSET + BTRFS_SUPERBLOCK_SIZE) { 97 | bail!("Decompressed image too short to contain superblock"); 98 | } else { 99 | let superblock_ptr = image[BTRFS_SUPERBLOCK_OFFSET..].as_mut_ptr() as *mut BtrfsSuperblock; 100 | let superblock = unsafe { &mut *superblock_ptr }; 101 | 102 | // We only support CRC32 for now 103 | if superblock.csum_type != BTRFS_CSUM_TYPE_CRC32 { 104 | let ty: u16 = superblock.csum_type; 105 | println!("Warning: wrong csum type in superblock, type={}", ty); 106 | } 107 | 108 | if superblock.magic != BTRFS_SUPERBLOCK_MAGIC { 109 | superblock.magic = BTRFS_SUPERBLOCK_MAGIC; 110 | } 111 | } 112 | 113 | // Recalculate checksum for each block 114 | for metadata in &compressed.metadata { 115 | if !metadata.needs_csum_fixup { 116 | continue; 117 | } 118 | 119 | let offset: usize = metadata.offset.try_into()?; 120 | 121 | let block_size = if offset == BTRFS_SUPERBLOCK_OFFSET 122 | || offset == BTRFS_SUPERBLOCK_OFFSET2 123 | || offset == BTRFS_SUPERBLOCK_OFFSET3 124 | { 125 | BTRFS_SUPERBLOCK_SIZE 126 | } else { 127 | compressed.node_size 128 | }; 129 | assert_ne!(block_size, 0); 130 | 131 | // Calculate checksum for block 132 | let begin = offset + BTRFS_CSUM_SIZE; 133 | let end = offset + block_size; 134 | let checksum: u32 = crc32c_append(BTRFS_CSUM_CRC32_SEED, &image[begin..end]); 135 | 136 | // Write checksum back into block 137 | // 138 | // NB: a crc32c checksum is only 4 bytes long. We'll leave the other 28 bytes alone. 139 | let _: Vec<_> = image 140 | .splice(offset..(offset + 4), checksum.to_le_bytes().iter().cloned()) 141 | .collect(); 142 | } 143 | 144 | Ok(image) 145 | } 146 | 147 | #[cfg(test)] 148 | fn generate_test_image() -> Vec { 149 | let mut orig = NamedTempFile::new().expect("Failed to create tempfile"); 150 | // mkfs.btrfs needs at least 120 MB to create an image 151 | orig.as_file() 152 | .set_len(120 << 20) 153 | .expect("Failed to increase orig image size"); 154 | // Seek to beginning just in case 155 | orig.as_file_mut() 156 | .seek(SeekFrom::Start(0)) 157 | .expect("Failed to seek to beginning of orig image"); 158 | 159 | // mkfs.brtrfs 160 | let output = Command::new("mkfs.btrfs") 161 | .arg(orig.path()) 162 | .output() 163 | .expect("Failed to run mkfs.btrfs"); 164 | assert!(output.status.success()); 165 | 166 | let mut orig_buffer = Vec::new(); 167 | orig.as_file() 168 | .read_to_end(&mut orig_buffer) 169 | .expect("Failed to read original image"); 170 | 171 | orig_buffer 172 | } 173 | 174 | /// Test that compressing and decompressing an image results in bit-for-bit equality 175 | #[test] 176 | fn test_compress_decompress() { 177 | let orig_buffer = generate_test_image(); 178 | let compressed = compress(&orig_buffer).expect("Failed to compress image"); 179 | let decompressed = decompress(&compressed).expect("Failed to decompress image"); 180 | 181 | assert!(orig_buffer == decompressed); 182 | } 183 | 184 | /// Test that checksums are correctly fixed up if they get corrupted 185 | #[test] 186 | fn test_checksum_fixup() { 187 | let orig_buffer = generate_test_image(); 188 | 189 | // This is pretty pricey -- 120M copy. Hopefully it doesn't cause any issues 190 | let mut corrupted_buffer = orig_buffer.clone(); 191 | let random: Vec = vec![0xDE, 0xAD, 0xBE, 0xEF]; 192 | corrupted_buffer.splice( 193 | BTRFS_SUPERBLOCK_OFFSET..(BTRFS_SUPERBLOCK_OFFSET + 4), 194 | random.iter().cloned(), 195 | ); 196 | 197 | // Now compress and decompress corrupted buffer 198 | let compressed = compress(&corrupted_buffer).expect("Failed to compress corrupted image"); 199 | let decompressed = decompress(&compressed).expect("Failed to decompress corrupted image"); 200 | 201 | // Corrupted checksum should be fixed up 202 | assert!(orig_buffer == decompressed); 203 | } 204 | 205 | #[test] 206 | fn test_superblock_magic_fixup() { 207 | let orig_buffer = generate_test_image(); 208 | 209 | let mut compressed = compress(&orig_buffer).expect("Failed to compress corrupted image"); 210 | 211 | // Corrupt the magic in the superblock 212 | let mut data_idx: usize = 0; 213 | let mut corrupted_super = false; 214 | for metadata in &compressed.metadata { 215 | let offset: usize = metadata.offset.try_into().unwrap(); 216 | let size: usize = metadata.size.try_into().unwrap(); 217 | 218 | if offset == BTRFS_SUPERBLOCK_OFFSET { 219 | let superblock = 220 | unsafe { &mut *(compressed.data[data_idx..].as_mut_ptr() as *mut BtrfsSuperblock) }; 221 | // Magic corruption 222 | superblock.magic[3] = b'Z'; 223 | corrupted_super = true; 224 | } 225 | 226 | data_idx += size; 227 | } 228 | assert!(corrupted_super); 229 | 230 | let decompressed = decompress(&compressed).expect("Failed to decompress corrupted image"); 231 | 232 | // Corrupted checksum should be fixed up 233 | assert!(orig_buffer == decompressed); 234 | } 235 | 236 | /// Test that checksums are recalculated on metadata changes. Note that this is pretty difficult to 237 | /// test accurately so we opt to just check that the checksum was changed. 238 | #[test] 239 | fn test_checksum_fixup_on_metadata_corruption() { 240 | let orig_buffer = generate_test_image(); 241 | 242 | let mut compressed = compress(&orig_buffer).expect("Failed to compress corrupted image"); 243 | let ones: Vec = vec![1; 45]; 244 | 245 | let mut first = true; 246 | let mut data_idx: usize = 0; 247 | let mut csum_before: Option> = None; 248 | let mut scribbed_offset: usize = 0; 249 | for metadata in &compressed.metadata { 250 | let size: usize = metadata.size.try_into().unwrap(); 251 | 252 | if !metadata.needs_csum_fixup { 253 | data_idx += size; 254 | continue; 255 | } 256 | 257 | // Skip superblock to avoid overtesting the superblock 258 | if first { 259 | first = false; 260 | data_idx += size; 261 | continue; 262 | } 263 | 264 | scribbed_offset = metadata.offset.try_into().unwrap(); 265 | 266 | // Store checksum before 267 | csum_before = Some(compressed.data[data_idx..(data_idx + BTRFS_CSUM_SIZE)].to_owned()); 268 | 269 | // Scribble over metadata a little 270 | let begin = data_idx + BTRFS_CSUM_SIZE; 271 | let end = data_idx + BTRFS_CSUM_SIZE + ones.len(); 272 | let _: Vec<_> = compressed 273 | .data 274 | .splice(begin..end, ones.iter().cloned()) 275 | .collect(); 276 | 277 | data_idx += size; 278 | } 279 | 280 | let decompressed = decompress(&compressed).expect("Failed to decompress corrupted image"); 281 | let csum_after = &decompressed[scribbed_offset..(scribbed_offset + BTRFS_CSUM_SIZE)]; 282 | 283 | // First test that the ones we wrote are where we expect so we know we didn't mess up the 284 | // offset calculations somewhere 285 | let begin = scribbed_offset + BTRFS_CSUM_SIZE; 286 | let end = scribbed_offset + BTRFS_CSUM_SIZE + ones.len(); 287 | assert!(&decompressed[begin..end] == ones.as_slice()); 288 | 289 | // Test that checksum changed 290 | assert!(csum_before.unwrap() != csum_after); 291 | } 292 | -------------------------------------------------------------------------------- /src/imgcompress/src/structs.rs: -------------------------------------------------------------------------------- 1 | pub const BTRFS_CSUM_SIZE: usize = 32; 2 | const BTRFS_LABEL_SIZE: usize = 256; 3 | const BTRFS_FSID_SIZE: usize = 16; 4 | const BTRFS_UUID_SIZE: usize = 16; 5 | const BTRFS_SYSTEM_CHUNK_ARRAY_SIZE: usize = 2048; 6 | 7 | pub const BTRFS_SUPERBLOCK_OFFSET: usize = 0x10_000; 8 | pub const BTRFS_SUPERBLOCK_OFFSET2: usize = 0x4_000_000; 9 | pub const BTRFS_SUPERBLOCK_OFFSET3: usize = 0x4_000_000_000; 10 | pub const BTRFS_SUPERBLOCK_MAGIC: [u8; 8] = *b"_BHRfS_M"; 11 | pub const BTRFS_SUPERBLOCK_SIZE: usize = 4096; 12 | pub const BTRFS_CSUM_TYPE_CRC32: u16 = 0; 13 | /// All the docs and code suggest it's `u32::MAX` but after many hours of debugging it turns out 14 | /// only 0 works. Something is definitely fishy here. At least we have tests that test checksum 15 | /// integrity. 16 | pub const BTRFS_CSUM_CRC32_SEED: u32 = 0; 17 | 18 | pub const BTRFS_CHUNK_ITEM_KEY: u8 = 228; 19 | pub const BTRFS_ROOT_ITEM_KEY: u8 = 132; 20 | 21 | #[repr(C, packed)] 22 | #[derive(Copy, Clone)] 23 | pub struct BtrfsDevItem { 24 | /// the internal btrfs device id 25 | pub devid: u64, 26 | /// size of the device 27 | pub total_bytes: u64, 28 | /// bytes used 29 | pub bytes_used: u64, 30 | /// optimal io alignment for this device 31 | pub io_align: u32, 32 | /// optimal io width for this device 33 | pub io_width: u32, 34 | /// minimal io size for this device 35 | pub sector_size: u32, 36 | /// type and info about this device 37 | pub ty: u64, 38 | /// expected generation for this device 39 | pub generation: u64, 40 | /// starting byte of this partition on the device, to allow for stripe alignment in the future 41 | pub start_offset: u64, 42 | /// grouping information for allocation decisions 43 | pub dev_group: u32, 44 | /// seek speed 0-100 where 100 is fastest 45 | pub seek_speed: u8, 46 | /// bandwidth 0-100 where 100 is fastest 47 | pub bandwidth: u8, 48 | /// btrfs generated uuid for this device 49 | pub uuid: [u8; BTRFS_UUID_SIZE], 50 | /// uuid of FS who owns this device 51 | pub fsid: [u8; BTRFS_UUID_SIZE], 52 | } 53 | 54 | #[repr(C, packed)] 55 | #[derive(Copy, Clone)] 56 | pub struct BtrfsRootBackup { 57 | pub tree_root: u64, 58 | pub tree_root_gen: u64, 59 | pub chunk_root: u64, 60 | pub chunk_root_gen: u64, 61 | pub extent_root: u64, 62 | pub extent_root_gen: u64, 63 | pub fs_root: u64, 64 | pub fs_root_gen: u64, 65 | pub dev_root: u64, 66 | pub dev_root_gen: u64, 67 | pub csum_root: u64, 68 | pub csum_root_gen: u64, 69 | pub total_bytes: u64, 70 | pub bytes_used: u64, 71 | pub num_devices: u64, 72 | /// future 73 | pub unused_64: [u64; 4], 74 | pub tree_root_level: u8, 75 | pub chunk_root_level: u8, 76 | pub extent_root_level: u8, 77 | pub fs_root_level: u8, 78 | pub dev_root_level: u8, 79 | pub csum_root_level: u8, 80 | /// future and to align 81 | pub unused_8: [u8; 10], 82 | } 83 | 84 | #[repr(C, packed)] 85 | #[derive(Copy, Clone)] 86 | pub struct BtrfsSuperblock { 87 | pub csum: [u8; BTRFS_CSUM_SIZE], 88 | pub fsid: [u8; BTRFS_FSID_SIZE], 89 | /// Physical address of this block 90 | pub bytenr: u64, 91 | pub flags: u64, 92 | pub magic: [u8; 0x8], 93 | pub generation: u64, 94 | /// Logical address of the root tree root 95 | pub root: u64, 96 | /// Logical address of the chunk tree root 97 | pub chunk_root: u64, 98 | /// Logical address of the log tree root 99 | pub log_root: u64, 100 | pub log_root_transid: u64, 101 | pub total_bytes: u64, 102 | pub bytes_used: u64, 103 | pub root_dir_objectid: u64, 104 | pub num_devices: u64, 105 | pub sector_size: u32, 106 | pub node_size: u32, 107 | /// Unused and must be equal to `nodesize` 108 | pub leafsize: u32, 109 | pub stripesize: u32, 110 | pub sys_chunk_array_size: u32, 111 | pub chunk_root_generation: u64, 112 | pub compat_flags: u64, 113 | pub compat_ro_flags: u64, 114 | pub incompat_flags: u64, 115 | pub csum_type: u16, 116 | pub root_level: u8, 117 | pub chunk_root_level: u8, 118 | pub log_root_level: u8, 119 | pub dev_item: BtrfsDevItem, 120 | pub label: [u8; BTRFS_LABEL_SIZE], 121 | pub cache_generation: u64, 122 | pub uuid_tree_generation: u64, 123 | pub metadata_uuid: [u8; BTRFS_FSID_SIZE], 124 | /// Future expansion 125 | pub _reserved: [u64; 28], 126 | pub sys_chunk_array: [u8; BTRFS_SYSTEM_CHUNK_ARRAY_SIZE], 127 | pub root_backups: [BtrfsRootBackup; 4], 128 | } 129 | 130 | #[repr(C, packed)] 131 | #[derive(Copy, Clone)] 132 | pub struct BtrfsStripe { 133 | pub devid: u64, 134 | pub offset: u64, 135 | pub dev_uuid: [u8; BTRFS_UUID_SIZE], 136 | } 137 | 138 | #[repr(C, packed)] 139 | #[derive(Copy, Clone)] 140 | pub struct BtrfsChunk { 141 | /// size of this chunk in bytes 142 | pub length: u64, 143 | /// objectid of the root referencing this chunk 144 | pub owner: u64, 145 | pub stripe_len: u64, 146 | pub ty: u64, 147 | /// optimal io alignment for this chunk 148 | pub io_align: u32, 149 | /// optimal io width for this chunk 150 | pub io_width: u32, 151 | /// minimal io size for this chunk 152 | pub sector_size: u32, 153 | /// 2^16 stripes is quite a lot, a second limit is the size of a single item in the btree 154 | pub num_stripes: u16, 155 | /// sub stripes only matter for raid10 156 | pub sub_stripes: u16, 157 | pub stripe: BtrfsStripe, 158 | // additional stripes go here 159 | } 160 | 161 | #[repr(C, packed)] 162 | #[derive(Copy, Clone)] 163 | pub struct BtrfsTimespec { 164 | pub sec: u64, 165 | pub nsec: u32, 166 | } 167 | 168 | #[repr(C, packed)] 169 | #[derive(Copy, Clone)] 170 | pub struct BtrfsInodeItem { 171 | /// nfs style generation number 172 | pub generation: u64, 173 | /// transid that last touched this inode 174 | pub transid: u64, 175 | pub size: u64, 176 | pub nbytes: u64, 177 | pub block_group: u64, 178 | pub nlink: u32, 179 | pub uid: u32, 180 | pub gid: u32, 181 | pub mode: u32, 182 | pub rdev: u64, 183 | pub flags: u64, 184 | /// modification sequence number for NFS 185 | pub sequence: u64, 186 | pub reserved: [u64; 4], 187 | pub atime: BtrfsTimespec, 188 | pub ctime: BtrfsTimespec, 189 | pub mtime: BtrfsTimespec, 190 | pub otime: BtrfsTimespec, 191 | } 192 | 193 | #[repr(C, packed)] 194 | #[derive(Copy, Clone)] 195 | pub struct BtrfsRootItem { 196 | pub inode: BtrfsInodeItem, 197 | pub generation: u64, 198 | pub root_dirid: u64, 199 | pub bytenr: u64, 200 | pub byte_limit: u64, 201 | pub bytes_used: u64, 202 | pub last_snapshot: u64, 203 | pub flags: u64, 204 | pub refs: u32, 205 | pub drop_progress: BtrfsKey, 206 | pub drop_level: u8, 207 | pub level: u8, 208 | pub generation_v2: u64, 209 | pub uuid: [u8; BTRFS_UUID_SIZE], 210 | pub parent_uuid: [u8; BTRFS_UUID_SIZE], 211 | pub received_uuid: [u8; BTRFS_UUID_SIZE], 212 | /// updated when an inode changes 213 | pub ctransid: u64, 214 | /// trans when created 215 | pub otransid: u64, 216 | /// trans when sent. non-zero for received subvol 217 | pub stransid: u64, 218 | /// trans when received. non-zero for received subvol 219 | pub rtransid: u64, 220 | pub ctime: BtrfsTimespec, 221 | pub otime: BtrfsTimespec, 222 | pub stime: BtrfsTimespec, 223 | pub rtime: BtrfsTimespec, 224 | pub reserved: [u64; 8], 225 | } 226 | 227 | #[repr(C, packed)] 228 | #[derive(Copy, Clone)] 229 | pub struct BtrfsDirItem { 230 | pub location: BtrfsKey, 231 | pub transid: u64, 232 | pub data_len: u16, 233 | pub name_len: u16, 234 | pub ty: u8, 235 | } 236 | 237 | #[repr(C, packed)] 238 | #[derive(Copy, Clone)] 239 | pub struct BtrfsInodeRef { 240 | pub index: u64, 241 | pub name_len: u16, 242 | } 243 | 244 | #[repr(C, packed)] 245 | #[derive(Copy, Clone)] 246 | pub struct BtrfsKey { 247 | pub objectid: u64, 248 | pub ty: u8, 249 | pub offset: u64, 250 | } 251 | 252 | #[repr(C, packed)] 253 | #[derive(Copy, Clone)] 254 | pub struct BtrfsHeader { 255 | pub csum: [u8; BTRFS_CSUM_SIZE], 256 | pub fsid: [u8; BTRFS_FSID_SIZE], 257 | /// Which block this node is supposed to live in 258 | pub bytenr: u64, 259 | pub flags: u64, 260 | pub chunk_tree_uuid: [u8; BTRFS_UUID_SIZE], 261 | pub generation: u64, 262 | pub owner: u64, 263 | pub nritems: u32, 264 | pub level: u8, 265 | } 266 | 267 | #[repr(C, packed)] 268 | #[derive(Copy, Clone)] 269 | /// A `BtrfsLeaf` is full of `BtrfsItem`s. `offset` and `size` (relative to start of data area) 270 | /// tell us where to find the item in the leaf. 271 | pub struct BtrfsItem { 272 | pub key: BtrfsKey, 273 | pub offset: u32, 274 | pub size: u32, 275 | } 276 | 277 | #[repr(C, packed)] 278 | #[derive(Copy, Clone)] 279 | pub struct BtrfsLeaf { 280 | pub header: BtrfsHeader, 281 | // `BtrfsItem`s begin here 282 | } 283 | 284 | #[repr(C, packed)] 285 | #[derive(Copy, Clone)] 286 | /// All non-leaf blocks are nodes and they hold only keys are pointers to other blocks 287 | pub struct BtrfsKeyPtr { 288 | pub key: BtrfsKey, 289 | pub blockptr: u64, 290 | pub generation: u64, 291 | } 292 | 293 | #[repr(C, packed)] 294 | #[derive(Copy, Clone)] 295 | pub struct BtrfsNode { 296 | pub header: BtrfsHeader, 297 | // `BtrfsKeyPtr`s begin here 298 | } 299 | -------------------------------------------------------------------------------- /src/imgcompress/src/tree.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | 3 | use crate::structs::*; 4 | 5 | /// Parse BtrfsHeader from a tree node (internal or leaf) 6 | pub fn parse_btrfs_header<'a>(buf: &'a [u8]) -> Result<&'a BtrfsHeader> { 7 | let header_size = std::mem::size_of::(); 8 | if buf.len() < header_size { 9 | bail!("Failed to parse BtrfsHeader b/c buf too small"); 10 | } 11 | 12 | Ok(unsafe { &*(buf.as_ptr() as *const BtrfsHeader) }) 13 | } 14 | 15 | /// Parse an internal tree node 16 | /// 17 | /// Precondition is that `buf` is not a leaf node. 18 | pub fn parse_btrfs_node<'a>(buf: &'a [u8]) -> Result> { 19 | let header = parse_btrfs_header(buf)?; 20 | let mut offset = std::mem::size_of::(); 21 | let mut key_ptrs = Vec::new(); 22 | for _ in 0..header.nritems { 23 | key_ptrs.push(unsafe { &*(buf.as_ptr().add(offset) as *const BtrfsKeyPtr) }); 24 | offset += std::mem::size_of::(); 25 | } 26 | 27 | Ok(key_ptrs) 28 | } 29 | 30 | /// Parse leaf tree node 31 | pub fn parse_btrfs_leaf<'a>(buf: &'a [u8]) -> Result> { 32 | let header = parse_btrfs_header(buf)?; 33 | let mut offset = std::mem::size_of::(); 34 | let mut items = Vec::new(); 35 | for _ in 0..header.nritems { 36 | items.push(unsafe { &*(buf.as_ptr().add(offset) as *const BtrfsItem) }); 37 | offset += std::mem::size_of::(); 38 | } 39 | 40 | Ok(items) 41 | } 42 | -------------------------------------------------------------------------------- /src/manager/__init__.py: -------------------------------------------------------------------------------- 1 | from .manager import Manager 2 | -------------------------------------------------------------------------------- /src/manager/manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import signal 4 | import shutil 5 | import sys 6 | import threading 7 | import time 8 | import uuid 9 | 10 | import pexpect 11 | 12 | MASTER_NAME = "master" 13 | KERNEL_PANIC_STR = "Kernel panic" 14 | 15 | 16 | def get_secondary_name(idx): 17 | return f"secondary_{idx}" 18 | 19 | 20 | def get_cmd_env_vars(): 21 | e = [] 22 | 23 | # We didn't build with the afl toolchain so our binary is not watermarked 24 | e.append("AFL_SKIP_BIN_CHECK=1") 25 | 26 | # Help debug crashes in our runner 27 | e.append("AFL_DEBUG_CHILD_OUTPUT=1") 28 | 29 | # Our custom mutator only fuzzes the FS metadata. Anything else is 30 | # ineffective 31 | e.append("AFL_CUSTOM_MUTATOR_LIBRARY=/btrfs-fuzz/libmutator.so") 32 | e.append("AFL_CUSTOM_MUTATOR_ONLY=1") 33 | 34 | # The custom mutator doesn't append or delete bytes. Trimming also messes 35 | # with deserializing input so, don't trim. 36 | e.append("AFL_DISABLE_TRIM=1") 37 | 38 | # Autoresume work 39 | e.append("AFL_AUTORESUME=1") 40 | 41 | return e 42 | 43 | 44 | def get_cmd_args(master=False, secondary=None): 45 | """Get arguments to invoke AFL with 46 | 47 | Note `master` and `secondary` cannot both be specified. 48 | 49 | master: If true, get arguments for parallel fuzzing master node 50 | secondary: If specified, the integer value is the secondary instance number. 51 | This function will then return arguments for parallel fuzzing 52 | secondary node. 53 | """ 54 | if master and secondary is not None: 55 | raise RuntimeError("Cannot specify both master and secondary arguments") 56 | 57 | c = [] 58 | 59 | c.append("/usr/local/bin/afl-fuzz") 60 | c.append("-m 500") 61 | c.append("-i /state/input") 62 | c.append("-o /state/output") 63 | 64 | # See bottom of 65 | # https://github.com/AFLplusplus/AFLplusplus/blob/stable/docs/power_schedules.md 66 | if master: 67 | c.append(f"-M {MASTER_NAME}") 68 | c.append("-p exploit") 69 | elif secondary is not None: 70 | c.append(f"-S {get_secondary_name(secondary)}") 71 | 72 | AFL_SECONDARY_SCHEDULES = ["coe", "fast", "explore"] 73 | idx = secondary % len(AFL_SECONDARY_SCHEDULES) 74 | c.append(f"-p {AFL_SECONDARY_SCHEDULES[idx]}") 75 | 76 | c.append("--") 77 | c.append("/btrfs-fuzz/runner") 78 | 79 | return c 80 | 81 | 82 | def get_docker_args(img, state_dir): 83 | c = [] 84 | 85 | c.append("podman run") 86 | c.append("-it") 87 | c.append("--rm") 88 | c.append("--privileged") 89 | c.append(f"-v {state_dir}:/state") 90 | c.append(img) 91 | 92 | return c 93 | 94 | 95 | def get_nspawn_args(fsdir, state_dir, machine_idx): 96 | c = [] 97 | 98 | abs_fsdir_path = os.path.abspath(fsdir) 99 | abs_state_dir = os.path.abspath(state_dir) 100 | 101 | c.append("sudo") 102 | c.append("SYSTEMD_NSPAWN_LOCK=0") 103 | c.append("systemd-nspawn") 104 | c.append(f"--directory {fsdir}") 105 | c.append(f"--bind={abs_state_dir}:/state") 106 | c.append("--chdir=/btrfs-fuzz") 107 | c.append(f"--machine btrfs-fuzz-{machine_idx}") 108 | 109 | # Map into the container /dev/kvm so qemu can run faster 110 | c.append(f"--bind=/dev/kvm:/dev/kvm") 111 | 112 | return c 113 | 114 | 115 | class VM: 116 | """One virtual machine instance""" 117 | 118 | def __init__(self, p, args, state_dir, needs_vm_entry=False, name=None): 119 | """Initialize VM 120 | p: An already spawned `pexpect.spawn` VM instance. Nothing should be 121 | running in the VM yet. 122 | args: Arguments to invoke AFL (string) 123 | state_dir: State directory for fuzzing session (could be shared between 124 | multiple VMs) 125 | needs_vm_entry: If true, the container has been entered but the VM has 126 | not been spawned yet. Pass true to also run ./entry.sh 127 | name: Name of the VM instance. Only needs to be specified if running 128 | multiple VMs (ie parallel mode) 129 | """ 130 | self.vm = p 131 | self.args = args 132 | self.state_dir = state_dir 133 | self.needs_vm_entry = needs_vm_entry 134 | self.name = name 135 | self.prompt_regex = "root@.*#" 136 | 137 | async def run_and_wait(self, cmd, disable_timeout=False): 138 | """Run a command in the VM and wait until the command completes""" 139 | self.vm.sendline(cmd) 140 | 141 | if disable_timeout: 142 | await self.vm.expect(self.prompt_regex, timeout=None, async_=True) 143 | else: 144 | await self.vm.expect(self.prompt_regex, async_=True) 145 | 146 | def handle_kernel_panic(self): 147 | """Save the current input if it triggers a kernel panic""" 148 | output_dir = f"{self.state_dir}/output" 149 | if self.name is not None: 150 | output_dir += f"/{self.name}" 151 | 152 | cur_input = os.path.abspath(f"{output_dir}/.cur_input") 153 | dest = os.path.abspath(f"{self.state_dir}/known_crashes/{uuid.uuid4()}") 154 | shutil.copy(cur_input, dest) 155 | 156 | async def _run(self): 157 | # `self.p` should not have been `expect()`d upon yet so we need to wait 158 | # until a prompt is ready 159 | await self.vm.expect(self.prompt_regex, async_=True) 160 | 161 | if self.needs_vm_entry: 162 | await self.run_and_wait("./entry.sh", disable_timeout=True) 163 | 164 | # Set core pattern 165 | await self.run_and_wait("echo core > /proc/sys/kernel/core_pattern") 166 | 167 | # Start running fuzzer 168 | self.vm.sendline(self.args) 169 | 170 | expected = [self.prompt_regex, KERNEL_PANIC_STR] 171 | idx = await self.vm.expect(expected, timeout=None, async_=True) 172 | if idx == 0: 173 | print( 174 | "Unexpected fuzzer exit. Is there a bug with the runner? Not continuing." 175 | ) 176 | elif idx == 1: 177 | print("Detected kernel panic!") 178 | self.handle_kernel_panic() 179 | else: 180 | raise RuntimeError(f"Unknown expected idx={idx}") 181 | 182 | async def run(self): 183 | try: 184 | await self._run() 185 | except asyncio.CancelledError: 186 | # podman won't clean up after us unless we kill the entire process tree 187 | pid = self.vm.pid 188 | print(f"Sending SIGKILL to all pids in pid={pid} tree") 189 | os.system(f"pkill -KILL -P {pid}") 190 | 191 | 192 | class Manager: 193 | def __init__(self, img, state_dir, nspawn=False, parallel=None): 194 | """Initialize Manager 195 | img: Name of docker image to run 196 | state_dir: Path to directory to map into /state inside VM 197 | nspawn: Treat `img` as the path to a untarred filesystem and use systemd-nspawn 198 | to start container 199 | parallel: If specified, run parallel fuzzing on provided # of CPUs. -1 means all, 200 | default is 1. 201 | """ 202 | # Which docker image to use 203 | self.img = img 204 | 205 | # Where the state dir is on host 206 | self.state_dir = state_dir 207 | 208 | self.nspawn = nspawn 209 | self.parallel = parallel 210 | 211 | self.vm = None 212 | 213 | # Use to create uniquely named machinectl names 214 | self.machine_idx = 0 215 | 216 | def spawn_vm(self, display): 217 | """Spawn a single VM 218 | 219 | display: Print child output to stdout 220 | 221 | Returns a `pexpect.spawn` instance 222 | """ 223 | if self.nspawn: 224 | args = get_nspawn_args(self.img, self.state_dir, self.machine_idx) 225 | self.machine_idx += 1 226 | else: 227 | args = get_docker_args(self.img, self.state_dir) 228 | cmd = " ".join(args) 229 | 230 | p = pexpect.spawn(cmd, encoding="utf-8") 231 | 232 | # Pipe everything the child prints to our stdout 233 | if display: 234 | p.logfile_read = sys.stdout 235 | 236 | return p 237 | 238 | def prep_one(self, master=False, secondary=None): 239 | """Run one fuzzer instance 240 | 241 | master: If true, spawn the master instance 242 | secondary: If specified, the integer number of the secondary instance 243 | 244 | Returns a `VM` instance 245 | """ 246 | # Start the VM (could take a few seconds) 247 | # 248 | # Only display output if master instance or sole instance in non-parallel 249 | # mode 250 | p = self.spawn_vm(master or (not master and not secondary)) 251 | 252 | # For docker images we rely on the ENTRYPOINT directive. For nspawn we 253 | # have to do it ourselves 254 | if self.nspawn: 255 | needs_vm_entry = True 256 | else: 257 | needs_vm_entry = False 258 | 259 | cmd = get_cmd_env_vars() 260 | cmd.extend(get_cmd_args(master, secondary)) 261 | 262 | if master: 263 | name = MASTER_NAME 264 | elif secondary is not None: 265 | name = get_secondary_name(secondary) 266 | else: 267 | name = None 268 | 269 | return VM( 270 | p, " ".join(cmd), self.state_dir, needs_vm_entry=needs_vm_entry, name=name 271 | ) 272 | 273 | async def run_parallel(self, nr_cpus): 274 | tasks = [] 275 | for i in range(nr_cpus): 276 | if i == 0: 277 | name = f"btrfs-fuzz-{MASTER_NAME}" 278 | vm = self.prep_one(master=True) 279 | else: 280 | name = f"btrfs-fuzz-{get_secondary_name(i)}" 281 | vm = self.prep_one(secondary=i) 282 | 283 | if sys.version_info < (3, 7): 284 | t = asyncio.ensure_future(vm.run()) 285 | else: 286 | t = asyncio.create_task(vm.run(), name=name) 287 | tasks.append(t) 288 | 289 | # Now manage all the running tasks -- if any die, we'll error out 290 | # for now. In the future we should log the crash and respawn the 291 | # thread. 292 | while True: 293 | triggering_task = None 294 | exit = False 295 | for t in tasks: 296 | if t.done(): 297 | if sys.version_info < (3, 8): 298 | name = "?" 299 | else: 300 | name = t.get_name() 301 | print(f"Task={name} unexpectedly exited. Exiting now.") 302 | triggering_task = t 303 | exit = True 304 | break 305 | 306 | if exit: 307 | # Cancel all the outstanding tasks so we don't leak VMs 308 | for t in [t for t in tasks if not t.done()]: 309 | t.cancel() 310 | 311 | print("All other tasks cancelled") 312 | 313 | # Print out stacktrace from task that triggered the exit 314 | t = triggering_task 315 | exc = t.exception() 316 | if exc: 317 | if sys.version_info < (3, 8): 318 | name = "?" 319 | else: 320 | name = t.get_name() 321 | print(f"Exception from {name}: {exc}") 322 | t.print_stack() 323 | 324 | break 325 | 326 | await asyncio.sleep(1) 327 | 328 | async def _run(self): 329 | run_parallel = False 330 | if self.parallel is not None: 331 | if self.parallel == -1: 332 | run_parallel = True 333 | nr_cpus = len(os.sched_getaffinity(0)) 334 | elif self.parallel > 1: 335 | run_parallel = True 336 | nr_cpus = self.parallel 337 | 338 | if run_parallel: 339 | await self.run_parallel(nr_cpus) 340 | else: 341 | await self.prep_one().run() 342 | 343 | def run(self): 344 | def cancel_all_tasks(): 345 | if sys.version_info < (3, 7): 346 | all_tasks = asyncio.Task.all_tasks() 347 | else: 348 | all_tasks = asyncio.all_tasks() 349 | 350 | for t in all_tasks: 351 | t.cancel() 352 | 353 | # If we don't cancel the outstanding tasks, the containers leak 354 | loop = asyncio.get_event_loop() 355 | loop.add_signal_handler(signal.SIGINT, cancel_all_tasks) 356 | loop.add_signal_handler(signal.SIGTERM, cancel_all_tasks) 357 | loop.add_signal_handler(signal.SIGHUP, cancel_all_tasks) 358 | 359 | try: 360 | loop.run_until_complete(self._run()) 361 | except asyncio.CancelledError: 362 | pass 363 | -------------------------------------------------------------------------------- /src/mutator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mutator" 3 | version = "0.1.0" 4 | authors = ["Daniel Xu "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | anyhow = "1.0" 12 | fuzzmutator = "0.2" 13 | imgcompress = { path = "../imgcompress" } 14 | libc = "0.2" 15 | rmp-serde = "0.14" 16 | serde = "1.0" 17 | -------------------------------------------------------------------------------- /src/mutator/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::boxed::Box; 2 | use std::ptr; 3 | use std::slice; 4 | 5 | use anyhow::Result; 6 | use fuzzmutator::mutator::MutatorEngine; 7 | use rmp_serde::{decode::from_read_ref, Serializer}; 8 | use serde::Serialize; 9 | 10 | use imgcompress::CompressedBtrfsImage; 11 | 12 | struct Mutator { 13 | engine: MutatorEngine, 14 | /// We'll return pointers to data in this buffer from `afl_custom_fuzz` 15 | fuzz_buf: Vec, 16 | } 17 | 18 | impl Mutator { 19 | fn new() -> Result { 20 | Ok(Self { 21 | engine: MutatorEngine::new()?, 22 | fuzz_buf: Vec::new(), 23 | }) 24 | } 25 | } 26 | 27 | /// Initialize this custom mutator 28 | /// 29 | /// @param[in] afl a pointer to the internal state object. Can be ignored for 30 | /// now. 31 | /// @param[in] seed A seed for this mutator - the same seed should always mutate 32 | /// in the same way. 33 | /// @return Pointer to the data object this custom mutator instance should use. 34 | /// There may be multiple instances of this mutator in one afl-fuzz run! 35 | /// Return NULL on error. 36 | #[no_mangle] 37 | pub extern "C" fn afl_custom_init( 38 | _afl: *mut libc::c_void, 39 | _seed: libc::c_uint, 40 | ) -> *mut libc::c_void { 41 | let mutator = match Mutator::new() { 42 | Ok(m) => m, 43 | Err(e) => { 44 | println!("{}", e); 45 | return ptr::null_mut(); 46 | } 47 | }; 48 | 49 | let boxed = Box::new(mutator); 50 | 51 | Box::into_raw(boxed) as *mut libc::c_void 52 | } 53 | 54 | /// Perform custom mutations on a given input 55 | /// 56 | /// Note that our implementation doesn't append or trim any data to/from the fuzzing 57 | /// payload. In theory it shouldn't be useful b/c the kernel driver usually won't read 58 | /// past the end of the structs it knows about. 59 | /// 60 | /// @param[in] data pointer returned in afl_custom_init for this fuzz case 61 | /// @param[in] buf Pointer to input data to be mutated 62 | /// @param[in] buf_size Size of input data 63 | /// @param[out] out_buf the buffer we will work on. we can reuse *buf. NULL on 64 | /// error. 65 | /// @param[in] add_buf Buffer containing the additional test case 66 | /// @param[in] add_buf_size Size of the additional test case 67 | /// @param[in] max_size Maximum size of the mutated output. The mutation must not 68 | /// produce data larger than max_size. 69 | /// @return Size of the mutated output. 70 | #[no_mangle] 71 | pub extern "C" fn afl_custom_fuzz( 72 | data: *mut libc::c_void, 73 | buf: *mut u8, 74 | buf_size: libc::size_t, 75 | out_buf: *mut *mut u8, 76 | _add_buf: *mut u8, 77 | _add_buf_size: libc::size_t, 78 | max_size: libc::size_t, 79 | ) -> libc::size_t { 80 | let mutator = unsafe { &mut *(data as *mut Mutator) }; 81 | 82 | // Deserialize input 83 | let serialized: &[u8] = unsafe { slice::from_raw_parts(buf, buf_size) }; 84 | let mut deserialized: CompressedBtrfsImage = match from_read_ref(&serialized) { 85 | Ok(d) => d, 86 | Err(e) => { 87 | eprintln!("Failed to deserialize fuzzer input: {}", e); 88 | unsafe { out_buf.write(ptr::null_mut()) }; 89 | return 0; 90 | } 91 | }; 92 | 93 | // Mutate payload (but don't touch the metadata) 94 | mutator.engine.mutate(&mut deserialized.data); 95 | // The engine shouldn't append any data but it's probably worthwhile to check again 96 | assert!(deserialized.data.len() + deserialized.metadata.len() <= max_size); 97 | 98 | // Serialize data again 99 | mutator.fuzz_buf.clear(); // Does not affect capacity 100 | match deserialized.serialize(&mut Serializer::new(&mut mutator.fuzz_buf)) { 101 | Ok(_) => (), 102 | Err(e) => { 103 | eprintln!("Failed to serialize fuzzer input: {}", e); 104 | unsafe { out_buf.write(ptr::null_mut()) }; 105 | return 0; 106 | } 107 | }; 108 | assert!(mutator.fuzz_buf.len() <= max_size); 109 | 110 | // Yes, it's ok to hand out ref to the Vec we own. The API is designed this way 111 | unsafe { out_buf.write(mutator.fuzz_buf.as_mut_ptr()) }; 112 | 113 | mutator.fuzz_buf.len() 114 | } 115 | 116 | /// Deinitialize everything 117 | /// 118 | /// @param data The data ptr from afl_custom_init 119 | #[no_mangle] 120 | pub extern "C" fn afl_custom_deinit(data: *mut libc::c_void) { 121 | // Reconstruct box and immediately drop to free resources 122 | unsafe { Box::from_raw(data as *mut Mutator) }; 123 | } 124 | 125 | /// Not confident that the 3rd party mutator works. Let's just make sure it seems sane. 126 | #[test] 127 | // Skip the test for now b/c maybe always mutating isn't a good thing. Who knows. Should be easy 128 | // enough to swap out a mutation engine some day and compare results. 129 | #[ignore] 130 | fn test_mutator_works() { 131 | let mut engine = MutatorEngine::new().expect("Failed to init mutator engine"); 132 | let one = vec![0; 10_000]; 133 | 134 | for _ in 0..10_000 { 135 | let mut two = one.clone(); 136 | engine.mutate(&mut two); 137 | assert!(one != two); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/runner/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "runner" 3 | version = "0.1.0" 4 | authors = ["Daniel Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | anyhow = "1.0" 9 | imgcompress = { path = "../imgcompress" } 10 | libc = "0.2" 11 | loopdev = "0.2" 12 | nix = "0.18" 13 | rmp-serde = "0.14" 14 | static_assertions = "1.1" 15 | structopt = "0.3" 16 | sys-mount = "1.2" 17 | 18 | [dev-dependencies] 19 | tempfile = "3.1" 20 | -------------------------------------------------------------------------------- /src/runner/src/constants.rs: -------------------------------------------------------------------------------- 1 | /// Shared memory region size created by afl-fuzz 2 | pub const AFL_MAP_SIZE: u32 = 1 << 16; 3 | 4 | // AFL++ Forkserver options 5 | pub const AFL_FS_OPT_ENABLED: u32 = 0x80000001; 6 | pub const AFL_FS_OPT_MAPSIZE: u32 = 0x40000000; 7 | 8 | /// Hardcoded file descriptors to communicate with AFL++ 9 | pub const AFL_FORKSERVER_READ_FD: i32 = 198; 10 | pub const AFL_FORKSERVER_WRITE_FD: i32 = AFL_FORKSERVER_READ_FD + 1; 11 | -------------------------------------------------------------------------------- /src/runner/src/forkserver.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::env; 3 | use std::ptr; 4 | use std::slice; 5 | use std::str::FromStr; 6 | 7 | use anyhow::{bail, Result}; 8 | use libc::{c_void, calloc, free, shmat, shmdt}; 9 | use nix::{unistd::read, unistd::write}; 10 | 11 | use crate::constants::*; 12 | 13 | enum SharedMemPtr { 14 | /// Allocated using `shmat`, must be deallocated with `shmdt` 15 | Shm(*mut c_void), 16 | /// Allocated 17 | Anon(*mut c_void), 18 | } 19 | 20 | pub enum RunStatus { 21 | Success, 22 | Failure, 23 | } 24 | 25 | /// This struct implements a fake AFL++ forkserver that does not actually fork children. Instead, 26 | /// we'll do our own persistent mode [0]. 27 | /// 28 | /// The reference implementation can be found at [1]. 29 | /// 30 | /// [0]: https://github.com/AFLplusplus/AFLplusplus/blob/stable/llvm_mode/README.persistent_mode.md 31 | /// [1]: https://github.com/AFLplusplus/AFLplusplus/blob/stable/gcc_plugin/afl-gcc-rt.o.c 32 | pub struct Forkserver { 33 | /// `false` implies AFL++ is running us. `true` implies we're in standalone mode (most likely 34 | /// to reproduce a test). 35 | disabled: bool, 36 | shared_mem: SharedMemPtr, 37 | } 38 | 39 | impl Forkserver { 40 | pub fn new() -> Result { 41 | let mut disabled = env::var_os("AFL_NO_FORKSRV").is_some(); 42 | 43 | // https://github.com/AFLplusplus/AFLplusplus/blob/fac108476c1cb5/include/config.h#L305 44 | let shared_mem = match env::var_os("__AFL_SHM_ID") { 45 | Some(id) => { 46 | let id = i32::from_str(&id.as_os_str().to_string_lossy())?; 47 | let ptr = unsafe { shmat(id, ptr::null(), 0) }; 48 | if ptr == -1i64 as *mut c_void { 49 | bail!("Failed to shmat() edge buffer"); 50 | } 51 | 52 | SharedMemPtr::Shm(ptr) 53 | } 54 | None => { 55 | println!("Running outside of AFL"); 56 | disabled = true; 57 | 58 | let ptr = unsafe { calloc(AFL_MAP_SIZE.try_into()?, 1) }; 59 | if ptr.is_null() { 60 | bail!("Failed to calloc() edge buffer"); 61 | } 62 | 63 | SharedMemPtr::Anon(ptr) 64 | } 65 | }; 66 | 67 | // Phone home and tell parent we're OK 68 | if !disabled { 69 | // Must be exactly 4 bytes 70 | let val: u32 = AFL_FS_OPT_ENABLED 71 | | AFL_FS_OPT_MAPSIZE 72 | | Self::forkserver_opt_set_mapsize(AFL_MAP_SIZE); 73 | 74 | if write(AFL_FORKSERVER_WRITE_FD, &val.to_ne_bytes())? != 4 { 75 | bail!("Forkserver failed to phone home"); 76 | } 77 | } 78 | 79 | Ok(Self { 80 | disabled, 81 | shared_mem, 82 | }) 83 | } 84 | 85 | /// Encode map size to a value we pass through the our phone home 86 | fn forkserver_opt_set_mapsize(size: u32) -> u32 { 87 | (size - 1) << 1 88 | } 89 | 90 | /// Get edge transition shared memory buffer 91 | pub fn shmem(&mut self) -> &mut [u8] { 92 | let ptr = match self.shared_mem { 93 | SharedMemPtr::Shm(p) => p, 94 | SharedMemPtr::Anon(p) => p, 95 | }; 96 | 97 | unsafe { slice::from_raw_parts_mut(ptr as *mut u8, AFL_MAP_SIZE.try_into().unwrap()) } 98 | } 99 | 100 | /// Initiate a new test run with AFL 101 | pub fn new_run(&mut self) -> Result<()> { 102 | if self.disabled { 103 | return Ok(()); 104 | } 105 | 106 | // Exactly 4 bytes 107 | let mut was_killed: Vec = vec![0; 4]; 108 | 109 | // We don't really care if AFL "killed" our child b/c we gave AFL a dummy PID anyways, 110 | // so ignore `was_killed` result 111 | if read(AFL_FORKSERVER_READ_FD, &mut was_killed)? != 4 { 112 | bail!("Failed to read was_killed from AFL"); 113 | } 114 | 115 | let fake_pid = i32::MAX.to_ne_bytes(); 116 | if write(AFL_FORKSERVER_WRITE_FD, &fake_pid)? != 4 { 117 | bail!("Failed to give AFL fake_pid"); 118 | } 119 | 120 | Ok(()) 121 | } 122 | 123 | /// Report result of test run to AFL 124 | pub fn report(&mut self, status: RunStatus) -> Result<()> { 125 | if self.disabled { 126 | match status { 127 | RunStatus::Success => (), 128 | RunStatus::Failure => eprintln!(">===== FAILURE REPORTED =====<"), 129 | }; 130 | 131 | return Ok(()); 132 | } 133 | 134 | let val: i32 = match status { 135 | RunStatus::Success => 0, 136 | // 139 is SIGSEGV terminated exit code as encoded in `wait(2)`s `wstatus` 137 | RunStatus::Failure => 139, 138 | }; 139 | 140 | if write(AFL_FORKSERVER_WRITE_FD, &val.to_ne_bytes())? != 4 { 141 | bail!("Failed to report status to AFL"); 142 | } 143 | 144 | Ok(()) 145 | } 146 | } 147 | 148 | impl Drop for Forkserver { 149 | fn drop(&mut self) { 150 | match self.shared_mem { 151 | SharedMemPtr::Shm(p) => { 152 | if unsafe { shmdt(p) } != 0 { 153 | // Panic instead of leak memory over time 154 | panic!("Failed to shmdt() edge buffer"); 155 | } 156 | } 157 | SharedMemPtr::Anon(p) => unsafe { free(p) }, 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/runner/src/kcov.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::fs::{File, OpenOptions}; 3 | use std::mem::size_of; 4 | use std::os::unix::io::AsRawFd; 5 | use std::ptr; 6 | use std::slice; 7 | use std::sync::atomic::AtomicUsize; 8 | use std::sync::atomic::Ordering; 9 | 10 | use anyhow::{anyhow, bail, Context, Result}; 11 | use nix::fcntl::{fcntl, FcntlArg, FdFlag}; 12 | use nix::{ioctl_read, ioctl_write_int_bad, request_code_none}; 13 | 14 | const COVER_SIZE: usize = 16 << 10; 15 | 16 | /// See include/uapi/linux/kcov.h 17 | const KCOV_IOCTL_MAGIC: u8 = b'c'; 18 | const KCOV_INIT_TRACE_IOCTL_SEQ: u8 = 1; 19 | const KCOV_ENABLE_IOCTL_SEQ: u8 = 100; 20 | #[allow(dead_code)] 21 | const KCOV_DISABLE_IOCTL_SEQ: u8 = 101; 22 | const KCOV_TRACE_PC: u64 = 0; 23 | 24 | ioctl_read!( 25 | kcov_init_trace, 26 | KCOV_IOCTL_MAGIC, 27 | KCOV_INIT_TRACE_IOCTL_SEQ, 28 | u64 29 | ); 30 | // Can't use nix::ioctl_none b/c kcov "broke" API by accepting an arg in a no-arg ioctl 31 | ioctl_write_int_bad!( 32 | kcov_enable, 33 | request_code_none!(KCOV_IOCTL_MAGIC, KCOV_ENABLE_IOCTL_SEQ) 34 | ); 35 | ioctl_write_int_bad!( 36 | kcov_disable, 37 | request_code_none!(KCOV_IOCTL_MAGIC, KCOV_DISABLE_IOCTL_SEQ) 38 | ); 39 | 40 | pub struct Kcov { 41 | fd: i32, 42 | ptr: *mut libc::c_void, 43 | /// Must hold onto the kcov control file b/c `as_raw_fd()` does not transfer ownership 44 | _file: File, 45 | } 46 | 47 | impl Kcov { 48 | pub fn new() -> Result { 49 | let file = OpenOptions::new() 50 | .read(true) 51 | .write(true) 52 | .open("/sys/kernel/debug/kcov") 53 | .with_context(|| "Failed to open kcov control file".to_string())?; 54 | let fd = file.as_raw_fd(); 55 | 56 | // Disable O_CLOEXEC so we can use this struct instance in a forked child 57 | let mut flags = FdFlag::from_bits(fcntl(fd, FcntlArg::F_GETFD)?) 58 | .ok_or_else(|| anyhow!("Failed to interpret FdFlag"))?; 59 | flags &= !FdFlag::FD_CLOEXEC; 60 | fcntl(fd, FcntlArg::F_SETFD(flags))?; 61 | 62 | if unsafe { 63 | kcov_init_trace(fd, COVER_SIZE as *mut u64) 64 | .with_context(|| "Failed to KCOV_INIT_TRACE".to_string())? 65 | } != 0 66 | { 67 | bail!("Failed to KCOV_INIT_TRACE"); 68 | } 69 | 70 | let ptr = unsafe { 71 | libc::mmap( 72 | ptr::null_mut(), 73 | COVER_SIZE * size_of::(), 74 | libc::PROT_READ | libc::PROT_WRITE, 75 | libc::MAP_SHARED, 76 | fd, 77 | 0, 78 | ) 79 | }; 80 | 81 | if ptr == libc::MAP_FAILED { 82 | bail!("Failed to mmap shared kcov buffer"); 83 | } 84 | 85 | Ok(Self { 86 | fd, 87 | ptr, 88 | _file: file, 89 | }) 90 | } 91 | 92 | pub fn enable(&mut self) -> Result<()> { 93 | // Reset the counter 94 | self.coverage()[0].store(0, Ordering::Relaxed); 95 | 96 | if unsafe { 97 | kcov_enable(self.fd, KCOV_TRACE_PC.try_into().unwrap()) 98 | .with_context(|| "Failed to enable kcov PC tracing".to_string())? 99 | } != 0 100 | { 101 | bail!("Failed to enable kcov PC tracing"); 102 | } 103 | 104 | // Reset counter again in case we traced anything as the ioctl returned 105 | self.coverage()[0].store(0, Ordering::Relaxed); 106 | 107 | Ok(()) 108 | } 109 | 110 | #[allow(dead_code)] 111 | pub fn disable(&mut self) -> Result { 112 | let len = self.coverage()[0].load(Ordering::Relaxed); 113 | 114 | if unsafe { 115 | kcov_disable(self.fd, 0 as i32) 116 | .with_context(|| "Failed to disable kcov tracing".to_string())? 117 | } != 0 118 | { 119 | bail!("Failed to disable kcov tracing"); 120 | } 121 | 122 | Ok(len) 123 | } 124 | 125 | pub fn coverage(&self) -> &[AtomicUsize] { 126 | // We can transmute from `usize` to `AtomicUsize` b/c they have the same in-memory 127 | // representations (as promised by the docs) 128 | unsafe { slice::from_raw_parts(self.ptr as *const AtomicUsize, COVER_SIZE) } 129 | } 130 | } 131 | 132 | impl Drop for Kcov { 133 | /// Panic if we fail to free resources. Current thinking is it's better to fail 134 | /// early here and cause afl to report a crash rather than slowly leak memory. 135 | fn drop(&mut self) { 136 | if unsafe { libc::munmap(self.ptr, COVER_SIZE * size_of::()) } != 0 { 137 | panic!("Failed to munmap shared kcov buffer"); 138 | } 139 | 140 | if unsafe { libc::close(self.fd) } != 0 { 141 | panic!("Failed to close kcov fd"); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/runner/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::convert::TryInto; 3 | use std::fs::{create_dir_all, OpenOptions}; 4 | use std::io::{self, Read, Write}; 5 | use std::os::unix::io::AsRawFd; 6 | use std::path::Path; 7 | use std::process::exit; 8 | use std::sync::atomic::Ordering; 9 | 10 | use anyhow::{bail, Context, Result}; 11 | use libc::c_void; 12 | use nix::errno::{errno, Errno}; 13 | use nix::fcntl::{open, OFlag}; 14 | use nix::ioctl_write_ptr; 15 | use nix::sys::stat::Mode; 16 | use nix::sys::wait::{waitpid, WaitStatus}; 17 | use nix::unistd::{fork, lseek, ForkResult, Whence}; 18 | use rmp_serde::decode::from_read_ref; 19 | use static_assertions::const_assert; 20 | use structopt::StructOpt; 21 | 22 | mod constants; 23 | mod forkserver; 24 | mod kcov; 25 | mod mount; 26 | 27 | use forkserver::{Forkserver, RunStatus}; 28 | use kcov::Kcov; 29 | use mount::Mounter; 30 | 31 | const FUZZED_IMAGE_PATH: &str = "/tmp/btrfsimage"; 32 | 33 | /// See /usr/include/linux/btrfs.h 34 | const BTRFS_IOCTL_MAGIC: u8 = 0x94; 35 | const BTRFS_FORGET_DEV_IOCTL_SEQ: u8 = 5; 36 | const BTRFS_PATH_NAME_MAX: usize = 4087; 37 | 38 | #[repr(C, packed)] 39 | pub struct BtrfsIoctlVolArgs { 40 | fd: i64, 41 | name: [u8; BTRFS_PATH_NAME_MAX + 1], 42 | } 43 | const_assert!(std::mem::size_of::() == 4096); 44 | 45 | ioctl_write_ptr!( 46 | btrfs_forget_dev, 47 | BTRFS_IOCTL_MAGIC, 48 | BTRFS_FORGET_DEV_IOCTL_SEQ, 49 | BtrfsIoctlVolArgs 50 | ); 51 | 52 | enum TestcaseStatus { 53 | Ok, 54 | NoMore, 55 | } 56 | 57 | #[derive(Debug, StructOpt)] 58 | #[structopt(name = "runner", about = "Run btrfs-fuzz test cases")] 59 | struct Opt { 60 | /// Turn on debug output 61 | #[structopt(short, long)] 62 | debug: bool, 63 | } 64 | 65 | /// Opens kmsg fd and seeks to end. 66 | /// 67 | /// Note we avoid using the higher level std::fs interfaces b/c /dev/kmsg is a bit special in that 68 | /// each read(2) returns exactly 1 entry in the kernel's printk buffer. So we don't want any high 69 | /// level APIs issuing multiple reads. The fd must also be opened in non-blocking mode otherwise 70 | /// reads will block until a new entry is available. 71 | fn open_kmsg() -> Result { 72 | let fd = open( 73 | "/dev/kmsg", 74 | OFlag::O_RDONLY | OFlag::O_NONBLOCK, 75 | Mode::empty(), 76 | )?; 77 | lseek(fd, 0, Whence::SeekEnd)?; 78 | Ok(fd) 79 | } 80 | 81 | fn kmsg_contains_bug(fd: i32) -> Result { 82 | let mut buf: Vec = vec![0; 8192]; 83 | let mut found = false; 84 | 85 | // NB: make sure we consume all the entries in kmsg otherwise the next test might see entries 86 | // from the previous run 87 | loop { 88 | let n = unsafe { libc::read(fd, (&mut buf).as_mut_ptr() as *mut c_void, buf.len()) }; 89 | match n.cmp(&0) { 90 | cmp::Ordering::Equal => break, 91 | cmp::Ordering::Less => { 92 | let errno = Errno::from_i32(errno()); 93 | if errno == Errno::EAGAIN { 94 | // No more entries in kmsg 95 | break; 96 | } else { 97 | bail!("Failed to read from /dev/kmsg"); 98 | } 99 | } 100 | cmp::Ordering::Greater => { 101 | buf[n as usize] = 0; 102 | 103 | let line = String::from_utf8_lossy(&buf); 104 | if line.contains("BUG") || line.contains("UBSAN:") { 105 | found = true; 106 | } 107 | } 108 | } 109 | } 110 | 111 | Ok(found) 112 | } 113 | 114 | /// Get next testcase from AFL and write it into file `into` 115 | /// 116 | /// Returns true on success, false on no more input 117 | fn get_next_testcase>(into: P) -> Result { 118 | let mut buffer = Vec::new(); 119 | 120 | // AFL feeds inputs via stdin 121 | let stdin = io::stdin(); 122 | let mut handle = stdin.lock(); 123 | handle.read_to_end(&mut buffer)?; 124 | if buffer.is_empty() { 125 | return Ok(TestcaseStatus::NoMore); 126 | } 127 | 128 | // Decompress input 129 | let deserialized = from_read_ref(&buffer)?; 130 | let image = imgcompress::decompress(&deserialized)?; 131 | 132 | // Write out FS image 133 | let mut file = OpenOptions::new() 134 | .write(true) 135 | .create(true) 136 | .truncate(true) 137 | .open(into)?; 138 | 139 | file.write_all(&image)?; 140 | 141 | Ok(TestcaseStatus::Ok) 142 | } 143 | 144 | /// Reset btrfs device cache 145 | /// 146 | /// Necessary to clean up kernel state between test cases 147 | fn reset_btrfs_devices() -> Result<()> { 148 | let file = OpenOptions::new() 149 | .read(true) 150 | .write(true) 151 | .open("/dev/btrfs-control") 152 | .with_context(|| "Failed to open btrfs control file".to_string())?; 153 | let fd = file.as_raw_fd(); 154 | 155 | let args: BtrfsIoctlVolArgs = unsafe { std::mem::zeroed() }; 156 | unsafe { btrfs_forget_dev(fd, &args) } 157 | .with_context(|| "Failed to forget btrfs devs".to_string())?; 158 | 159 | Ok(()) 160 | } 161 | 162 | /// Test code 163 | /// 164 | /// Note how this doesn't return errors. That's because our definition of error is a kernel BUG() 165 | /// or panic. We expect that some operations here fail (such as mount(2)) 166 | fn work>(mounter: &mut Mounter, image: P, debug: bool) { 167 | let r = mounter.mount(image.as_ref(), "/mnt/btrfs"); 168 | 169 | if debug { 170 | match r { 171 | Ok(_) => (), 172 | Err(e) => { 173 | eprintln!("Mount error: {}", e); 174 | return; 175 | } 176 | } 177 | } 178 | 179 | let nested_dir_path = "/mnt/btrfs/one/two/three/four/five/six"; 180 | let ret = create_dir_all(nested_dir_path); 181 | if debug { 182 | match ret { 183 | Ok(_) => (), 184 | Err(e) => { 185 | eprintln!("Failed to create some directories in work fn: {}", e); 186 | return; 187 | } 188 | } 189 | } 190 | 191 | let mut file = match OpenOptions::new() 192 | .create(true) 193 | .write(true) 194 | .open(format!("{}/file", nested_dir_path)) 195 | { 196 | Ok(f) => f, 197 | Err(e) => { 198 | if debug { 199 | eprintln!("Failed to open test file: {}", e); 200 | } 201 | 202 | return; 203 | } 204 | }; 205 | 206 | match writeln!(file, "hello world") { 207 | Ok(_) => (), 208 | Err(e) => { 209 | if debug { 210 | eprintln!("Failed to write to test file: {}", e); 211 | return; 212 | } 213 | } 214 | } 215 | 216 | match file.sync_all() { 217 | Ok(()) => (), 218 | Err(e) => { 219 | if debug { 220 | eprintln!("Failed to sync test file: {}", e); 221 | return; 222 | } 223 | } 224 | } 225 | } 226 | 227 | /// Fork a child and execute test case. 228 | /// 229 | /// NB: Returning an error crashes the fuzzer. DO NOT return an error unless it's truly unrecoverable. 230 | fn fork_work_and_wait>( 231 | kcov: &mut Kcov, 232 | kmsg: i32, 233 | mounter: &mut Mounter, 234 | image: P, 235 | debug: bool, 236 | ) -> Result { 237 | const EXIT_OK: i32 = 88; 238 | const EXIT_BAD: i32 = 89; 239 | 240 | match fork()? { 241 | ForkResult::Parent { child } => { 242 | let res = waitpid(child, None)?; 243 | 244 | if kmsg_contains_bug(kmsg)? { 245 | return Ok(RunStatus::Failure); 246 | } 247 | 248 | match res { 249 | WaitStatus::Exited(pid, rc) => { 250 | if rc != EXIT_OK { 251 | bail!("Forked child={} had an unclean exit={}", pid, rc); 252 | } 253 | 254 | Ok(RunStatus::Success) 255 | } 256 | WaitStatus::Signaled(_, _, _) => Ok(RunStatus::Failure), 257 | _ => bail!("Unexpected waitpid() status={:?}", res), 258 | } 259 | } 260 | // Be careful not to return from the child branch -- we must always exit the child 261 | // process so the parent can reap our status. 262 | ForkResult::Child => { 263 | match kcov.enable() { 264 | Ok(_) => (), 265 | Err(e) => { 266 | eprintln!("Failed to enable kcov: {}", e); 267 | exit(EXIT_BAD); 268 | } 269 | } 270 | 271 | work(mounter, image, debug); 272 | 273 | // Kcov is automatically disabled when the child terminates 274 | exit(EXIT_OK); 275 | } 276 | } 277 | } 278 | 279 | fn _main() -> Result<()> { 280 | let opts = Opt::from_args(); 281 | 282 | // Initialize forkserver and handshake with AFL 283 | let mut forkserver = Forkserver::new()?; 284 | 285 | // Initialize kernel coverage interface 286 | let mut kcov = Kcov::new()?; 287 | 288 | // Open /dev/kmsg 289 | let kmsg = open_kmsg()?; 290 | 291 | // Create a persistent loopdev to use 292 | let mut mounter = Mounter::new()?; 293 | 294 | loop { 295 | // Tell AFL we want to start a new run 296 | forkserver.new_run()?; 297 | 298 | // Now pull the next testcase from AFL and write it to tmpfs 299 | match get_next_testcase(FUZZED_IMAGE_PATH)? { 300 | TestcaseStatus::Ok => (), 301 | TestcaseStatus::NoMore => break, 302 | }; 303 | 304 | // Reset kernel state 305 | reset_btrfs_devices()?; 306 | 307 | // Fork a child and perform test 308 | let status = 309 | fork_work_and_wait(&mut kcov, kmsg, &mut mounter, FUZZED_IMAGE_PATH, opts.debug)?; 310 | 311 | // When the child exits coverage is disabled so we're good to read memory mapped data here 312 | let coverage = kcov.coverage(); 313 | let size = coverage[0].load(Ordering::Relaxed); 314 | 315 | if opts.debug { 316 | println!("{} kcov entries", size); 317 | } 318 | 319 | // Report edge transitions to AFL 320 | let shmem = forkserver.shmem(); 321 | let mut prev_loc: u64 = 0xDEAD; // Our compile time "random" 322 | for i in 0..size { 323 | // First calculate which idx in shmem to write to 324 | let current_loc: u64 = coverage[i + 1].load(Ordering::Relaxed).try_into().unwrap(); 325 | // Mask with 0xFFFF for 16 bits b/c AFL_MAP_SIZE == 1 << 16 326 | let mixed: u64 = (current_loc & 0xFFFF) ^ prev_loc; 327 | prev_loc = (current_loc & 0xFFFF) >> 1; 328 | 329 | // Increment value in shmem 330 | let (val, overflow) = shmem[mixed as usize].overflowing_add(1); 331 | if overflow { 332 | shmem[mixed as usize] = u8::MAX; 333 | } else { 334 | shmem[mixed as usize] = val; 335 | } 336 | 337 | if opts.debug { 338 | println!("kcov entry: 0x{:x}", current_loc); 339 | } 340 | } 341 | 342 | // Report run status to AFL 343 | forkserver.report(status)?; 344 | } 345 | 346 | Ok(()) 347 | } 348 | 349 | fn main() { 350 | match _main() { 351 | Ok(_) => exit(0), 352 | Err(e) => { 353 | eprintln!("Unclean runner exit: {}", e); 354 | exit(1); 355 | } 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /src/runner/src/mount.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::os::unix::io::AsRawFd; 3 | use std::path::Path; 4 | use std::sync::atomic::{AtomicBool, Ordering}; 5 | 6 | use anyhow::{anyhow, bail, Context, Result}; 7 | use loopdev::{LoopControl, LoopDevice}; 8 | use nix::fcntl::{fcntl, FcntlArg, FdFlag}; 9 | use sys_mount::{FilesystemType, MountFlags, Unmount, UnmountFlags}; 10 | 11 | pub struct Mounter { 12 | loopdev: LoopDevice, 13 | /// If a file is attached to the loopdev 14 | attached: AtomicBool, 15 | } 16 | 17 | impl Mounter { 18 | pub fn new() -> Result { 19 | let control = 20 | LoopControl::open().with_context(|| "Failed to open loop control".to_string())?; 21 | let device = control 22 | .next_free() 23 | .with_context(|| "Failed to get next free loop dev".to_string())?; 24 | 25 | // Disable O_CLOEXEC on underlying loopdev FD so that instances of this mounter 26 | // may be used in forked child processes. 27 | let fd = device.as_raw_fd(); 28 | let mut flags = FdFlag::from_bits(fcntl(fd, FcntlArg::F_GETFD)?) 29 | .ok_or_else(|| anyhow!("Failed to interpret FdFlag"))?; 30 | flags &= !FdFlag::FD_CLOEXEC; 31 | fcntl(fd, FcntlArg::F_SETFD(flags))?; 32 | 33 | Ok(Self { 34 | loopdev: device, 35 | attached: AtomicBool::new(false), 36 | }) 37 | } 38 | 39 | pub fn mount>(&mut self, src: P, dest: &'static str) -> Result { 40 | // Will fail if directory already exists 41 | let _ = fs::create_dir(dest); 42 | 43 | if self.attached.load(Ordering::SeqCst) { 44 | bail!("Loop dev is still being used by a previous mount"); 45 | } 46 | 47 | self.loopdev 48 | .attach_file(src) 49 | .with_context(|| "Failed to attach file to loop dev".to_string())?; 50 | self.attached.store(true, Ordering::SeqCst); 51 | 52 | let mount = sys_mount::Mount::new( 53 | self.loopdev 54 | .path() 55 | .ok_or_else(|| anyhow!("Failed to get path of loop dev"))?, 56 | dest, 57 | FilesystemType::Manual("btrfs"), 58 | MountFlags::empty(), 59 | None, 60 | ) 61 | .with_context(|| "Failed to mount btrfs image".to_string()); 62 | 63 | match mount { 64 | Ok(m) => Ok(Mount { 65 | inner: m, 66 | loopdev: &self.loopdev, 67 | attached: &self.attached, 68 | }), 69 | Err(e) => { 70 | // Be careful to detach the backing file from the loopdev if the mount fails, 71 | // otherwise following attaches will fail with EBUSY 72 | self.loopdev.detach()?; 73 | self.attached.store(false, Ordering::SeqCst); 74 | Err(e) 75 | } 76 | } 77 | } 78 | } 79 | 80 | impl Drop for Mounter { 81 | fn drop(&mut self) { 82 | // Panic here if detaching fails b/c otherwise we'd slowly leak resources. 83 | if self.attached.load(Ordering::SeqCst) { 84 | self.loopdev.detach().unwrap(); 85 | } 86 | } 87 | } 88 | 89 | /// A mounted filesystem. 90 | /// 91 | /// Will umount on drop. 92 | pub struct Mount<'a> { 93 | inner: sys_mount::Mount, 94 | loopdev: &'a LoopDevice, 95 | attached: &'a AtomicBool, 96 | } 97 | 98 | impl<'a> Drop for Mount<'a> { 99 | fn drop(&mut self) { 100 | // Panic here if detaching fails b/c otherwise we'd slowly leak resources. 101 | self.inner.unmount(UnmountFlags::empty()).unwrap(); 102 | self.loopdev.detach().unwrap(); 103 | self.attached.store(false, Ordering::SeqCst); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /x.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | 3 | import argparse 4 | import os 5 | import pathlib 6 | import shutil 7 | import subprocess 8 | import sys 9 | 10 | import src.manager as manager 11 | 12 | DOCKER_IMAGE_REMOTE = "dxuu/btrfs-fuzz" 13 | DOCKER_IMAGE_LOCAL = "localhost/btrfs-fuzz" 14 | 15 | 16 | def sh(cmd): 17 | try: 18 | subprocess.run(cmd, shell=True, check=True) 19 | except subprocess.CalledProcessError as e: 20 | sys.exit(1) 21 | 22 | 23 | # Docker tends to freak out if a directory begins with `_` 24 | def sanitize_docker_dir(dir): 25 | if dir[0] == "/": 26 | return dir 27 | else: 28 | return "./" + dir 29 | 30 | 31 | def cmd_build(args): 32 | if args.buildah: 33 | tool = "buildah" 34 | else: 35 | tool = "podman" 36 | 37 | if args.remote: 38 | sh(f"{tool} pull {DOCKER_IMAGE_REMOTE}") 39 | else: 40 | if args.buildah: 41 | tool += " build-using-dockerfile" 42 | tool += " --runtime $(which runc)" 43 | else: 44 | tool += " build" 45 | 46 | build_args = "" 47 | if args.kernel_repo: 48 | build_args += f"--build-arg KERNEL_REPO={args.kernel_repo} " 49 | 50 | if args.kernel_branch: 51 | build_args += f"--build-arg KERNEL_BRANCH={args.kernel_branch} " 52 | 53 | sh(f"{tool} {build_args} -t btrfs-fuzz .") 54 | 55 | 56 | def cmd_build_tar(args): 57 | # First build the latest image 58 | if not args.no_build: 59 | cmd_build(args) 60 | 61 | tmpname = "btrfs-fuzz-tmp" 62 | 63 | if args.zstd and not args.file.endswith(".tzst"): 64 | args.file = args.file + ".tzst" 65 | elif not args.file.endswith(".tar"): 66 | args.file = args.file + ".tar" 67 | 68 | c = ["podman export"] 69 | c.append("$(") 70 | c.append("podman create --name") 71 | c.append(tmpname) 72 | 73 | if args.remote: 74 | c.append(DOCKER_IMAGE_REMOTE) 75 | else: 76 | c.append(DOCKER_IMAGE_LOCAL) 77 | 78 | c.append("/bin/true") 79 | c.append(")") 80 | 81 | if args.zstd: 82 | c.append("|") 83 | c.append("zstd") 84 | c.append("-f") 85 | 86 | # Both `podman export` and zstd take the `-o OUTPUT` flag 87 | c.append("-o") 88 | c.append(args.file) 89 | 90 | sh(" ".join(c)) 91 | sh(f"podman rm {tmpname}") 92 | 93 | 94 | def cmd_run(args): 95 | print("Starting btrfs-fuzz") 96 | 97 | if args.remote: 98 | img = DOCKER_IMAGE_REMOTE 99 | else: 100 | img = DOCKER_IMAGE_LOCAL 101 | 102 | nspawn = False 103 | if args.fs_dir: 104 | # Must be root to use systemd-nspawn 105 | if os.geteuid() != 0: 106 | print("--fs-dir requires root") 107 | return 108 | 109 | img = args.fs_dir 110 | nspawn = True 111 | 112 | state_dir = sanitize_docker_dir(args.state_dir) 113 | 114 | m = manager.Manager(img, state_dir, nspawn=nspawn, parallel=args.parallel) 115 | m.run() 116 | 117 | 118 | def cmd_shell(args): 119 | c = ["podman run"] 120 | c.append("-it") 121 | c.append("--privileged") 122 | 123 | if args.state_dir: 124 | c.append(f"-v {sanitize_docker_dir(args.state_dir)}:/state") 125 | 126 | if args.remote: 127 | c.append(DOCKER_IMAGE_REMOTE) 128 | else: 129 | c.append(DOCKER_IMAGE_LOCAL) 130 | 131 | sh(" ".join(c)) 132 | 133 | 134 | def cmd_seed(args): 135 | if pathlib.Path(args.state_dir).exists(): 136 | print(f"{args.state_dir} already exists, noop-ing") 137 | return 138 | 139 | pathlib.Path.mkdir(pathlib.Path(f"{args.state_dir}/input"), parents=True) 140 | pathlib.Path.mkdir(pathlib.Path(f"{args.state_dir}/output")) 141 | pathlib.Path.mkdir(pathlib.Path(f"{args.state_dir}/known_crashes")) 142 | 143 | # Generate raw image 144 | image_path = pathlib.Path(f"{args.state_dir}/input/image") 145 | with open(image_path, "wb") as i: 146 | # 120 MB is just about the minimum size for a raw btrfs image 147 | i.truncate(120 << 20) 148 | 149 | sh(f"mkfs.btrfs {image_path}") 150 | 151 | # Compress raw image into a new file and then remove the raw image 152 | compressed_image_path = f"{args.state_dir}/input/img_compressed" 153 | sh(f"cargo run --bin imgcompress -- compress {image_path} {compressed_image_path}") 154 | sh(f"rm {image_path}") 155 | 156 | # Copy files from checked in corpus over too 157 | corpus_files = os.listdir("./corpus") 158 | for f in corpus_files: 159 | assert f.endswith(".zst") 160 | raw_fname = f[:-4] 161 | raw_path = f"{args.state_dir}/input/{raw_fname}" 162 | assert raw_path.endswith(".raw") 163 | compressed_fname = raw_fname[:-4] 164 | compressed_path = f"{args.state_dir}/input/{compressed_fname}" 165 | 166 | sh(f"zstd -d ./corpus/{f} -o {raw_path}") 167 | sh(f"cargo run -p imgcompress compress -- {raw_path} {compressed_path}") 168 | sh(f"rm {raw_path}") 169 | 170 | # Write a readme to describe what each directory contains 171 | readme_path = pathlib.Path(f"{args.state_dir}/README") 172 | with open(readme_path, "w") as f: 173 | content = "This directory holds all the state for a fuzzing session.\n\n" 174 | content += "Each subdirectory contains as follows:\n\n" 175 | content += ( 176 | "known_crashes: test cast images that are known to cause a kernel panic\n" 177 | ) 178 | content += "input: afl++ input directory\n" 179 | content += "output: afl++ output directory\n" 180 | f.write(content) 181 | 182 | 183 | def cmd_repro(args): 184 | import pexpect 185 | 186 | print(f"Reproducing {args.image}") 187 | 188 | # Share the entire directory containing the image under test 189 | image_dir = str(pathlib.Path(args.image).parent) 190 | if image_dir[0] != "/": 191 | # Necessary so docker doesn't freak out 192 | image_dir = "./" + image_dir 193 | 194 | image_fname = str(pathlib.Path(args.image).name) 195 | 196 | c = ["podman run"] 197 | c.append("-it") 198 | c.append("--privileged") 199 | c.append(f"-v {image_dir}:/state") 200 | 201 | if args.remote: 202 | c.append(DOCKER_IMAGE_REMOTE) 203 | else: 204 | c.append(DOCKER_IMAGE_LOCAL) 205 | 206 | p = pexpect.spawn(" ".join(c), encoding="utf-8") 207 | p.expect("root@.*#") 208 | p.sendline('/bin/bash -c "echo core > /proc/sys/kernel/core_pattern"') 209 | 210 | c = [] 211 | c.append("/btrfs-fuzz/runner") 212 | c.append(f"< /state/{image_fname}") 213 | 214 | p.expect("root@.*#") 215 | 216 | if args.exit: 217 | # Send child output to stdout 218 | p.logfile_read = sys.stdout 219 | p.sendline(" ".join(c)) 220 | 221 | p.expect("root@.*#") 222 | 223 | # `C-a x` to exit qemu 224 | p.sendcontrol("a") 225 | p.send("x") 226 | else: 227 | p.sendline(" ".join(c)) 228 | 229 | # Give control back to terminal 230 | p.interact() 231 | 232 | 233 | def cmd_push(args): 234 | c = ["podman push"] 235 | c.append(DOCKER_IMAGE_LOCAL) 236 | c.append(f"docker://docker.io/{DOCKER_IMAGE_REMOTE}:latest") 237 | 238 | sh(" ".join(c)) 239 | 240 | 241 | def main(): 242 | parser = argparse.ArgumentParser( 243 | prog="x", formatter_class=argparse.ArgumentDefaultsHelpFormatter 244 | ) 245 | parser.add_argument("--remote", action="store_true", help="Use remote docker image") 246 | parser.set_defaults(func=lambda _: parser.print_help()) 247 | 248 | subparsers = parser.add_subparsers(help="subcommands") 249 | 250 | build = subparsers.add_parser("build", help="build btrfs-fuzz components") 251 | build.add_argument( 252 | "-b", 253 | "--buildah", 254 | action="store_true", 255 | help="Use buildah to build image", 256 | ) 257 | build.add_argument( 258 | "--kernel-repo", 259 | type=str, 260 | help="Kernel repo to clone", 261 | ) 262 | build.add_argument( 263 | "--kernel-branch", 264 | type=str, 265 | help="Kernel branch to build from", 266 | ) 267 | build.set_defaults(func=cmd_build) 268 | 269 | build_tar = subparsers.add_parser( 270 | "build-tar", help="build btrfs-fuzz image into a tarball" 271 | ) 272 | build_tar.add_argument( 273 | "file", 274 | type=str, 275 | help="Filename for output tarball", 276 | ) 277 | build_tar.add_argument( 278 | "--zstd", 279 | action="store_true", 280 | help="zstd compress archive", 281 | ) 282 | build_tar.add_argument( 283 | "--no-build", 284 | action="store_true", 285 | help="Do not rebuild image if stale", 286 | ) 287 | build_tar.add_argument( 288 | "-b", 289 | "--buildah", 290 | action="store_true", 291 | help="Use buildah to build image", 292 | ) 293 | build_tar.set_defaults(func=cmd_build_tar) 294 | 295 | run = subparsers.add_parser("run", help="run fuzzer") 296 | run.add_argument( 297 | "-s", 298 | "--state-dir", 299 | type=str, 300 | default="./_state", 301 | help="Shared state directory between host and VM, mounted in VM at " 302 | "/state. The directory must contain `input` and `output` " 303 | "subdirectories, with `input` containing initial test cases.", 304 | ) 305 | run.add_argument( 306 | "-f", 307 | "--fs-dir", 308 | type=str, 309 | help="Use systemd-nspawn instead of docker to run container. " 310 | "The provided argument is the directory to use as filesystem " 311 | "root (typically generated by `build-tar`). Requires root. " 312 | "When active, --remote is ignored.", 313 | ) 314 | run.add_argument( 315 | "--parallel", 316 | type=int, 317 | help="Fuzz in parallel on PARALLEL # of cpus. -1 means all cpus.", 318 | ) 319 | run.set_defaults(func=cmd_run) 320 | 321 | shell = subparsers.add_parser("shell", help="start shell in VM") 322 | shell.add_argument( 323 | "-s", 324 | "--state-dir", 325 | type=str, 326 | default="./_state", 327 | help="Shared state directory between host and VM, mounted in VM at /state", 328 | ) 329 | shell.set_defaults(func=cmd_shell) 330 | 331 | seed = subparsers.add_parser("seed", help="seed input corpus") 332 | seed.add_argument( 333 | "-s", 334 | "--state-dir", 335 | type=str, 336 | default="./_state", 337 | help="Shared state directory between host and VM", 338 | ) 339 | seed.set_defaults(func=cmd_seed) 340 | 341 | repro = subparsers.add_parser("repro", help="reproduce a test case") 342 | repro.add_argument( 343 | "image", 344 | type=str, 345 | help="btrfs filesystem image to test against (must be imgcompress-compressed)", 346 | ) 347 | repro.add_argument( 348 | "--exit", 349 | action="store_true", 350 | help="Exit VM after repro runs (useful for scripting)", 351 | ) 352 | repro.set_defaults(func=cmd_repro) 353 | 354 | push = subparsers.add_parser("push", help="push local image to docker hub") 355 | push.set_defaults(func=cmd_push) 356 | 357 | help = subparsers.add_parser("help", help="print help") 358 | help.set_defaults(func=lambda _: parser.print_help()) 359 | 360 | args = parser.parse_args() 361 | args.func(args) 362 | 363 | 364 | if __name__ == "__main__": 365 | proj_dir = pathlib.Path(__file__).parent 366 | os.chdir(proj_dir) 367 | 368 | main() 369 | --------------------------------------------------------------------------------