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

View File

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

View File

@@ -940,15 +940,20 @@ pub trait ScreenHandler: Send + Sync {
let mut cursor_stack = behaviour.cursor_stack.lock().await; let mut cursor_stack = behaviour.cursor_stack.lock().await;
let initial_count = cursor_stack.item_count; let initial_count = cursor_stack.item_count;
let slots_count = behaviour.drag_slots.len();
for slot_index in &behaviour.drag_slots { 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; let stack = slot.get_stack().await;
if (stack.are_items_and_components_equal(&cursor_stack) || stack.is_empty()) if (stack.are_items_and_components_equal(&cursor_stack) || stack.is_empty())
&& slot.can_insert(&cursor_stack).await && slot.can_insert(&cursor_stack).await
{ {
let mut inserting_count = match drag_button { 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, 1 => 1,
2 => { 2 => {
cursor_stack.item_count = cursor_stack.get_max_stack_size(); cursor_stack.item_count = cursor_stack.get_max_stack_size();

View File

@@ -17,6 +17,21 @@ dependencies = [
"derive_arbitrary", "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]] [[package]]
name = "bytes" name = "bytes"
version = "1.12.0" version = "1.12.0"
@@ -56,6 +71,16 @@ dependencies = [
"cfg-if", "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]] [[package]]
name = "derive_arbitrary" name = "derive_arbitrary"
version = "1.4.2" version = "1.4.2"
@@ -68,10 +93,14 @@ dependencies = [
] ]
[[package]] [[package]]
name = "either" name = "digest"
version = "1.16.0" version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
@@ -89,6 +118,40 @@ dependencies = [
"miniz_oxide", "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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.3.4" version = "0.3.4"
@@ -97,10 +160,21 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"r-efi", "r-efi 5.3.0",
"wasip2", "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]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.18" version = "1.0.18"
@@ -113,10 +187,21 @@ version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.3.4",
"libc", "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]] [[package]]
name = "libc" name = "libc"
version = "0.2.186" version = "0.2.186"
@@ -133,6 +218,16 @@ dependencies = [
"cc", "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]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.0" version = "2.8.0"
@@ -172,24 +267,23 @@ dependencies = [
[[package]] [[package]]
name = "pumpkin-codecs" name = "pumpkin-codecs"
version = "0.1.0-dev+26.1" version = "0.1.0-dev+26.2-26.40"
dependencies = [ dependencies = [
"either",
"serde_json", "serde_json",
"tracing", "tracing",
] ]
[[package]] [[package]]
name = "pumpkin-nbt" name = "pumpkin-nbt"
version = "0.1.0-dev+26.1" version = "0.1.0-dev+26.2-26.40"
dependencies = [ dependencies = [
"bytes", "bytes",
"cesu8", "cesu8",
"flate2", "flate2",
"pumpkin-codecs", "pumpkin-codecs",
"serde",
"thiserror", "thiserror",
"tracing", "tracing",
"uuid",
] ]
[[package]] [[package]]
@@ -199,7 +293,6 @@ dependencies = [
"arbitrary", "arbitrary",
"libfuzzer-sys", "libfuzzer-sys",
"pumpkin-nbt", "pumpkin-nbt",
"serde",
] ]
[[package]] [[package]]
@@ -217,6 +310,18 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 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]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@@ -224,7 +329,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [ dependencies = [
"serde_core", "serde_core",
"serde_derive",
] ]
[[package]] [[package]]
@@ -272,6 +376,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.117" version = "2.0.117"
@@ -334,12 +444,37 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.24" version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" 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]] [[package]]
name = "wasip2" name = "wasip2"
version = "1.0.3+wasi-0.2.9" version = "1.0.3+wasi-0.2.9"
@@ -349,6 +484,51 @@ dependencies = [
"wit-bindgen", "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]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.57.1" version = "0.57.1"

View File

@@ -51,6 +51,18 @@ impl NbtCompound {
/// Advances a reader past a compound's payload without allocating its tags. /// 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> { 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 { loop {
let tag_id = match reader.get_u8() { let tag_id = match reader.get_u8() {
Ok(id) => id, Ok(id) => id,
@@ -65,7 +77,7 @@ impl NbtCompound {
reader.skip_string()?; reader.skip_string()?;
// Skip Value // Skip Value
NbtTag::skip_data(reader, tag_id)?; NbtTag::skip_data_depth(reader, tag_id, depth + 1)?;
} }
Ok(()) Ok(())
@@ -73,6 +85,18 @@ impl NbtCompound {
/// Deserializes a compound payload, starting after the compound's name. /// Deserializes a compound payload, starting after the compound's name.
pub fn deserialize_content<'a, R: NbtReadHelper<'a>>(reader: &mut R) -> Result<Self, Error> { 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(); let mut compound = Self::new();
loop { loop {
@@ -87,7 +111,7 @@ impl NbtCompound {
} }
let name = reader.get_string()?; 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); 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; pub const LONG_ARRAY_ID: u8 = 0x0C;
/// Maximum number of elements accepted when decoding a list or array. /// 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. /// Errors produced while reading, writing, or converting NBT data.
#[derive(Error, Debug)] #[derive(Error, Debug)]
@@ -101,6 +103,12 @@ pub enum Error {
/// A Bedrock variable-length long exceeded its maximum encoded size. /// A Bedrock variable-length long exceeded its maximum encoded size.
#[error("Failed to decode varlong - value too large")] #[error("Failed to decode varlong - value too large")]
VarLongTooLarge, 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. /// A complete NBT document containing a named root compound.

View File

@@ -6,8 +6,7 @@ use serializer::NbtWriteHelper;
use crate::{ use crate::{
BYTE_ARRAY_ID, BYTE_ID, COMPOUND_ID, DOUBLE_ID, END_ID, Error, FLOAT_ID, INT_ARRAY_ID, INT_ID, 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, LIST_ID, LONG_ARRAY_ID, LONG_ID, SHORT_ID, STRING_ID, compound, deserializer, serializer,
serializer,
}; };
/// A value represented by one of the tag types defined by the NBT format. /// 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`. /// 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> { 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 { match tag_id {
END_ID => Ok(()), END_ID => Ok(()),
BYTE_ID => reader.skip_i8(), BYTE_ID => reader.skip_i8(),
@@ -216,7 +228,11 @@ impl NbtTag {
if len < 0 { if len < 0 {
return Err(Error::NegativeLength(len)); 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(), STRING_ID => reader.skip_string(),
LIST_ID => { LIST_ID => {
@@ -225,20 +241,33 @@ impl NbtTag {
if len < 0 { if len < 0 {
return Err(Error::NegativeLength(len)); 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 { for _ in 0..len {
Self::skip_data(reader, tag_type_id)?; Self::skip_data_depth(reader, tag_type_id, depth + 1)?;
} }
Ok(()) Ok(())
} }
COMPOUND_ID => NbtCompound::skip_content(reader), COMPOUND_ID => NbtCompound::skip_content_depth(reader, depth + 1),
INT_ARRAY_ID => { INT_ARRAY_ID => {
let len = reader.get_i32()?; let len = reader.get_i32()?;
if len < 0 { if len < 0 {
return Err(Error::NegativeLength(len)); 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 { for _ in 0..len {
reader.skip_i32()?; reader.skip_i32()?;
} }
@@ -251,6 +280,11 @@ impl NbtTag {
return Err(Error::NegativeLength(len)); 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 { for _ in 0..len {
reader.skip_i64()?; reader.skip_i64()?;
} }
@@ -266,6 +300,20 @@ impl NbtTag {
reader: &mut R, reader: &mut R,
tag_id: u8, tag_id: u8,
) -> Result<Self, Error> { ) -> 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 { match tag_id {
END_ID => Ok(Self::End), END_ID => Ok(Self::End),
BYTE_ID => { BYTE_ID => {
@@ -299,10 +347,10 @@ impl NbtTag {
} }
let len = len as usize; let len = len as usize;
if len > MAX_ARRAY_LENGTH { if len > crate::MAX_ARRAY_LENGTH {
return Err(Error::LargeLength(len)); 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 { for _ in 0..len {
let byte = reader.get_i8()?; let byte = reader.get_i8()?;
byte_array.push(byte); byte_array.push(byte);
@@ -316,22 +364,30 @@ impl NbtTag {
if len < 0 { if len < 0 {
return Err(Error::NegativeLength(len)); 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; let len = len as usize;
if len > MAX_ARRAY_LENGTH { if len > crate::MAX_ARRAY_LENGTH {
return Err(Error::LargeLength(len)); 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 { for _ in 0..len {
let tag = Self::deserialize_data(reader, tag_type_id)?; let tag = Self::deserialize_data_depth(reader, tag_type_id, depth + 1)?;
assert_eq!(tag.get_type_id(), tag_type_id); if tag.get_type_id() != tag_type_id {
return Err(Error::InvalidListTag(tag.get_type_id()));
}
// Try unwrapping the tag. // Try unwrapping the tag.
list.push(Self::flatten(tag)); list.push(Self::flatten(tag));
} }
Ok(Self::List(list)) 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 => { INT_ARRAY_ID => {
let len = reader.get_i32()?; let len = reader.get_i32()?;
if len < 0 { if len < 0 {
@@ -339,10 +395,11 @@ impl NbtTag {
} }
let len = len as usize; let len = len as usize;
if len > MAX_ARRAY_LENGTH { if len > crate::MAX_ARRAY_LENGTH {
return Err(Error::LargeLength(len)); 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 { for _ in 0..len {
let int = reader.get_i32()?; let int = reader.get_i32()?;
int_array.push(int); int_array.push(int);
@@ -356,10 +413,11 @@ impl NbtTag {
} }
let len = len as usize; let len = len as usize;
if len > MAX_ARRAY_LENGTH { if len > crate::MAX_ARRAY_LENGTH {
return Err(Error::LargeLength(len)); 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 { for _ in 0..len {
let long = reader.get_i64()?; let long = reader.get_i64()?;
long_array.push(long); 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] [dependencies.pumpkin-protocol]
path = ".." path = ".."
[dependencies.pumpkin]
path = "../../pumpkin"
[dependencies.pumpkin-util] [dependencies.pumpkin-util]
path = "../../pumpkin-util" path = "../../pumpkin-util"

View File

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

View File

@@ -2,25 +2,40 @@
use libfuzzer_sys::fuzz_target; use libfuzzer_sys::fuzz_target;
use pumpkin_protocol::ServerPacket; use pumpkin_protocol::ServerPacket;
use pumpkin_protocol::java::{ use pumpkin_protocol::java::{
client::{
config::CFinishConfig,
login::CLoginDisconnect,
play::CPlayerPosition,
status::{CPingResponse, CStatusResponse},
},
packet_decoder::TCPNetworkDecoder, packet_decoder::TCPNetworkDecoder,
server::{ server::{
config::{ config::{
SClientInformationConfig, SConfigCookieResponse, SConfigResourcePack, SKnownPacks, SAcknowledgeFinishConfig, SClientInformationConfig, SConfigCookieResponse,
SPluginMessage, SConfigResourcePack, SCustomClickAction as SConfigCustomClickAction,
SKeepAlive as SConfigKeepAlive, SKnownPacks, SPluginMessage,
}, },
handshake::SHandShake, handshake::SHandShake,
login::{SEncryptionResponse, SLoginCookieResponse, SLoginPluginResponse, SLoginStart}, login::{
SEncryptionResponse, SLoginAcknowledged, SLoginCookieResponse, SLoginPluginResponse,
SLoginStart,
},
play::{ play::{
SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClickSlot, SClientCommand, SAttack, SBundleItemSelected, SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch,
SClientInformationPlay, SCloseContainer, SCommandSuggestion, SConfirmTeleport, SClickSlot, SClientCommand, SClientInformationPlay, SClientTickEnd, SCloseContainer,
SContainerButtonClick, SCookieResponse, SCustomPayload, SInteract, SKeepAlive, SCommandSuggestion, SConfirmTeleport, SContainerButtonClick, SCookieResponse,
SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPlayPingRequest, SPlayerAbilities, SCustomClickAction as SPlayCustomClickAction, SCustomPayload, SDebugSampleSubscription,
SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition, SDebugSubscriptionRequest, SEditBook, SInteract, SJigsawGenerate,
SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SSetCommandBlock, SKeepAlive as SPlayKeepAlive, SMoveVehicle, SPaddleBoat, SPickItemFromBlock,
SSetCreativeSlot, SSetHeldItem, SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem, 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, SUseItemOn,
}, },
status::SStatusPingRequest, status::{SStatusPingRequest, SStatusRequest},
}, },
}; };
use pumpkin_util::version::JavaMinecraftVersion; 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. // 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]) { fn fuzz_all_deserializers(payload: &[u8]) {
let mut cursor = Cursor::new(payload);
macro_rules! run_read { macro_rules! run_read {
($($packet:ty),* $(,)?) => { ($($packet:ty),* $(,)?) => {
$( $(
cursor.set_position(0); let mut slice = payload;
let _ = <$packet>::read(&mut cursor, &TARGET_VERSION); let _ = <$packet>::read(&mut slice, &TARGET_VERSION);
)* )*
}; };
} }
@@ -50,54 +63,85 @@ fn fuzz_all_deserializers(payload: &[u8]) {
SHandShake, SHandShake,
// Status // Status
SStatusPingRequest, SStatusPingRequest,
SStatusRequest,
// Login // Login
SLoginStart, SLoginStart,
SEncryptionResponse, SEncryptionResponse,
SLoginPluginResponse, SLoginPluginResponse,
SLoginCookieResponse, SLoginCookieResponse,
SLoginAcknowledged,
// Config // Config
SAcknowledgeFinishConfig,
SClientInformationConfig, SClientInformationConfig,
SPluginMessage,
SKnownPacks,
SConfigCookieResponse, SConfigCookieResponse,
SConfigCustomClickAction,
SConfigKeepAlive,
SKnownPacks,
SPluginMessage,
SConfigResourcePack, SConfigResourcePack,
// Play // Play
SConfirmTeleport, SAttack,
SBundleItemSelected,
SChangeGameMode, SChangeGameMode,
SChatCommand, SChatCommand,
SChatMessage, SChatMessage,
SClientInformationPlay, SChunkBatch,
SClickSlot,
SClientCommand, SClientCommand,
SPlayerInput, SClientInformationPlay,
SClientTickEnd,
SCloseContainer,
SCommandSuggestion,
SConfirmTeleport,
SContainerButtonClick,
SCookieResponse,
SPlayCustomClickAction,
SCustomPayload,
SDebugSampleSubscription,
SDebugSubscriptionRequest,
SEditBook,
SInteract,
SJigsawGenerate,
SPlayKeepAlive,
SMoveVehicle, SMoveVehicle,
SPaddleBoat, SPaddleBoat,
SInteract, SPickItemFromBlock,
SKeepAlive, SPickItemFromEntity,
SPlayPingRequest,
SPlaceRecipe,
SPlayerAbilities,
SPlayerAction,
SPlayerCommand,
SPlayerInput,
SPlayerLoaded,
SPlayerPosition, SPlayerPosition,
SPlayerPositionRotation, SPlayerPositionRotation,
SPlayerRotation, SPlayerRotation,
SSetPlayerGround,
SPickItemFromBlock,
SPlayerAbilities,
SPlayerAction,
SSetCommandBlock,
SPlayerCommand,
SPlayerLoaded,
SPlayPingRequest,
SClickSlot,
SContainerButtonClick,
SSetHeldItem,
SSetCreativeSlot,
SSwingArm,
SUpdateSign,
SUseItemOn,
SUseItem,
SCommandSuggestion,
SCookieResponse,
SCloseContainer,
SChunkBatch,
SPlayerSession, 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 => { 2 => {
let mut aes_key = [0u8; 16]; let mut aes_key = [0u8; 16];
aes_key.copy_from_slice(key); aes_key.copy_from_slice(key);
decoder.set_encryption(&aes_key); let _ = decoder.set_encryption(&aes_key);
} }
3 => { 3 => {
decoder.set_compression(256); decoder.set_compression(256);
let mut aes_key = [0u8; 16]; let mut aes_key = [0u8; 16];
aes_key.copy_from_slice(key); 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] #![no_main]
use libfuzzer_sys::fuzz_target; use libfuzzer_sys::fuzz_target;
use pumpkin_protocol::bedrock::packet_encoder::BedrockBatchEncoder;
use pumpkin_protocol::bedrock::SubClient; use pumpkin_protocol::bedrock::SubClient;
use pumpkin_protocol::bedrock::packet_encoder::BedrockBatchEncoder;
fuzz_target!(|data: &[u8]| { fuzz_target!(|data: &[u8]| {
if data.len() < 10 { if data.len() < 10 {
@@ -33,11 +33,5 @@ fuzz_target!(|data: &[u8]| {
} }
let mut out = Vec::new(); let mut out = Vec::new();
let _ = encoder.write_game_packet( let _ = encoder.write_game_packet(packet_id, sender, target, packet_payload, &mut out);
packet_id,
sender,
target,
packet_payload,
&mut out
);
}); });

View File

@@ -1,14 +1,13 @@
#![no_main] #![no_main]
use libfuzzer_sys::fuzz_target; 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::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 pumpkin_util::version::JavaMinecraftVersion;
use std::io::Cursor;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
const TARGET_VERSION: JavaMinecraftVersion = JavaMinecraftVersion::V_1_21_4; const TARGET_VERSION: JavaMinecraftVersion = JavaMinecraftVersion::V_1_21_4;
@@ -34,24 +33,28 @@ fuzz_target!(|data: &[u8]| {
encoder.set_compression((compression_threshold, compression_level)); encoder.set_compression((compression_threshold, compression_level));
} }
if use_encryption { if use_encryption {
encoder.set_encryption(&encryption_key); let _ = encoder.set_encryption(&encryption_key);
} }
// 1. Fuzz with raw bytes (simulating various packet payloads) // 1. Fuzz with raw bytes (simulating various packet payloads)
let mut buf = Vec::new(); 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)); let _ = buf.write_var_int(&VarInt(packet_id));
buf.extend_from_slice(packet_data); buf.extend_from_slice(packet_data);
let _ = encoder.write_packet(buf.into()).await; let _ = encoder.write_packet(buf.into()).await;
// 2. Fuzz with actual packets if they can be partially read // 2. Fuzz with actual packets if they can be partially read
let mut cursor = Cursor::new(packet_data); let mut slice = packet_data;
if let Ok(packet) = CPlayerPosition::read(&mut cursor, &TARGET_VERSION) { if let Ok(packet) = CPlayerPosition::read(&mut slice, &TARGET_VERSION) {
let mut packet_buf = Vec::new(); let mut packet_buf = Vec::new();
let id = CPlayerPosition::to_id(TARGET_VERSION); let id = CPlayerPosition::to_id(TARGET_VERSION);
let _ = packet_buf.write_var_int(&VarInt(id)); let _ = packet_buf.write_var_int(&VarInt(id));
let _ = packet.write_packet_data(&mut packet_buf, &TARGET_VERSION); let _ = packet.write_packet_data(&mut packet_buf, &TARGET_VERSION);
let _ = encoder.write_packet(packet_buf.into()).await; let _ = encoder.write_packet(packet_buf.into()).await;
} }
let _ = encoder.flush().await; let _ = encoder.flush().await;

View File

@@ -1,6 +1,6 @@
#![no_main] #![no_main]
use libfuzzer_sys::fuzz_target; use libfuzzer_sys::fuzz_target;
use pumpkin::net::rcon::packet::Packet; use pumpkin_protocol::rcon::Packet;
fuzz_target!(|data: &[u8]| { fuzz_target!(|data: &[u8]| {
let mut incoming = data.to_vec(); 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 std::num::NonZeroI32;
use pumpkin_data::item::{BedrockItem, JavaToBedrockItemMapping}; 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 block_runtime_id = VarInt(VarUInt::read(buf)?.0 as i32);
let user_data_len = VarUInt::read(buf)?.0; let user_data_len = VarUInt::read(buf)?.0 as usize;
let mut user_data = vec![0u8; user_data_len 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)?; buf.read_exact(&mut user_data)?;
let (nbt_data, place_on_blocks, destroy_blocks, shield_blocking_tick) = 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 block_runtime_id = VarInt(VarUInt::read(buf)?.0 as i32);
let user_data_len = VarUInt::read(buf)?.0; let user_data_len = VarUInt::read(buf)?.0 as usize;
let mut user_data = vec![0u8; user_data_len 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)?; buf.read_exact(&mut user_data)?;
let (nbt_data, place_on_blocks, destroy_blocks, shield_blocking_tick) = 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 block_runtime_id = VarUInt::read(buf)?;
let extra_data_len = VarUInt::read(buf)?.0; let extra_data_len = VarUInt::read(buf)?.0 as usize;
let mut extra_data = vec![0u8; extra_data_len 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)?; buf.read_exact(&mut extra_data)?;
Ok(Self { Ok(Self {
@@ -513,8 +531,14 @@ impl PacketRead for NetworkItemStack {
let aux_value = VarUInt::read(buf)?; let aux_value = VarUInt::read(buf)?;
let block_runtime_id = VarInt::read(buf)?; let block_runtime_id = VarInt::read(buf)?;
let extra_data_len = VarUInt::read(buf)?.0; let extra_data_len = VarUInt::read(buf)?.0 as usize;
let mut extra_data = vec![0u8; extra_data_len 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)?; buf.read_exact(&mut extra_data)?;
Ok(Self { 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> { fn read_user_data_strings<R: Read>(reader: &mut R) -> Result<Vec<String>, Error> {
let len = i32::read(reader)?; let len = i32::read(reader)?;
if len < 0 { if !(0..=1024).contains(&len) {
return Err(Error::new( return Err(Error::new(
std::io::ErrorKind::InvalidData, 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 { for _ in 0..len {
let mut length = [0; 2]; let mut length = [0; 2];
reader.read_exact(&mut length)?; 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)?; reader.read_exact(&mut bytes)?;
values.push( values.push(
String::from_utf8(bytes) String::from_utf8(bytes)

View File

@@ -13,7 +13,13 @@ pub struct SClientCacheBlobStatus {
impl PacketRead for SClientCacheBlobStatus { impl PacketRead for SClientCacheBlobStatus {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> { fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let miss_count = VarUInt::read(reader)?.0 as usize; 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 { for _ in 0..miss_count {
let mut bytes = [0u8; 8]; let mut bytes = [0u8; 8];
reader.read_exact(&mut bytes)?; reader.read_exact(&mut bytes)?;
@@ -21,7 +27,13 @@ impl PacketRead for SClientCacheBlobStatus {
} }
let hit_count = VarUInt::read(reader)?.0 as usize; 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 { for _ in 0..hit_count {
let mut bytes = [0u8; 8]; let mut bytes = [0u8; 8];
reader.read_exact(&mut bytes)?; reader.read_exact(&mut bytes)?;

View File

@@ -53,6 +53,12 @@ impl PacketRead for StackRequestItem {
let count = i16::read(reader)? as u16; let count = i16::read(reader)? as u16;
let block_runtime_id = VarUInt::read(reader)?; let block_runtime_id = VarUInt::read(reader)?;
let data_len = VarUInt::read(reader)?.0 as usize; 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]; let mut extra_data = vec![0; data_len];
reader.read_exact(&mut extra_data)?; reader.read_exact(&mut extra_data)?;
Ok(Self { Ok(Self {

View File

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

View File

@@ -13,12 +13,22 @@ pub struct SResourcePackResponse {
impl PacketRead for SResourcePackResponse { impl PacketRead for SResourcePackResponse {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> { fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let encoded_status = VarUInt::read(reader)?.0; let encoded_status = VarUInt::read(reader)?.0;
let response = u8::try_from(encoded_status + 1) let response = encoded_status
.map_err(|_| Error::new(ErrorKind::InvalidData, "resource pack status is too large"))?; .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 _status_name = String::read(reader)?;
let pack_ids = if response == Self::STATUS_SEND_PACKS { let pack_ids = if response == Self::STATUS_SEND_PACKS {
let count = VarUInt::read(reader)?.0; let count = VarUInt::read(reader)?.0;
if count > 1024 {
return Err(Error::new(
ErrorKind::InvalidData,
"too many resource pack identifiers",
));
}
(0..count) (0..count)
.map(|_| String::read(reader)) .map(|_| String::read(reader))
.collect::<Result<Vec<_>, _>>()? .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> { fn skip_effect_parameters(seq: &mut impl NetworkReadExt) -> Result<(), ReadingError> {
// amplifier const MAX_EFFECT_DEPTH: usize = 32;
seq.get_var_int()?; let mut depth = 0;
// duration loop {
seq.get_var_int()?; // amplifier
// ambient seq.get_var_int()?;
seq.get_bool()?; // duration
// show_particles seq.get_var_int()?;
seq.get_bool()?; // ambient
// show_icon seq.get_bool()?;
seq.get_bool()?; // show_particles
// has_hidden (recursive) seq.get_bool()?;
let has_hidden = seq.get_bool()?; // show_icon
if has_hidden { seq.get_bool()?;
skip_effect_parameters(seq)?; // 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(()) Ok(())
} }

View File

@@ -109,6 +109,9 @@ fn read_length_prefixed_component(
let byte_len: usize = byte_len let byte_len: usize = byte_len
.try_into() .try_into()
.map_err(|_| ReadingError::Message("Negative component data length".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 component_impl = if byte_len <= 256 {
let mut stack_buf = [0u8; 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_data::packet::serverbound::PLAY_CLIENT_TICK_END;
use pumpkin_macros::java_packet; use pumpkin_macros::java_packet;
use pumpkin_util::version::JavaMinecraftVersion;
#[java_packet(PLAY_CLIENT_TICK_END)] #[java_packet(PLAY_CLIENT_TICK_END)]
pub struct SClientTickEnd; 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 session_id = read.get_uuid()?;
let expires_at = read.get_i64_be()?; 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]; let mut public_key = vec![0u8; public_key_length];
read.read_bytes_to_buf(&mut public_key)?; 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]; let mut key_signature = vec![0u8; key_signature_length];
read.read_bytes_to_buf(&mut key_signature)?; read.read_bytes_to_buf(&mut key_signature)?;

View File

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

View File

@@ -35,7 +35,6 @@ pub enum ClientboundPacket {
impl ClientboundPacket { impl ClientboundPacket {
#[must_use] #[must_use]
pub fn write_buf(self, id: i32, body: &str) -> BytesMut { pub fn write_buf(self, id: i32, body: &str) -> BytesMut {
// let len = outgoing.len() as u64;
let mut buf = BytesMut::new(); let mut buf = BytesMut::new();
// 10 is for 4 bytes ty, 4 bytes id, and 2 terminating nul bytes. // 10 is for 4 bytes ty, 4 bytes id, and 2 terminating nul bytes.
buf.put_i32_le(10 + body.len() as i32); buf.put_i32_le(10 + body.len() as i32);
@@ -53,9 +52,9 @@ impl ClientboundPacket {
pub enum PacketError { pub enum PacketError {
#[error("Invalid length")] #[error("Invalid length")]
InvalidLength, InvalidLength,
#[error("Dailed to send packet")] #[error("Failed to send packet")]
FailedSend(std::io::Error), FailedSend(std::io::Error),
#[error("Missing packet lull terminator")] #[error("Missing packet null terminator")]
MissingNullTerminator, MissingNullTerminator,
#[error("Invalid packet string body")] #[error("Invalid packet string body")]
InvalidBody(std::str::Utf8Error), 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>; fn get_str_bounded(&mut self, bound: usize) -> Result<Box<str>, ReadingError>;
#[inline] #[inline]
fn get_str(&mut self) -> Result<Box<str>, ReadingError> { 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_uuid(&mut self) -> Result<uuid::Uuid, ReadingError>;
fn get_fixed_bitset(&mut self, bits: usize) -> Result<FixedBitSet, ReadingError>; fn get_fixed_bitset(&mut self, bits: usize) -> Result<FixedBitSet, ReadingError>;
@@ -164,7 +164,7 @@ pub trait NetworkReadSliceExt<'a> {
#[inline] #[inline]
fn get_cow_str_borrowed(&mut self) -> Result<Cow<'a, str>, ReadingError> { 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] #[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> { fn get_str_bounded_borrowed(&mut self, bound: usize) -> Result<&'a str, ReadingError> {
let bytes_len = self.get_var_uint()?.0 as usize; 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 { if bytes_len > maximum_utf8_bytes {
return Err(ReadingError::TooLarge(format!( return Err(ReadingError::TooLarge(format!(
"string has too many bytes ({bytes_len} > {maximum_utf8_bytes})" "string has too many bytes ({bytes_len} > {maximum_utf8_bytes})"
@@ -225,7 +225,7 @@ impl<'a> NetworkReadSliceExt<'a> for &'a [u8] {
#[inline] #[inline]
fn get_str_borrowed(&mut self) -> Result<&'a str, ReadingError> { 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. // 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: // 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 { if bytes_len > maximum_utf8_bytes {
return Err(ReadingError::TooLarge(format!( return Err(ReadingError::TooLarge(format!(
"string has too many bytes ({bytes_len} > {maximum_utf8_bytes})" "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> { impl<T: PacketRead> PacketRead for Vec<T> {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> { fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let len = VarUInt::read(reader)?.0 as _; let len = VarUInt::read(reader)?.0 as usize;
let mut buf = Self::with_capacity(len); 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 { for _ in 0..len {
buf.push(T::read(reader)?); buf.push(T::read(reader)?);
} }

View File

@@ -72,8 +72,8 @@ rsa.workspace = true
# verification & WASM parsing # verification & WASM parsing
ed25519-dalek.workspace = true ed25519-dalek.workspace = true
hex.workspace = true hex.workspace = true
wasmparser = "0.255" wasmparser = "0.256"
wasm-encoder = "0.255" wasm-encoder = "0.256"
# authentication # authentication
ureq = { workspace = true, features = ["json"] } 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_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_SERVICES_URL: &str = "https://api.minecraftservices.com/";
const MOJANG_PROFILE_BY_NAME_URL: &str = const MOJANG_PROFILE_BY_NAME_URL: &str =
"https://api.mojang.com/users/profiles/minecraft/{username}"; "https://api.mojang.com/users/profiles/minecraft/{username}";

View File

@@ -811,7 +811,7 @@ impl BedrockClient {
Ok(packet) => Some(packet), Ok(packet) => Some(packet),
Err(err) => { Err(err) => {
if !matches!(err, PacketDecodeError::ConnectionClosed) { 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}"); let text = format!("Error while reading incoming packet {err}");
self.kick(DisconnectReason::BadPacket, text).await; self.kick(DisconnectReason::BadPacket, text).await;
} }

View File

@@ -411,7 +411,7 @@ impl JavaClient {
Ok(packet) => Some(packet), Ok(packet) => Some(packet),
Err(err) => { Err(err) => {
if !matches!(err, PacketDecodeError::ConnectionClosed) { 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}"); let text = format!("Error while reading incoming packet {err}");
self.kick(TextComponent::text(text)).await; self.kick(TextComponent::text(text)).await;
} }

View File

@@ -125,7 +125,7 @@ impl PendingConnection {
Ok(packet) => Some(packet), Ok(packet) => Some(packet),
Err(err) => { Err(err) => {
if !matches!(err, PacketDecodeError::ConnectionClosed) { 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}"); let text = format!("Error while reading incoming packet {err}");
self.kick(TextComponent::text(text)).await; self.kick(TextComponent::text(text)).await;
} }

View File

@@ -65,6 +65,9 @@ pub async fn velocity_login(connection: &mut PendingConnection) {
#[must_use] #[must_use]
pub fn check_integrity(data: (&[u8], &[u8]), secret: &str) -> bool { pub fn check_integrity(data: (&[u8], &[u8]), secret: &str) -> bool {
if secret.is_empty() {
return false;
}
let (signature, data_without_signature) = data; let (signature, data_without_signature) = data;
// Our fault, we can panic/expect? // Our fault, we can panic/expect?
let Ok(mut mac) = HmacSha256::new_from_slice(secret.as_bytes()) else { 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::command::CommandSender;
use crate::{SHOULD_STOP, STOP_INTERRUPT, server::Server}; use crate::{SHOULD_STOP, STOP_INTERRUPT, server::Server};
pub mod packet; pub use pumpkin_protocol::rcon as packet;
pub struct RCONServer; pub struct RCONServer;
impl RCONServer { impl RCONServer {
pub async fn run(config: &RCONConfig, server: Arc<Server>) { 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 { let listener = match tokio::net::TcpListener::bind(config.address).await {
Ok(l) => l, Ok(l) => l,
Err(e) => { Err(e) => {
@@ -109,7 +116,7 @@ impl RCONClient {
let config = &server.advanced_config.networking.rcon; let config = &server.advanced_config.networking.rcon;
match packet.get_type() { match packet.get_type() {
ServerboundPacket::Auth => { ServerboundPacket::Auth => {
if packet.get_body() == password { if !password.is_empty() && packet.get_body() == password {
self.send(ClientboundPacket::AuthResponse, packet.get_id(), "") self.send(ClientboundPacket::AuthResponse, packet.get_id(), "")
.await?; .await?;
if config.logging.logged_successfully { 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());
}
}