chore: some fuzz fixes

This commit is contained in:
Alexander Medvedev
2026-08-15 13:35:14 +02:00
parent 6e94e6dfca
commit a2b92e6dc5
36 changed files with 1466 additions and 2814 deletions

223
Cargo.lock generated
View File

@@ -99,12 +99,56 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
]
[[package]]
name = "anyhow"
version = "1.0.104"
@@ -639,6 +683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
@@ -647,8 +692,22 @@ version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
@@ -681,6 +740,12 @@ dependencies = [
"thiserror 2.0.20",
]
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "colored"
version = "3.1.1"
@@ -1775,6 +1840,18 @@ dependencies = [
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi 5.3.0",
"wasip2",
]
[[package]]
name = "getrandom"
version = "0.4.3"
@@ -1783,7 +1860,7 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"r-efi 6.0.0",
"rand_core 0.10.1",
]
@@ -2277,6 +2354,12 @@ version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.13.0"
@@ -2770,6 +2853,12 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "oorandom"
version = "11.1.5"
@@ -3118,6 +3207,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "pretty_assertions"
version = "1.4.1"
@@ -3316,7 +3414,7 @@ dependencies = [
"pumpkin-protocol",
"pumpkin-util",
"pumpkin-world",
"rand",
"rand 0.10.2",
"rayon",
"rsa",
"rustc-hash",
@@ -3338,8 +3436,8 @@ dependencies = [
"tracing-subscriber",
"ureq",
"uuid",
"wasm-encoder 0.255.0",
"wasmparser 0.255.0",
"wasm-encoder 0.256.0",
"wasmparser 0.256.0",
"wasmtime",
"wasmtime-wasi",
"wasmtime-wasi-http",
@@ -3405,10 +3503,23 @@ dependencies = [
"phf",
"pumpkin-nbt",
"pumpkin-util",
"rand",
"rand 0.10.2",
"serde",
]
[[package]]
name = "pumpkin-fuzzer"
version = "0.1.0-dev+26.2-26.40"
dependencies = [
"bytes",
"clap",
"colored",
"pumpkin-protocol",
"pumpkin-util",
"rand 0.9.5",
"tokio",
]
[[package]]
name = "pumpkin-inventory"
version = "0.1.0-dev+26.2-26.40"
@@ -3417,7 +3528,7 @@ dependencies = [
"pumpkin-protocol",
"pumpkin-util",
"pumpkin-world",
"rand",
"rand 0.10.2",
"thiserror 2.0.20",
"tokio",
"tracing",
@@ -3458,7 +3569,7 @@ dependencies = [
"serde_json",
"tracing",
"tracing-serde-structured",
"wit-bindgen",
"wit-bindgen 0.60.0",
]
[[package]]
@@ -3532,7 +3643,7 @@ dependencies = [
"pumpkin-data",
"pumpkin-nbt",
"pumpkin-util",
"rand",
"rand 0.10.2",
"rayon",
"rustc-hash",
"ruzstd",
@@ -3578,6 +3689,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
@@ -3593,6 +3710,16 @@ dependencies = [
"ptr_meta",
]
[[package]]
name = "rand"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
dependencies = [
"rand_chacha",
"rand_core 0.9.5",
]
[[package]]
name = "rand"
version = "0.10.2"
@@ -3604,6 +3731,16 @@ dependencies = [
"rand_core 0.10.1",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.5",
]
[[package]]
name = "rand_core"
version = "0.6.4"
@@ -3613,6 +3750,15 @@ dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rand_core"
version = "0.10.1"
@@ -3817,7 +3963,7 @@ dependencies = [
"bytes",
"hex",
"log",
"rand",
"rand 0.10.2",
"rcgen",
"ring",
"rtc-datachannel",
@@ -3874,7 +4020,7 @@ dependencies = [
"log",
"p256",
"p384 0.13.1",
"rand",
"rand 0.10.2",
"rand_core 0.6.4",
"rcgen",
"ring",
@@ -3898,7 +4044,7 @@ dependencies = [
"bytes",
"crc",
"log",
"rand",
"rand 0.10.2",
"rtc-mdns",
"rtc-shared",
"rtc-stun",
@@ -3915,7 +4061,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f66ea5650a91773d10e4d98729f6fe256681ec1314c7ded5839fa34867305f79"
dependencies = [
"log",
"rand",
"rand 0.10.2",
"rtc-interceptor-derive",
"rtc-rtcp",
"rtc-rtp",
@@ -3955,7 +4101,7 @@ checksum = "4fbc2f460a321eccdc0907819f8fc7027036edcbd94620c3328c1aefb070dfe5"
dependencies = [
"byteorder",
"bytes",
"rand",
"rand 0.10.2",
"rtc-rtp",
"rtc-shared",
"thiserror 2.0.20",
@@ -3979,7 +4125,7 @@ checksum = "aac1a05add837d45d562f48bc769c9b264440741aab319fed48ce6a696e6aa0c"
dependencies = [
"bytes",
"memchr",
"rand",
"rand 0.10.2",
"rtc-shared",
"serde",
]
@@ -3993,7 +4139,7 @@ dependencies = [
"bytes",
"crc32c",
"log",
"rand",
"rand 0.10.2",
"rtc-shared",
"rustc-hash",
"slab",
@@ -4006,7 +4152,7 @@ version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0379bb552d4813e042d376db99f2005038017fc7864676fc835334b810db015"
dependencies = [
"rand",
"rand 0.10.2",
"rtc-shared",
"url",
]
@@ -4023,7 +4169,7 @@ dependencies = [
"bytes",
"nix",
"p256",
"rand",
"rand 0.10.2",
"rcgen",
"sec1 0.7.3",
"serde",
@@ -4063,7 +4209,7 @@ dependencies = [
"crc",
"lazy_static",
"md-5",
"rand",
"rand 0.10.2",
"ring",
"rtc-shared",
"sansio",
@@ -4545,6 +4691,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "substring"
version = "1.4.5"
@@ -4765,6 +4917,7 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@@ -5193,6 +5346,15 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.4+wasi-0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
dependencies = [
"wit-bindgen 0.57.1",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.127"
@@ -5260,12 +5422,12 @@ dependencies = [
[[package]]
name = "wasm-encoder"
version = "0.255.0"
version = "0.256.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b524283fb5df62eec102ed0574838961bdd7ba5ac9c50d38e2756c51c971a42"
checksum = "ec1492381bfd5ea51c2a99a919b676662559925cb8d7490547ec2e14c1ad3eb1"
dependencies = [
"leb128fmt",
"wasmparser 0.255.0",
"wasmparser 0.256.0",
]
[[package]]
@@ -5310,6 +5472,17 @@ name = "wasmparser"
version = "0.255.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8e329ef4b5d46e73b91d3ac6924417cad55a8cbbf869c199283383427c3320b"
dependencies = [
"bitflags 2.13.1",
"indexmap",
"semver",
]
[[package]]
name = "wasmparser"
version = "0.256.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60bd825ffedc6cba8a642924ba7ae424afbc47811cffbcb7b92031ec24e59b4c"
dependencies = [
"bitflags 2.13.1",
"hashbrown 0.17.1",
@@ -5570,7 +5743,7 @@ dependencies = [
"cfg-if",
"futures",
"io-lifetimes 3.0.1",
"rand",
"rand 0.10.2",
"rustix",
"thiserror 2.0.20",
"tokio",
@@ -5972,6 +6145,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "wit-bindgen"
version = "0.60.0"

View File

@@ -14,6 +14,7 @@ members = [
"crates/pumpkin-util",
"crates/pumpkin-world",
"tools/pumpkin-codegen",
"tools/pumpkin-fuzzer",
]
# [workspace.lints.rust]

View File

@@ -940,15 +940,20 @@ pub trait ScreenHandler: Send + Sync {
let mut cursor_stack = behaviour.cursor_stack.lock().await;
let initial_count = cursor_stack.item_count;
let slots_count = behaviour.drag_slots.len();
for slot_index in &behaviour.drag_slots {
let slot = behaviour.slots[*slot_index as usize].clone();
let Some(slot) = behaviour.slots.get(*slot_index as usize).cloned() else {
continue;
};
let stack = slot.get_stack().await;
if (stack.are_items_and_components_equal(&cursor_stack) || stack.is_empty())
&& slot.can_insert(&cursor_stack).await
{
let mut inserting_count = match drag_button {
0 => initial_count / behaviour.drag_slots.len() as u8,
0 => (initial_count as usize)
.checked_div(slots_count)
.map_or(0, |c| c as u8),
1 => 1,
2 => {
cursor_stack.item_count = cursor_stack.get_max_stack_size();

View File

@@ -17,6 +17,21 @@ dependencies = [
"derive_arbitrary",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "bytes"
version = "1.12.0"
@@ -56,6 +71,16 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
@@ -68,10 +93,14 @@ dependencies = [
]
[[package]]
name = "either"
version = "1.16.0"
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "find-msvc-tools"
@@ -89,6 +118,40 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.3.4"
@@ -97,10 +160,21 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"r-efi 5.3.0",
"wasip2",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
]
[[package]]
name = "itoa"
version = "1.0.18"
@@ -113,10 +187,21 @@ version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom",
"getrandom 0.3.4",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.186"
@@ -133,6 +218,16 @@ dependencies = [
"cc",
]
[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest",
]
[[package]]
name = "memchr"
version = "2.8.0"
@@ -172,24 +267,23 @@ dependencies = [
[[package]]
name = "pumpkin-codecs"
version = "0.1.0-dev+26.1"
version = "0.1.0-dev+26.2-26.40"
dependencies = [
"either",
"serde_json",
"tracing",
]
[[package]]
name = "pumpkin-nbt"
version = "0.1.0-dev+26.1"
version = "0.1.0-dev+26.2-26.40"
dependencies = [
"bytes",
"cesu8",
"flate2",
"pumpkin-codecs",
"serde",
"thiserror",
"tracing",
"uuid",
]
[[package]]
@@ -199,7 +293,6 @@ dependencies = [
"arbitrary",
"libfuzzer-sys",
"pumpkin-nbt",
"serde",
]
[[package]]
@@ -217,6 +310,18 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "serde"
version = "1.0.228"
@@ -224,7 +329,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
@@ -272,6 +376,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "syn"
version = "2.0.117"
@@ -334,12 +444,37 @@ dependencies = [
"once_cell",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "uuid"
version = "1.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9"
dependencies = [
"getrandom 0.4.3",
"js-sys",
"md-5",
"serde_core",
"wasm-bindgen",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasip2"
version = "1.0.3+wasi-0.2.9"
@@ -349,6 +484,51 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"

View File

@@ -51,6 +51,18 @@ impl NbtCompound {
/// Advances a reader past a compound's payload without allocating its tags.
pub fn skip_content<'a, R: NbtReadHelper<'a>>(reader: &mut R) -> Result<(), Error> {
Self::skip_content_depth(reader, 0)
}
/// Advances a reader past a compound's payload with depth tracking.
pub fn skip_content_depth<'a, R: NbtReadHelper<'a>>(
reader: &mut R,
depth: usize,
) -> Result<(), Error> {
if depth > crate::MAX_NBT_DEPTH {
return Err(Error::MaxDepthExceeded);
}
loop {
let tag_id = match reader.get_u8() {
Ok(id) => id,
@@ -65,7 +77,7 @@ impl NbtCompound {
reader.skip_string()?;
// Skip Value
NbtTag::skip_data(reader, tag_id)?;
NbtTag::skip_data_depth(reader, tag_id, depth + 1)?;
}
Ok(())
@@ -73,6 +85,18 @@ impl NbtCompound {
/// Deserializes a compound payload, starting after the compound's name.
pub fn deserialize_content<'a, R: NbtReadHelper<'a>>(reader: &mut R) -> Result<Self, Error> {
Self::deserialize_content_depth(reader, 0)
}
/// Deserializes a compound payload with depth tracking.
pub fn deserialize_content_depth<'a, R: NbtReadHelper<'a>>(
reader: &mut R,
depth: usize,
) -> Result<Self, Error> {
if depth > crate::MAX_NBT_DEPTH {
return Err(Error::MaxDepthExceeded);
}
let mut compound = Self::new();
loop {
@@ -87,7 +111,7 @@ impl NbtCompound {
}
let name = reader.get_string()?;
let tag = NbtTag::deserialize_data(reader, tag_id)?;
let tag = NbtTag::deserialize_data_depth(reader, tag_id, depth + 1)?;
compound.child_tags.insert(name.into(), tag);
}

View File

@@ -63,7 +63,9 @@ pub const INT_ARRAY_ID: u8 = 0x0B;
pub const LONG_ARRAY_ID: u8 = 0x0C;
/// Maximum number of elements accepted when decoding a list or array.
pub const MAX_ARRAY_LENGTH: usize = 2_000_000;
pub const MAX_ARRAY_LENGTH: usize = 512_000;
/// Maximum nesting depth allowed when decoding NBT compound or list tags.
pub const MAX_NBT_DEPTH: usize = 512;
/// Errors produced while reading, writing, or converting NBT data.
#[derive(Error, Debug)]
@@ -101,6 +103,12 @@ pub enum Error {
/// A Bedrock variable-length long exceeded its maximum encoded size.
#[error("Failed to decode varlong - value too large")]
VarLongTooLarge,
/// NBT nesting depth exceeded the maximum allowed limit.
#[error("NBT depth exceeded maximum allowed limit")]
MaxDepthExceeded,
/// A list tag specified an invalid element tag type.
#[error("Invalid element tag type for list: {0}")]
InvalidListTag(u8),
}
/// A complete NBT document containing a named root compound.

View File

@@ -6,8 +6,7 @@ use serializer::NbtWriteHelper;
use crate::{
BYTE_ARRAY_ID, BYTE_ID, COMPOUND_ID, DOUBLE_ID, END_ID, Error, FLOAT_ID, INT_ARRAY_ID, INT_ID,
LIST_ID, LONG_ARRAY_ID, LONG_ID, MAX_ARRAY_LENGTH, SHORT_ID, STRING_ID, compound, deserializer,
serializer,
LIST_ID, LONG_ARRAY_ID, LONG_ID, SHORT_ID, STRING_ID, compound, deserializer, serializer,
};
/// A value represented by one of the tag types defined by the NBT format.
@@ -203,6 +202,19 @@ impl NbtTag {
/// Advances a reader past the payload belonging to `tag_id`.
pub fn skip_data<'a, R: NbtReadHelper<'a>>(reader: &mut R, tag_id: u8) -> Result<(), Error> {
Self::skip_data_depth(reader, tag_id, 0)
}
/// Advances a reader past the payload belonging to `tag_id` with depth tracking.
pub fn skip_data_depth<'a, R: NbtReadHelper<'a>>(
reader: &mut R,
tag_id: u8,
depth: usize,
) -> Result<(), Error> {
if depth > crate::MAX_NBT_DEPTH {
return Err(Error::MaxDepthExceeded);
}
match tag_id {
END_ID => Ok(()),
BYTE_ID => reader.skip_i8(),
@@ -216,7 +228,11 @@ impl NbtTag {
if len < 0 {
return Err(Error::NegativeLength(len));
}
reader.skip_bytes(i64::from(len))
let len = len as usize;
if len > crate::MAX_ARRAY_LENGTH {
return Err(Error::LargeLength(len));
}
reader.skip_bytes(len as i64)
}
STRING_ID => reader.skip_string(),
LIST_ID => {
@@ -225,20 +241,33 @@ impl NbtTag {
if len < 0 {
return Err(Error::NegativeLength(len));
}
if tag_type_id == END_ID && len > 0 {
return Err(Error::InvalidListTag(tag_type_id));
}
let len = len as usize;
if len > crate::MAX_ARRAY_LENGTH {
return Err(Error::LargeLength(len));
}
for _ in 0..len {
Self::skip_data(reader, tag_type_id)?;
Self::skip_data_depth(reader, tag_type_id, depth + 1)?;
}
Ok(())
}
COMPOUND_ID => NbtCompound::skip_content(reader),
COMPOUND_ID => NbtCompound::skip_content_depth(reader, depth + 1),
INT_ARRAY_ID => {
let len = reader.get_i32()?;
if len < 0 {
return Err(Error::NegativeLength(len));
}
let len = len as usize;
if len > crate::MAX_ARRAY_LENGTH {
return Err(Error::LargeLength(len));
}
for _ in 0..len {
reader.skip_i32()?;
}
@@ -251,6 +280,11 @@ impl NbtTag {
return Err(Error::NegativeLength(len));
}
let len = len as usize;
if len > crate::MAX_ARRAY_LENGTH {
return Err(Error::LargeLength(len));
}
for _ in 0..len {
reader.skip_i64()?;
}
@@ -266,6 +300,20 @@ impl NbtTag {
reader: &mut R,
tag_id: u8,
) -> Result<Self, Error> {
Self::deserialize_data_depth(reader, tag_id, 0)
}
/// Deserializes a payload whose type is identified by `tag_id` with depth tracking.
#[allow(clippy::too_many_lines)]
pub fn deserialize_data_depth<'a, R: NbtReadHelper<'a>>(
reader: &mut R,
tag_id: u8,
depth: usize,
) -> Result<Self, Error> {
if depth > crate::MAX_NBT_DEPTH {
return Err(Error::MaxDepthExceeded);
}
match tag_id {
END_ID => Ok(Self::End),
BYTE_ID => {
@@ -299,10 +347,10 @@ impl NbtTag {
}
let len = len as usize;
if len > MAX_ARRAY_LENGTH {
if len > crate::MAX_ARRAY_LENGTH {
return Err(Error::LargeLength(len));
}
let mut byte_array = Vec::with_capacity(len);
let mut byte_array = Vec::with_capacity(len.min(4096));
for _ in 0..len {
let byte = reader.get_i8()?;
byte_array.push(byte);
@@ -316,22 +364,30 @@ impl NbtTag {
if len < 0 {
return Err(Error::NegativeLength(len));
}
if tag_type_id == END_ID && len > 0 {
return Err(Error::InvalidListTag(tag_type_id));
}
let len = len as usize;
if len > MAX_ARRAY_LENGTH {
if len > crate::MAX_ARRAY_LENGTH {
return Err(Error::LargeLength(len));
}
let mut list = Vec::with_capacity(len);
let mut list = Vec::with_capacity(len.min(4096));
for _ in 0..len {
let tag = Self::deserialize_data(reader, tag_type_id)?;
assert_eq!(tag.get_type_id(), tag_type_id);
let tag = Self::deserialize_data_depth(reader, tag_type_id, depth + 1)?;
if tag.get_type_id() != tag_type_id {
return Err(Error::InvalidListTag(tag.get_type_id()));
}
// Try unwrapping the tag.
list.push(Self::flatten(tag));
}
Ok(Self::List(list))
}
COMPOUND_ID => Ok(Self::Compound(NbtCompound::deserialize_content(reader)?)),
COMPOUND_ID => Ok(Self::Compound(NbtCompound::deserialize_content_depth(
reader,
depth + 1,
)?)),
INT_ARRAY_ID => {
let len = reader.get_i32()?;
if len < 0 {
@@ -339,10 +395,11 @@ impl NbtTag {
}
let len = len as usize;
if len > MAX_ARRAY_LENGTH {
if len > crate::MAX_ARRAY_LENGTH {
return Err(Error::LargeLength(len));
}
let mut int_array = Vec::with_capacity(len);
let mut int_array = Vec::with_capacity(len.min(4096));
for _ in 0..len {
let int = reader.get_i32()?;
int_array.push(int);
@@ -356,10 +413,11 @@ impl NbtTag {
}
let len = len as usize;
if len > MAX_ARRAY_LENGTH {
if len > crate::MAX_ARRAY_LENGTH {
return Err(Error::LargeLength(len));
}
let mut long_array = Vec::with_capacity(len);
let mut long_array = Vec::with_capacity(len.min(4096));
for _ in 0..len {
let long = reader.get_i64()?;
long_array.push(long);

File diff suppressed because it is too large Load Diff

View File

@@ -19,9 +19,6 @@ libfuzzer-sys = "0.4"
[dependencies.pumpkin-protocol]
path = ".."
[dependencies.pumpkin]
path = "../../pumpkin"
[dependencies.pumpkin-util]
path = "../../pumpkin-util"

View File

@@ -2,18 +2,14 @@
use libfuzzer_sys::fuzz_target;
use pumpkin_protocol::bedrock::packet_decoder::BedrockBatchDecoder;
use pumpkin_protocol::bedrock::server::{
client_cache_status::SClientCacheStatus,
command_request::SCommandRequest,
container_close::SContainerClose,
interaction::SInteraction,
loading_screen::SLoadingScreen,
login::SLogin,
player_auth_input::SPlayerAuthInput,
request_chunk_radius::SRequestChunkRadius,
request_network_settings::SRequestNetworkSettings,
text::SText,
SAnimate, SBlockPickRequest, SClientCacheBlobStatus, SClientCacheStatus, SCommandRequest,
SContainerClose, SEmote, SEmoteList, SInteraction, SInventoryTransaction, SItemStackRequest,
SLoadingScreen, SLogin, SMobEquipment, SModalFormResponse, SPacketViolationWarning,
SPlayerAction, SPlayerAuthInput, SPlayerHotbar, SRequestAbility, SRequestChunkRadius,
SRequestNetworkSettings, SResourcePackResponse, SRespawn, SSetLocalPlayerAsInitialized,
SSetPlayerInventoryOptions, SText,
};
use pumpkin_protocol::serial::PacketRead;
use pumpkin_protocol::serial::{PacketRead, PacketReadSlice};
use std::io::Cursor;
// ---------------------------------------------------------------------------
@@ -26,25 +22,53 @@ fn fuzz_serverbound_packets(payload: &[u8]) {
($($packet:ty),* $(,)?) => {
$(
cursor.set_position(0);
// Attempt to read as a standard Game Packet (with version)
let _ = <$packet>::read(&mut cursor);
)*
};
}
// Standard Bedrock Serverbound Packets
macro_rules! run_read_slice {
($($packet:ty),* $(,)?) => {
$(
let mut slice = payload;
let _ = <$packet>::read_slice(&mut slice);
)*
};
}
// Standard Bedrock Serverbound Packets (PacketRead)
run_read!(
SAnimate,
SBlockPickRequest,
SClientCacheBlobStatus,
SClientCacheStatus,
SCommandRequest,
SContainerClose,
SEmote,
SEmoteList,
SInteraction,
SInventoryTransaction,
SItemStackRequest,
SLoadingScreen,
SLogin,
SMobEquipment,
SModalFormResponse,
SPacketViolationWarning,
SPlayerAction,
SPlayerAuthInput,
SPlayerHotbar,
SRequestAbility,
SRequestChunkRadius,
SRequestNetworkSettings,
SResourcePackResponse,
SRespawn,
SSetLocalPlayerAsInitialized,
SSetPlayerInventoryOptions,
SText,
);
// Bedrock Serverbound Packets supporting zero-copy PacketReadSlice
run_read_slice!(SCommandRequest, SEmote, SModalFormResponse, SText,);
}
// ---------------------------------------------------------------------------
@@ -67,8 +91,8 @@ fuzz_target!(|data: &[u8]| {
decoder.set_compression((threshold_raw as u32).try_into().unwrap());
}
// 1. Fuzz the Decoder (Framing/VarInts/Bitmasks)
let decoder_cursor = Cursor::new(stream_data.to_vec());
if let Ok(raw_packet) = decoder.get_game_packet(decoder_cursor) {
let mut decoder_cursor = Cursor::new(stream_data.to_vec());
if let Ok(raw_packet) = decoder.get_game_packet(&mut decoder_cursor) {
// If framed correctly, fuzz the internal payload
fuzz_serverbound_packets(&raw_packet.payload);
}

View File

@@ -2,25 +2,40 @@
use libfuzzer_sys::fuzz_target;
use pumpkin_protocol::ServerPacket;
use pumpkin_protocol::java::{
client::{
config::CFinishConfig,
login::CLoginDisconnect,
play::CPlayerPosition,
status::{CPingResponse, CStatusResponse},
},
packet_decoder::TCPNetworkDecoder,
server::{
config::{
SClientInformationConfig, SConfigCookieResponse, SConfigResourcePack, SKnownPacks,
SPluginMessage,
SAcknowledgeFinishConfig, SClientInformationConfig, SConfigCookieResponse,
SConfigResourcePack, SCustomClickAction as SConfigCustomClickAction,
SKeepAlive as SConfigKeepAlive, SKnownPacks, SPluginMessage,
},
handshake::SHandShake,
login::{SEncryptionResponse, SLoginCookieResponse, SLoginPluginResponse, SLoginStart},
login::{
SEncryptionResponse, SLoginAcknowledged, SLoginCookieResponse, SLoginPluginResponse,
SLoginStart,
},
play::{
SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClickSlot, SClientCommand,
SClientInformationPlay, SCloseContainer, SCommandSuggestion, SConfirmTeleport,
SContainerButtonClick, SCookieResponse, SCustomPayload, SInteract, SKeepAlive,
SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPlayPingRequest, SPlayerAbilities,
SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition,
SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SSetCommandBlock,
SSetCreativeSlot, SSetHeldItem, SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem,
SAttack, SBundleItemSelected, SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch,
SClickSlot, SClientCommand, SClientInformationPlay, SClientTickEnd, SCloseContainer,
SCommandSuggestion, SConfirmTeleport, SContainerButtonClick, SCookieResponse,
SCustomClickAction as SPlayCustomClickAction, SCustomPayload, SDebugSampleSubscription,
SDebugSubscriptionRequest, SEditBook, SInteract, SJigsawGenerate,
SKeepAlive as SPlayKeepAlive, SMoveVehicle, SPaddleBoat, SPickItemFromBlock,
SPickItemFromEntity, SPlaceRecipe, SPlayPingRequest, SPlayerAbilities, SPlayerAction,
SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition, SPlayerPositionRotation,
SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings, SRecipeBookSeenRecipe,
SRenameItem, SSeenAdvancement, SSelectTrade, SSetBeacon, SSetCommandBlock,
SSetCreativeSlot, SSetHeldItem, SSetJigsawBlock, SSetPlayerGround, SSetTestBlock,
SSwingArm, STeleportToEntity, STestInstanceBlockAction, SUpdateSign, SUseItem,
SUseItemOn,
},
status::SStatusPingRequest,
status::{SStatusPingRequest, SStatusRequest},
},
};
use pumpkin_util::version::JavaMinecraftVersion;
@@ -31,16 +46,14 @@ const TARGET_VERSION: JavaMinecraftVersion = JavaMinecraftVersion::V_26_1;
// ---------------------------------------------------------------------------
// Helper: run every known ServerPacket::read against the same payload.
// Uses a Cursor and the Version enum as required by the new signature.
// Uses a slice and the Version enum as required by the ServerPacket signature.
// ---------------------------------------------------------------------------
fn fuzz_all_deserializers(payload: &[u8]) {
let mut cursor = Cursor::new(payload);
macro_rules! run_read {
($($packet:ty),* $(,)?) => {
$(
cursor.set_position(0);
let _ = <$packet>::read(&mut cursor, &TARGET_VERSION);
let mut slice = payload;
let _ = <$packet>::read(&mut slice, &TARGET_VERSION);
)*
};
}
@@ -50,54 +63,85 @@ fn fuzz_all_deserializers(payload: &[u8]) {
SHandShake,
// Status
SStatusPingRequest,
SStatusRequest,
// Login
SLoginStart,
SEncryptionResponse,
SLoginPluginResponse,
SLoginCookieResponse,
SLoginAcknowledged,
// Config
SAcknowledgeFinishConfig,
SClientInformationConfig,
SPluginMessage,
SKnownPacks,
SConfigCookieResponse,
SConfigCustomClickAction,
SConfigKeepAlive,
SKnownPacks,
SPluginMessage,
SConfigResourcePack,
// Play
SConfirmTeleport,
SAttack,
SBundleItemSelected,
SChangeGameMode,
SChatCommand,
SChatMessage,
SClientInformationPlay,
SChunkBatch,
SClickSlot,
SClientCommand,
SPlayerInput,
SClientInformationPlay,
SClientTickEnd,
SCloseContainer,
SCommandSuggestion,
SConfirmTeleport,
SContainerButtonClick,
SCookieResponse,
SPlayCustomClickAction,
SCustomPayload,
SDebugSampleSubscription,
SDebugSubscriptionRequest,
SEditBook,
SInteract,
SJigsawGenerate,
SPlayKeepAlive,
SMoveVehicle,
SPaddleBoat,
SInteract,
SKeepAlive,
SPickItemFromBlock,
SPickItemFromEntity,
SPlayPingRequest,
SPlaceRecipe,
SPlayerAbilities,
SPlayerAction,
SPlayerCommand,
SPlayerInput,
SPlayerLoaded,
SPlayerPosition,
SPlayerPositionRotation,
SPlayerRotation,
SSetPlayerGround,
SPickItemFromBlock,
SPlayerAbilities,
SPlayerAction,
SSetCommandBlock,
SPlayerCommand,
SPlayerLoaded,
SPlayPingRequest,
SClickSlot,
SContainerButtonClick,
SSetHeldItem,
SSetCreativeSlot,
SSwingArm,
SUpdateSign,
SUseItemOn,
SUseItem,
SCommandSuggestion,
SCookieResponse,
SCloseContainer,
SChunkBatch,
SPlayerSession,
SCustomPayload,
SRecipeBookChangeSettings,
SRecipeBookSeenRecipe,
SRenameItem,
SSeenAdvancement,
SSelectTrade,
SSetBeacon,
SSetCommandBlock,
SSetCreativeSlot,
SSetHeldItem,
SSetJigsawBlock,
SSetPlayerGround,
SSetTestBlock,
SSwingArm,
STeleportToEntity,
STestInstanceBlockAction,
SUpdateSign,
SUseItem,
SUseItemOn,
// Clientbound packets implementing ServerPacket
CFinishConfig,
CLoginDisconnect,
CPlayerPosition,
CPingResponse,
CStatusResponse,
);
}
@@ -131,13 +175,13 @@ fuzz_target!(|data: &[u8]| {
2 => {
let mut aes_key = [0u8; 16];
aes_key.copy_from_slice(key);
decoder.set_encryption(&aes_key);
let _ = decoder.set_encryption(&aes_key);
}
3 => {
decoder.set_compression(256);
let mut aes_key = [0u8; 16];
aes_key.copy_from_slice(key);
decoder.set_encryption(&aes_key);
let _ = decoder.set_encryption(&aes_key);
}
_ => {}
}

View File

@@ -1,7 +1,7 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use pumpkin_protocol::bedrock::packet_encoder::BedrockBatchEncoder;
use pumpkin_protocol::bedrock::SubClient;
use pumpkin_protocol::bedrock::packet_encoder::BedrockBatchEncoder;
fuzz_target!(|data: &[u8]| {
if data.len() < 10 {
@@ -33,11 +33,5 @@ fuzz_target!(|data: &[u8]| {
}
let mut out = Vec::new();
let _ = encoder.write_game_packet(
packet_id,
sender,
target,
packet_payload,
&mut out
);
let _ = encoder.write_game_packet(packet_id, sender, target, packet_payload, &mut out);
});

View File

@@ -1,14 +1,13 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use pumpkin_protocol::java::packet_encoder::TCPNetworkEncoder;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::ser::NetworkWriteExt;
use pumpkin_protocol::ServerPacket;
use pumpkin_protocol::java::client::play::CPlayerPosition;
use pumpkin_protocol::packet::MultiVersionJavaPacket;
use pumpkin_protocol::ClientPacket;
use pumpkin_protocol::ServerPacket;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::java::client::play::CPlayerPosition;
use pumpkin_protocol::java::packet_encoder::TCPNetworkEncoder;
use pumpkin_protocol::packet::MultiVersionJavaPacket;
use pumpkin_protocol::ser::NetworkWriteExt;
use pumpkin_util::version::JavaMinecraftVersion;
use std::io::Cursor;
use tokio::runtime::Runtime;
const TARGET_VERSION: JavaMinecraftVersion = JavaMinecraftVersion::V_1_21_4;
@@ -34,24 +33,28 @@ fuzz_target!(|data: &[u8]| {
encoder.set_compression((compression_threshold, compression_level));
}
if use_encryption {
encoder.set_encryption(&encryption_key);
let _ = encoder.set_encryption(&encryption_key);
}
// 1. Fuzz with raw bytes (simulating various packet payloads)
let mut buf = Vec::new();
let packet_id = if packet_data.is_empty() { 0 } else { packet_data[0] as i32 };
let packet_id = if packet_data.is_empty() {
0
} else {
packet_data[0] as i32
};
let _ = buf.write_var_int(&VarInt(packet_id));
buf.extend_from_slice(packet_data);
let _ = encoder.write_packet(buf.into()).await;
// 2. Fuzz with actual packets if they can be partially read
let mut cursor = Cursor::new(packet_data);
if let Ok(packet) = CPlayerPosition::read(&mut cursor, &TARGET_VERSION) {
let mut packet_buf = Vec::new();
let id = CPlayerPosition::to_id(TARGET_VERSION);
let _ = packet_buf.write_var_int(&VarInt(id));
let _ = packet.write_packet_data(&mut packet_buf, &TARGET_VERSION);
let _ = encoder.write_packet(packet_buf.into()).await;
let mut slice = packet_data;
if let Ok(packet) = CPlayerPosition::read(&mut slice, &TARGET_VERSION) {
let mut packet_buf = Vec::new();
let id = CPlayerPosition::to_id(TARGET_VERSION);
let _ = packet_buf.write_var_int(&VarInt(id));
let _ = packet.write_packet_data(&mut packet_buf, &TARGET_VERSION);
let _ = encoder.write_packet(packet_buf.into()).await;
}
let _ = encoder.flush().await;

View File

@@ -1,6 +1,6 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use pumpkin::net::rcon::packet::Packet;
use pumpkin_protocol::rcon::Packet;
fuzz_target!(|data: &[u8]| {
let mut incoming = data.to_vec();

View File

@@ -1,4 +1,4 @@
use std::io::{Error, Read, Write};
use std::io::{Error, ErrorKind, Read, Write};
use std::num::NonZeroI32;
use pumpkin_data::item::{BedrockItem, JavaToBedrockItemMapping};
@@ -47,8 +47,14 @@ impl PacketRead for NetworkItemDescriptor {
let block_runtime_id = VarInt(VarUInt::read(buf)?.0 as i32);
let user_data_len = VarUInt::read(buf)?.0;
let mut user_data = vec![0u8; user_data_len as usize];
let user_data_len = VarUInt::read(buf)?.0 as usize;
if user_data_len > 1_048_576 {
return Err(Error::new(
ErrorKind::InvalidData,
"user_data_len exceeds 1MB limit",
));
}
let mut user_data = vec![0u8; user_data_len];
buf.read_exact(&mut user_data)?;
let (nbt_data, place_on_blocks, destroy_blocks, shield_blocking_tick) =
@@ -193,8 +199,14 @@ impl PacketRead for ItemStackWrapper {
let block_runtime_id = VarInt(VarUInt::read(buf)?.0 as i32);
let user_data_len = VarUInt::read(buf)?.0;
let mut user_data = vec![0u8; user_data_len as usize];
let user_data_len = VarUInt::read(buf)?.0 as usize;
if user_data_len > 1_048_576 {
return Err(Error::new(
ErrorKind::InvalidData,
"user_data_len exceeds 1MB limit",
));
}
let mut user_data = vec![0u8; user_data_len];
buf.read_exact(&mut user_data)?;
let (nbt_data, place_on_blocks, destroy_blocks, shield_blocking_tick) =
@@ -285,8 +297,14 @@ impl PacketRead for NetworkItemStackDescriptor {
let block_runtime_id = VarUInt::read(buf)?;
let extra_data_len = VarUInt::read(buf)?.0;
let mut extra_data = vec![0u8; extra_data_len as usize];
let extra_data_len = VarUInt::read(buf)?.0 as usize;
if extra_data_len > 1_048_576 {
return Err(Error::new(
ErrorKind::InvalidData,
"extra_data_len exceeds 1MB limit",
));
}
let mut extra_data = vec![0u8; extra_data_len];
buf.read_exact(&mut extra_data)?;
Ok(Self {
@@ -513,8 +531,14 @@ impl PacketRead for NetworkItemStack {
let aux_value = VarUInt::read(buf)?;
let block_runtime_id = VarInt::read(buf)?;
let extra_data_len = VarUInt::read(buf)?.0;
let mut extra_data = vec![0u8; extra_data_len as usize];
let extra_data_len = VarUInt::read(buf)?.0 as usize;
if extra_data_len > 1_048_576 {
return Err(Error::new(
ErrorKind::InvalidData,
"extra_data_len exceeds 1MB limit",
));
}
let mut extra_data = vec![0u8; extra_data_len];
buf.read_exact(&mut extra_data)?;
Ok(Self {
@@ -541,17 +565,24 @@ fn write_user_data_strings<W: Write>(writer: &mut W, values: &[String]) -> Resul
fn read_user_data_strings<R: Read>(reader: &mut R) -> Result<Vec<String>, Error> {
let len = i32::read(reader)?;
if len < 0 {
if !(0..=1024).contains(&len) {
return Err(Error::new(
std::io::ErrorKind::InvalidData,
"negative item string array length",
"item string array length out of bounds",
));
}
let mut values = Vec::with_capacity(len as usize);
let mut values = Vec::with_capacity((len as usize).min(32));
for _ in 0..len {
let mut length = [0; 2];
reader.read_exact(&mut length)?;
let mut bytes = vec![0; usize::from(u16::from_be_bytes(length))];
let str_len = usize::from(u16::from_be_bytes(length));
if str_len > 32767 {
return Err(Error::new(
std::io::ErrorKind::InvalidData,
"item string too long",
));
}
let mut bytes = vec![0; str_len];
reader.read_exact(&mut bytes)?;
values.push(
String::from_utf8(bytes)

View File

@@ -13,7 +13,13 @@ pub struct SClientCacheBlobStatus {
impl PacketRead for SClientCacheBlobStatus {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let miss_count = VarUInt::read(reader)?.0 as usize;
let mut miss_hashes = Vec::with_capacity(miss_count);
if miss_count > 4096 {
return Err(Error::new(
std::io::ErrorKind::InvalidData,
"miss_count exceeds limit",
));
}
let mut miss_hashes = Vec::with_capacity(miss_count.min(256));
for _ in 0..miss_count {
let mut bytes = [0u8; 8];
reader.read_exact(&mut bytes)?;
@@ -21,7 +27,13 @@ impl PacketRead for SClientCacheBlobStatus {
}
let hit_count = VarUInt::read(reader)?.0 as usize;
let mut hit_hashes = Vec::with_capacity(hit_count);
if hit_count > 4096 {
return Err(Error::new(
std::io::ErrorKind::InvalidData,
"hit_count exceeds limit",
));
}
let mut hit_hashes = Vec::with_capacity(hit_count.min(256));
for _ in 0..hit_count {
let mut bytes = [0u8; 8];
reader.read_exact(&mut bytes)?;

View File

@@ -53,6 +53,12 @@ impl PacketRead for StackRequestItem {
let count = i16::read(reader)? as u16;
let block_runtime_id = VarUInt::read(reader)?;
let data_len = VarUInt::read(reader)?.0 as usize;
if data_len > 1_048_576 {
return Err(Error::new(
ErrorKind::InvalidData,
"extra_data length exceeds limit",
));
}
let mut extra_data = vec![0; data_len];
reader.read_exact(&mut extra_data)?;
Ok(Self {

View File

@@ -94,7 +94,13 @@ impl PacketRead for SPlayerAuthInput {
// 3. Block Actions
let block_actions = if bool::read(reader)? && bool::read(reader)? {
let count = VarUInt::read(reader)?.0 as usize;
let mut actions = Vec::with_capacity(count);
if count > 1024 {
return Err(Error::new(
ErrorKind::InvalidData,
"block_actions count exceeds limit",
));
}
let mut actions = Vec::with_capacity(count.min(64));
for _ in 0..count {
actions.push(PlayerBlockAction::read(reader)?);
}
@@ -155,7 +161,14 @@ impl PacketRead for PlayerInventoryAction {
let legacy_request_id = VarInt::read(buf)?;
let mut legacy_slots = Vec::new();
if bool::read(buf)? && legacy_request_id.0 < -1 && (legacy_request_id.0 & 1) == 0 {
let slots_len = VarUInt::read(buf)?.0;
let slots_len = VarUInt::read(buf)?.0 as usize;
if slots_len > 1024 {
return Err(Error::new(
ErrorKind::InvalidData,
"slots_len exceeds limit",
));
}
legacy_slots.reserve(slots_len.min(64));
for _ in 0..slots_len {
legacy_slots.push(
crate::bedrock::server::inventory_transaction::LegacySetItemSlot::read(buf)?,
@@ -164,8 +177,14 @@ impl PacketRead for PlayerInventoryAction {
}
let mut actions = Vec::new();
if bool::read(buf)? && bool::read(buf)? {
let actions_len = VarUInt::read(buf)?.0;
actions.reserve(actions_len as usize);
let actions_len = VarUInt::read(buf)?.0 as usize;
if actions_len > 1024 {
return Err(Error::new(
ErrorKind::InvalidData,
"actions_len exceeds limit",
));
}
actions.reserve(actions_len.min(64));
for _ in 0..actions_len {
actions.push(
crate::bedrock::server::inventory_transaction::InventoryAction::read(buf)?,

View File

@@ -13,12 +13,22 @@ pub struct SResourcePackResponse {
impl PacketRead for SResourcePackResponse {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let encoded_status = VarUInt::read(reader)?.0;
let response = u8::try_from(encoded_status + 1)
.map_err(|_| Error::new(ErrorKind::InvalidData, "resource pack status is too large"))?;
let response = encoded_status
.checked_add(1)
.and_then(|v| u8::try_from(v).ok())
.ok_or_else(|| {
Error::new(ErrorKind::InvalidData, "resource pack status is too large")
})?;
let _status_name = String::read(reader)?;
let pack_ids = if response == Self::STATUS_SEND_PACKS {
let count = VarUInt::read(reader)?.0;
if count > 1024 {
return Err(Error::new(
ErrorKind::InvalidData,
"too many resource pack identifiers",
));
}
(0..count)
.map(|_| String::read(reader))
.collect::<Result<Vec<_>, _>>()?

View File

@@ -578,22 +578,32 @@ impl DataComponentCodec<Self> for PotionContentsImpl {
}
}
/// Helper to skip hidden effect parameters recursively
/// Helper to skip hidden effect parameters iteratively with a depth cap
fn skip_effect_parameters(seq: &mut impl NetworkReadExt) -> Result<(), ReadingError> {
// amplifier
seq.get_var_int()?;
// duration
seq.get_var_int()?;
// ambient
seq.get_bool()?;
// show_particles
seq.get_bool()?;
// show_icon
seq.get_bool()?;
// has_hidden (recursive)
let has_hidden = seq.get_bool()?;
if has_hidden {
skip_effect_parameters(seq)?;
const MAX_EFFECT_DEPTH: usize = 32;
let mut depth = 0;
loop {
// amplifier
seq.get_var_int()?;
// duration
seq.get_var_int()?;
// ambient
seq.get_bool()?;
// show_particles
seq.get_bool()?;
// show_icon
seq.get_bool()?;
// has_hidden
let has_hidden = seq.get_bool()?;
if !has_hidden {
break;
}
depth += 1;
if depth > MAX_EFFECT_DEPTH {
return Err(ReadingError::TooLarge(
"Potion effect hidden depth exceeded".into(),
));
}
}
Ok(())
}

View File

@@ -109,6 +109,9 @@ fn read_length_prefixed_component(
let byte_len: usize = byte_len
.try_into()
.map_err(|_| ReadingError::Message("Negative component data length".into()))?;
if byte_len > crate::MAX_PACKET_DATA_SIZE {
return Err(ReadingError::TooLarge("Component data too large".into()));
}
let component_impl = if byte_len <= 256 {
let mut stack_buf = [0u8; 256];

View File

@@ -1,5 +1,16 @@
use crate::{ServerPacket, ser::ReadingError};
use pumpkin_data::packet::serverbound::PLAY_CLIENT_TICK_END;
use pumpkin_macros::java_packet;
use pumpkin_util::version::JavaMinecraftVersion;
#[java_packet(PLAY_CLIENT_TICK_END)]
pub struct SClientTickEnd;
impl<'a> ServerPacket<'a> for SClientTickEnd {
fn read(
_bytebuf: &mut &'a [u8],
_protocol_version: &JavaMinecraftVersion,
) -> Result<Self, ReadingError> {
Ok(Self)
}
}

View File

@@ -21,11 +21,19 @@ impl<'a> ServerPacket<'a> for SPlayerSession {
let session_id = read.get_uuid()?;
let expires_at = read.get_i64_be()?;
let public_key_length = read.get_var_int()?.0 as usize;
let public_key_length = usize::try_from(read.get_var_int()?.0)
.map_err(|_| ReadingError::Message("Negative public key length".into()))?;
if public_key_length > 2048 {
return Err(ReadingError::TooLarge("Public key too long".into()));
}
let mut public_key = vec![0u8; public_key_length];
read.read_bytes_to_buf(&mut public_key)?;
let key_signature_length = read.get_var_int()?.0 as usize;
let key_signature_length = usize::try_from(read.get_var_int()?.0)
.map_err(|_| ReadingError::Message("Negative key signature length".into()))?;
if key_signature_length > 4096 {
return Err(ReadingError::TooLarge("Key signature too long".into()));
}
let mut key_signature = vec![0u8; key_signature_length];
read.read_bytes_to_buf(&mut key_signature)?;

View File

@@ -29,6 +29,7 @@ pub mod java;
pub mod packet;
#[cfg(feature = "query")]
pub mod query;
pub mod rcon;
pub mod ser;
pub mod serial;

View File

@@ -35,7 +35,6 @@ pub enum ClientboundPacket {
impl ClientboundPacket {
#[must_use]
pub fn write_buf(self, id: i32, body: &str) -> BytesMut {
// let len = outgoing.len() as u64;
let mut buf = BytesMut::new();
// 10 is for 4 bytes ty, 4 bytes id, and 2 terminating nul bytes.
buf.put_i32_le(10 + body.len() as i32);
@@ -53,9 +52,9 @@ impl ClientboundPacket {
pub enum PacketError {
#[error("Invalid length")]
InvalidLength,
#[error("Dailed to send packet")]
#[error("Failed to send packet")]
FailedSend(std::io::Error),
#[error("Missing packet lull terminator")]
#[error("Missing packet null terminator")]
MissingNullTerminator,
#[error("Invalid packet string body")]
InvalidBody(std::str::Utf8Error),

View File

@@ -105,7 +105,7 @@ pub trait NetworkReadExt {
fn get_str_bounded(&mut self, bound: usize) -> Result<Box<str>, ReadingError>;
#[inline]
fn get_str(&mut self) -> Result<Box<str>, ReadingError> {
self.get_str_bounded(i32::MAX as usize)
self.get_str_bounded(32767)
}
fn get_uuid(&mut self) -> Result<uuid::Uuid, ReadingError>;
fn get_fixed_bitset(&mut self, bits: usize) -> Result<FixedBitSet, ReadingError>;
@@ -164,7 +164,7 @@ pub trait NetworkReadSliceExt<'a> {
#[inline]
fn get_cow_str_borrowed(&mut self) -> Result<Cow<'a, str>, ReadingError> {
self.get_cow_str_bounded_borrowed(i32::MAX as usize)
self.get_cow_str_bounded_borrowed(32767)
}
#[inline]
@@ -203,7 +203,7 @@ impl<'a> NetworkReadSliceExt<'a> for &'a [u8] {
fn get_str_bounded_borrowed(&mut self, bound: usize) -> Result<&'a str, ReadingError> {
let bytes_len = self.get_var_uint()?.0 as usize;
let maximum_utf8_bytes = bound.saturating_mul(3);
let maximum_utf8_bytes = bound.saturating_mul(3).min(crate::MAX_PACKET_DATA_SIZE);
if bytes_len > maximum_utf8_bytes {
return Err(ReadingError::TooLarge(format!(
"string has too many bytes ({bytes_len} > {maximum_utf8_bytes})"
@@ -225,7 +225,7 @@ impl<'a> NetworkReadSliceExt<'a> for &'a [u8] {
#[inline]
fn get_str_borrowed(&mut self) -> Result<&'a str, ReadingError> {
self.get_str_bounded_borrowed(i32::MAX as usize)
self.get_str_bounded_borrowed(32767)
}
}
@@ -314,7 +314,7 @@ impl<R: Read> NetworkReadExt for R {
// First, check if there are too many bytes to even fit in the UTF-16 bound.
// 1 Java `char` takes a maximum of 3 bytes in UTF-8:
let maximum_utf8_bytes = bound.saturating_mul(3);
let maximum_utf8_bytes = bound.saturating_mul(3).min(crate::MAX_PACKET_DATA_SIZE);
if bytes_len > maximum_utf8_bytes {
return Err(ReadingError::TooLarge(format!(
"string has too many bytes ({bytes_len} > {maximum_utf8_bytes})"

View File

@@ -172,8 +172,14 @@ impl PacketRead for String {
impl<T: PacketRead> PacketRead for Vec<T> {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let len = VarUInt::read(reader)?.0 as _;
let mut buf = Self::with_capacity(len);
let len = VarUInt::read(reader)?.0 as usize;
if len > 65536 {
return Err(Error::new(
ErrorKind::InvalidData,
format!("Vector length {len} exceeds limit of 65536"),
));
}
let mut buf = Self::with_capacity(len.min(1024));
for _ in 0..len {
buf.push(T::read(reader)?);
}

View File

@@ -72,8 +72,8 @@ rsa.workspace = true
# verification & WASM parsing
ed25519-dalek.workspace = true
hex.workspace = true
wasmparser = "0.255"
wasm-encoder = "0.255"
wasmparser = "0.256"
wasm-encoder = "0.256"
# authentication
ureq = { workspace = true, features = ["json"] }

View File

@@ -47,7 +47,7 @@ pub struct MojangPublicKeys {
}
const MOJANG_AUTHENTICATION_URL: &str = "https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}";
const MOJANG_PREVENT_PROXY_AUTHENTICATION_URL: &str = "https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}";
const MOJANG_PREVENT_PROXY_AUTHENTICATION_URL: &str = "https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}&ip={ip}";
const MOJANG_SERVICES_URL: &str = "https://api.minecraftservices.com/";
const MOJANG_PROFILE_BY_NAME_URL: &str =
"https://api.mojang.com/users/profiles/minecraft/{username}";

View File

@@ -811,7 +811,7 @@ impl BedrockClient {
Ok(packet) => Some(packet),
Err(err) => {
if !matches!(err, PacketDecodeError::ConnectionClosed) {
warn!("Failed to decode packet from client: {err}");
debug!("Failed to decode packet from client: {err}");
let text = format!("Error while reading incoming packet {err}");
self.kick(DisconnectReason::BadPacket, text).await;
}

View File

@@ -411,7 +411,7 @@ impl JavaClient {
Ok(packet) => Some(packet),
Err(err) => {
if !matches!(err, PacketDecodeError::ConnectionClosed) {
warn!("Failed to decode packet from client {}: {}", self.id, err);
debug!("Failed to decode packet from client {}: {}", self.id, err);
let text = format!("Error while reading incoming packet {err}");
self.kick(TextComponent::text(text)).await;
}

View File

@@ -125,7 +125,7 @@ impl PendingConnection {
Ok(packet) => Some(packet),
Err(err) => {
if !matches!(err, PacketDecodeError::ConnectionClosed) {
warn!("Failed to decode packet from client {}: {}", self.id, err);
debug!("Failed to decode packet from client {}: {}", self.id, err);
let text = format!("Error while reading incoming packet {err}");
self.kick(TextComponent::text(text)).await;
}

View File

@@ -65,6 +65,9 @@ pub async fn velocity_login(connection: &mut PendingConnection) {
#[must_use]
pub fn check_integrity(data: (&[u8], &[u8]), secret: &str) -> bool {
if secret.is_empty() {
return false;
}
let (signature, data_without_signature) = data;
// Our fault, we can panic/expect?
let Ok(mut mac) = HmacSha256::new_from_slice(secret.as_bytes()) else {

View File

@@ -12,12 +12,19 @@ use tracing::{debug, error, info};
use crate::command::CommandSender;
use crate::{SHOULD_STOP, STOP_INTERRUPT, server::Server};
pub mod packet;
pub use pumpkin_protocol::rcon as packet;
pub struct RCONServer;
impl RCONServer {
pub async fn run(config: &RCONConfig, server: Arc<Server>) {
if config.password.trim().is_empty() {
error!(
"RCON is enabled but password is empty! Refusing to start RCON server for security."
);
return;
}
let listener = match tokio::net::TcpListener::bind(config.address).await {
Ok(l) => l,
Err(e) => {
@@ -109,7 +116,7 @@ impl RCONClient {
let config = &server.advanced_config.networking.rcon;
match packet.get_type() {
ServerboundPacket::Auth => {
if packet.get_body() == password {
if !password.is_empty() && packet.get_body() == password {
self.send(ClientboundPacket::AuthResponse, packet.get_id(), "")
.await?;
if config.logging.logged_successfully {

View File

@@ -0,0 +1,22 @@
[package]
name = "pumpkin-fuzzer"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
pumpkin-protocol.workspace = true
pumpkin-util.workspace = true
tokio = { workspace = true, features = ["full"] }
clap = { version = "4", features = ["derive"] }
colored.workspace = true
bytes.workspace = true
rand = "0.9"
[lints.clippy]
expect_used = "allow"
unwrap_used = "allow"
panic = "allow"
pedantic = { level = "allow", priority = -1 }
all = { level = "allow", priority = -1 }

View File

@@ -0,0 +1,385 @@
use clap::{Parser, ValueEnum};
use colored::Colorize;
use rand::Rng;
use std::{
net::SocketAddr,
sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
},
time::{Duration, Instant},
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
time::timeout,
};
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
enum FuzzMode {
All,
Raw,
Framed,
Stateful,
CorruptVarint,
}
#[derive(Parser, Debug)]
#[command(author, version, about = "Pumpkin Server Network Packet Fuzzer & Stress Tool", long_about = None)]
struct Args {
/// Target server host
#[arg(long, default_value = "127.0.0.1")]
host: String,
/// Target server port
#[arg(short, long, default_value_t = 25565)]
port: u16,
/// Number of concurrent async fuzz worker tasks
#[arg(short, long, default_value_t = 8)]
concurrency: usize,
/// Duration to fuzz in seconds
#[arg(short, long, default_value_t = 30)]
duration: u64,
/// Fuzzing mode
#[arg(short, long, value_enum, default_value_t = FuzzMode::All)]
mode: FuzzMode,
}
#[derive(Default)]
struct Stats {
connections: AtomicU64,
packets_sent: AtomicU64,
bytes_sent: AtomicU64,
server_closes: AtomicU64,
errors: AtomicU64,
}
fn encode_var_int(val: i32, buf: &mut Vec<u8>) {
let mut uval = val as u32;
loop {
let mut byte = (uval & 0x7F) as u8;
uval >>= 7;
if uval != 0 {
byte |= 0x80;
}
buf.push(byte);
if uval == 0 {
break;
}
}
}
fn encode_packet(packet_id: i32, payload: &[u8]) -> Vec<u8> {
let mut id_buf = Vec::new();
encode_var_int(packet_id, &mut id_buf);
let total_len = id_buf.len() + payload.len();
let mut out = Vec::new();
encode_var_int(total_len as i32, &mut out);
out.extend_from_slice(&id_buf);
out.extend_from_slice(payload);
out
}
fn make_handshake_packet(next_state: i32) -> Vec<u8> {
let mut payload = Vec::new();
encode_var_int(769, &mut payload); // Protocol version 769 (1.21.4)
let addr = b"127.0.0.1";
encode_var_int(addr.len() as i32, &mut payload);
payload.extend_from_slice(addr);
payload.extend_from_slice(&25565u16.to_be_bytes());
encode_var_int(next_state, &mut payload);
encode_packet(0x00, &payload)
}
async fn check_server_alive(addr: SocketAddr) -> bool {
let Ok(Ok(mut stream)) = timeout(Duration::from_millis(1500), TcpStream::connect(addr)).await
else {
return false;
};
let handshake = make_handshake_packet(1); // Status state
if stream.write_all(&handshake).await.is_err() {
return false;
}
let status_req = encode_packet(0x00, &[]);
if stream.write_all(&status_req).await.is_err() {
return false;
}
let mut buf = [0u8; 1024];
matches!(
timeout(Duration::from_millis(1500), stream.read(&mut buf)).await,
Ok(Ok(n)) if n > 0
)
}
async fn fuzz_worker(
target_addr: SocketAddr,
mode: FuzzMode,
stop: Arc<AtomicBool>,
stats: Arc<Stats>,
) {
while !stop.load(Ordering::Relaxed) {
let stream_result =
timeout(Duration::from_millis(1500), TcpStream::connect(target_addr)).await;
let mut stream = match stream_result {
Ok(Ok(s)) => {
stats.connections.fetch_add(1, Ordering::Relaxed);
s
}
_ => {
stats.errors.fetch_add(1, Ordering::Relaxed);
tokio::time::sleep(Duration::from_millis(10)).await;
continue;
}
};
let selected_mode = match mode {
FuzzMode::All => match rand::rng().random_range(0..4) {
0 => FuzzMode::Stateful,
1 => FuzzMode::Framed,
2 => FuzzMode::CorruptVarint,
_ => FuzzMode::Raw,
},
other => other,
};
let res: Result<(), tokio::io::Error> = async {
match selected_mode {
FuzzMode::Stateful => {
let next_state = match rand::rng().random_range(0..3) {
0 => 1, // Status
1 => 2, // Login
_ => 7, // Config
};
let handshake = make_handshake_packet(next_state);
stream.write_all(&handshake).await?;
stats.packets_sent.fetch_add(1, Ordering::Relaxed);
stats
.bytes_sent
.fetch_add(handshake.len() as u64, Ordering::Relaxed);
let count = rand::rng().random_range(10..40);
for _ in 0..count {
if stop.load(Ordering::Relaxed) {
break;
}
let (frame, sleep_ms) = {
let mut rng = rand::rng();
let pkt_id = rng.random_range(0..0x60);
let pkt_len = rng.random_range(0..512);
let mut payload = vec![0u8; pkt_len];
rng.fill(&mut payload[..]);
(encode_packet(pkt_id, &payload), rng.random_range(1..5))
};
stream.write_all(&frame).await?;
stats.packets_sent.fetch_add(1, Ordering::Relaxed);
stats
.bytes_sent
.fetch_add(frame.len() as u64, Ordering::Relaxed);
tokio::time::sleep(Duration::from_millis(sleep_ms)).await;
}
}
FuzzMode::Framed => {
let count = rand::rng().random_range(5..30);
for _ in 0..count {
if stop.load(Ordering::Relaxed) {
break;
}
let (frame, sleep_ms) = {
let mut rng = rand::rng();
let pkt_id = rng.random_range(0..255);
let pkt_len = rng.random_range(0..2048);
let mut payload = vec![0u8; pkt_len];
rng.fill(&mut payload[..]);
(encode_packet(pkt_id, &payload), rng.random_range(1..4))
};
stream.write_all(&frame).await?;
stats.packets_sent.fetch_add(1, Ordering::Relaxed);
stats
.bytes_sent
.fetch_add(frame.len() as u64, Ordering::Relaxed);
tokio::time::sleep(Duration::from_millis(sleep_ms)).await;
}
}
FuzzMode::CorruptVarint => {
let count = rand::rng().random_range(5..20);
for _ in 0..count {
if stop.load(Ordering::Relaxed) {
break;
}
let bad_varint = {
let mut rng = rand::rng();
let byte_count = rng.random_range(5..10);
let mut v = Vec::with_capacity(byte_count + 64);
for _ in 0..byte_count {
v.push(0x80 | rng.random_range(0..128));
}
let extra_len = rng.random_range(10..100);
let mut extra = vec![0u8; extra_len];
rng.fill(&mut extra[..]);
v.extend_from_slice(&extra);
v
};
stream.write_all(&bad_varint).await?;
stats.packets_sent.fetch_add(1, Ordering::Relaxed);
stats
.bytes_sent
.fetch_add(bad_varint.len() as u64, Ordering::Relaxed);
}
}
FuzzMode::Raw | FuzzMode::All => {
let count = rand::rng().random_range(5..20);
for _ in 0..count {
if stop.load(Ordering::Relaxed) {
break;
}
let (chunk, sleep_ms) = {
let mut rng = rand::rng();
let chunk_len = rng.random_range(1..4096);
let mut c = vec![0u8; chunk_len];
rng.fill(&mut c[..]);
(c, rng.random_range(1..5))
};
stream.write_all(&chunk).await?;
stats.packets_sent.fetch_add(1, Ordering::Relaxed);
stats
.bytes_sent
.fetch_add(chunk.len() as u64, Ordering::Relaxed);
tokio::time::sleep(Duration::from_millis(sleep_ms)).await;
}
}
}
Ok(())
}
.await;
if res.is_ok() {
stats.server_closes.fetch_add(1, Ordering::Relaxed);
} else {
stats.errors.fetch_add(1, Ordering::Relaxed);
}
let sleep_ms = rand::rng().random_range(5..30);
tokio::time::sleep(Duration::from_millis(sleep_ms)).await;
}
}
#[tokio::main]
async fn main() {
let args = Args::parse();
let addr_str = format!("{}:{}", args.host, args.port);
let target_addr: SocketAddr = match addr_str.parse() {
Ok(a) => a,
Err(e) => {
eprintln!(
"{} Failed to parse address '{}': {}",
"Error:".red().bold(),
addr_str,
e
);
std::process::exit(1);
}
};
println!(
"{}",
"=== Pumpkin Server Network Packet Fuzzer ===".cyan().bold()
);
println!("{:<15} {}", "Target:".bold(), addr_str.yellow());
println!("{:<15} {}", "Concurrency:".bold(), args.concurrency);
println!("{:<15} {}s", "Duration:".bold(), args.duration);
println!("{:<15} {:?}", "Mode:".bold(), args.mode);
println!(
"{}",
"-------------------------------------------------".dimmed()
);
print!("Probing target server... ");
if check_server_alive(target_addr).await {
println!("{}", "ONLINE (Responding to status pings)".green().bold());
} else {
println!(
"{}",
"UNRESPONSIVE / OFFLINE (will proceed with test)"
.yellow()
.bold()
);
}
let stats = Arc::new(Stats::default());
let stop = Arc::new(AtomicBool::new(false));
let mut handles = Vec::new();
for _ in 0..args.concurrency {
let handle = tokio::spawn(fuzz_worker(
target_addr,
args.mode,
stop.clone(),
stats.clone(),
));
handles.push(handle);
}
let start = Instant::now();
let duration = Duration::from_secs(args.duration);
while start.elapsed() < duration {
tokio::time::sleep(Duration::from_secs(1)).await;
let elapsed = start.elapsed().as_secs();
let conns = stats.connections.load(Ordering::Relaxed);
let pkts = stats.packets_sent.load(Ordering::Relaxed);
let bytes = stats.bytes_sent.load(Ordering::Relaxed);
let closes = stats.server_closes.load(Ordering::Relaxed);
let errs = stats.errors.load(Ordering::Relaxed);
let rate_kb = (bytes as f64 / elapsed.max(1) as f64) / 1024.0;
println!(
"[{:3}s] Conns: {:5} | Packets: {:6} | Data: {:7.1} KB ({:6.1} KB/s) | Closes: {:4} | Resets/Timeouts: {:4}",
elapsed,
conns,
pkts,
bytes as f64 / 1024.0,
rate_kb,
closes,
errs
);
}
stop.store(true, Ordering::Relaxed);
for h in handles {
let _ = h.await;
}
println!(
"{}",
"-------------------------------------------------".dimmed()
);
let conns = stats.connections.load(Ordering::Relaxed);
let pkts = stats.packets_sent.load(Ordering::Relaxed);
let bytes = stats.bytes_sent.load(Ordering::Relaxed);
let closes = stats.server_closes.load(Ordering::Relaxed);
let errs = stats.errors.load(Ordering::Relaxed);
println!("{}", "Fuzzing Session Complete:".green().bold());
println!(" Total Connections: {}", conns);
println!(" Total Packets Sent: {}", pkts);
println!(" Total Data Sent: {:.2} KB", bytes as f64 / 1024.0);
println!(" Server Normal Drops: {}", closes);
println!(" Connection Resets: {}", errs);
print!("\nVerifying server health post-fuzzing... ");
if check_server_alive(target_addr).await {
println!(
"{}",
"SUCCESS: Server is alive and responsive!".green().bold()
);
} else {
println!("{}", "WARNING: Server did not respond to post-fuzz query. Check server logs for any crash or hang.".red().bold());
}
}