chore: more plugin api work

also added bedrock fuzzing
This commit is contained in:
Alexander Medvedev 2026-04-01 19:37:01 +02:00
parent 7bf248d16a
commit c15ebf181d
15 changed files with 625 additions and 137 deletions

15
Cargo.lock generated
View File

@ -2735,7 +2735,7 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
"ureq", "ureq",
"uuid", "uuid",
"wasmparser 0.245.1", "wasmparser 0.246.1",
"wasmtime", "wasmtime",
"wasmtime-wasi", "wasmtime-wasi",
] ]
@ -4352,6 +4352,19 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "wasmparser"
version = "0.246.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d991c35d79bf8336dc1cd632ed4aacf0dc5fac4bc466c670625b037b972bb9c"
dependencies = [
"bitflags",
"hashbrown 0.16.1",
"indexmap",
"semver",
"serde",
]
[[package]] [[package]]
name = "wasmprinter" name = "wasmprinter"
version = "0.245.1" version = "0.245.1"

View File

@ -181,7 +181,7 @@ ureq = "3.3.0"
wasmtime = "43.0" wasmtime = "43.0"
wasmtime-wasi = "43.0" wasmtime-wasi = "43.0"
wit-bindgen = "0.54" wit-bindgen = "0.54"
wasmparser = "0.245" wasmparser = "0.246"
postcard = { version = "1.1", features = ["alloc"] } postcard = { version = "1.1", features = ["alloc"] }
tracing-serde-structured = "0.4" tracing-serde-structured = "0.4"

View File

@ -4,6 +4,16 @@ interface player {
use permission.{permission-level}; use permission.{permission-level};
use text.{text-component}; use text.{text-component};
record player-abilities {
invulnerable: bool,
flying: bool,
allow-flying: bool,
creative: bool,
allow-modify-world: bool,
fly-speed: f32,
walk-speed: f32,
}
resource player { resource player {
get-id: func () -> result<string, string>; get-id: func () -> result<string, string>;
get-name: func () -> result<string, string>; get-name: func () -> result<string, string>;
@ -45,8 +55,23 @@ interface player {
get-max-health: func () -> result<f32, string>; get-max-health: func () -> result<f32, string>;
get-food-level: func () -> result<u8, string>; get-food-level: func () -> result<u8, string>;
get-saturation: func () -> result<f32, string>; get-saturation: func () -> result<f32, string>;
get-experience-level: func () -> result<s32, string>;
get-experience-progress: func () -> result<f32, string>; get-experience-level: func() -> result<s32, string>;
get-experience-points: func () -> result<s32, string>; get-experience-progress: func() -> result<f32, string>;
get-experience-points: func() -> result<s32, string>;
set-experience-level: func(level: s32) -> result<_, string>;
set-experience-progress: func(progress: f32) -> result<_, string>;
set-experience-points: func(points: s32) -> result<_, string>;
add-experience-levels: func(levels: s32) -> result<_, string>;
add-experience-points: func(points: s32) -> result<_, string>;
is-sneaking: func() -> result<bool, string>;
is-sprinting: func() -> result<bool, string>;
is-on-ground: func() -> result<bool, string>;
is-flying: func() -> result<bool, string>;
set-flying: func(flying: bool) -> result<_, string>;
get-abilities: func() -> result<player-abilities, string>;
set-abilities: func(abilities: player-abilities) -> result<_, string>;
} }
} }

View File

@ -7,6 +7,16 @@ interface server {
} }
resource server { resource server {
/// Returns the current difficulty level of the server
get-difficulty: func() -> difficulty; get-difficulty: func() -> difficulty;
/// Returns the total number of players connected across all worlds.
get-player-count: func() -> u32;
/// Returns the rolling average Milliseconds Per Tick (MSPT) over the last 100 ticks
get-mspt: func() -> f64;
/// Returns the effective Ticks Per Second (TPS)
get-tps: func() -> f64;
} }
} }

View File

@ -1,5 +1,49 @@
interface %world { interface %world {
resource %world { enum piston-behavior {
get-id: func () -> string; normal,
destroy,
block,
ignore,
push-only
} }
}
flags block-flags {
notify-neighbors,
notify-listeners,
force-state,
skip-drops,
moved,
skip-redstone-wire-state-replacement,
skip-block-entity-replaced-callback,
skip-block-added-callback,
}
record block-state {
id: u16,
luminance: u8,
opacity: u8,
hardness: f32,
is-air: bool,
is-liquid: bool,
is-solid: bool,
is-full-cube: bool,
piston-behavior: piston-behavior,
has-random-ticks: bool,
}
record block-pos {
x: s32,
y: s32,
z: s32,
}
resource %world {
get-id: func() -> string;
get-block-state-id: func(pos: block-pos) -> u16;
get-block-state: func(pos: block-pos) -> block-state;
set-block-state: func(pos: block-pos, state: u16, update-flags: block-flags);
}
}

View File

@ -33,9 +33,9 @@ checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
[[package]] [[package]]
name = "async-compression" name = "async-compression"
version = "0.4.40" version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d67d43201f4d20c78bcda740c142ca52482d81da80681533d33bf3f0596c8e2" checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
dependencies = [ dependencies = [
"compression-codecs", "compression-codecs",
"compression-core", "compression-core",
@ -289,9 +289,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]] [[package]]
name = "crossfire" name = "crossfire"
version = "3.1.3" version = "3.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4150133c3f558d3190b52a4d36b571e29c18a24e98d24d12201bed0c038986f8" checksum = "e0042e53977a94d5d10de04ce7eb6016aa212c08e83ea202f7a9f102ff104303"
dependencies = [ dependencies = [
"crossbeam-utils", "crossbeam-utils",
"futures-core", "futures-core",
@ -1039,9 +1039,19 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "pumpkin-codecs"
version = "0.1.0-dev+26.1"
dependencies = [
"dashmap",
"either",
"serde_json",
"tracing",
]
[[package]] [[package]]
name = "pumpkin-config" name = "pumpkin-config"
version = "0.1.0-dev+1.21.11" version = "0.1.0-dev+26.1"
dependencies = [ dependencies = [
"pumpkin-util", "pumpkin-util",
"serde", "serde",
@ -1052,18 +1062,19 @@ dependencies = [
[[package]] [[package]]
name = "pumpkin-data" name = "pumpkin-data"
version = "0.1.0-dev+1.21.11" version = "0.1.0-dev+26.1"
dependencies = [ dependencies = [
"crc-fast", "crc-fast",
"phf", "phf",
"pumpkin-nbt", "pumpkin-nbt",
"pumpkin-util", "pumpkin-util",
"rand",
"serde", "serde",
] ]
[[package]] [[package]]
name = "pumpkin-macros" name = "pumpkin-macros"
version = "0.1.0-dev+1.21.11" version = "0.1.0-dev+26.1"
dependencies = [ dependencies = [
"proc-macro-error2", "proc-macro-error2",
"pumpkin-data", "pumpkin-data",
@ -1073,18 +1084,20 @@ dependencies = [
[[package]] [[package]]
name = "pumpkin-nbt" name = "pumpkin-nbt"
version = "0.1.0-dev+1.21.11" version = "0.1.0-dev+26.1"
dependencies = [ dependencies = [
"bytes", "bytes",
"cesu8", "cesu8",
"flate2", "flate2",
"pumpkin-codecs",
"serde", "serde",
"thiserror", "thiserror",
"tracing",
] ]
[[package]] [[package]]
name = "pumpkin-protocol" name = "pumpkin-protocol"
version = "0.1.0-dev+1.21.11" version = "0.1.0-dev+26.1"
dependencies = [ dependencies = [
"aes", "aes",
"async-compression", "async-compression",
@ -1110,12 +1123,13 @@ version = "0.0.0"
dependencies = [ dependencies = [
"libfuzzer-sys", "libfuzzer-sys",
"pumpkin-protocol", "pumpkin-protocol",
"pumpkin-util",
"tokio", "tokio",
] ]
[[package]] [[package]]
name = "pumpkin-util" name = "pumpkin-util"
version = "0.1.0-dev+1.21.11" version = "0.1.0-dev+26.1"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
@ -1139,7 +1153,7 @@ dependencies = [
[[package]] [[package]]
name = "pumpkin-world" name = "pumpkin-world"
version = "0.1.0-dev+1.21.11" version = "0.1.0-dev+26.1"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"bytes", "bytes",
@ -1160,7 +1174,7 @@ dependencies = [
"rustc-hash", "rustc-hash",
"ruzstd", "ruzstd",
"serde", "serde",
"sha2 0.11.0-rc.5", "sha2 0.11.0",
"slotmap", "slotmap",
"thiserror", "thiserror",
"tokio", "tokio",
@ -1231,9 +1245,9 @@ dependencies = [
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.1" version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]] [[package]]
name = "rustversion" name = "rustversion"
@ -1321,9 +1335,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "1.0.4" version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
@ -1341,12 +1355,12 @@ dependencies = [
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.11.0-rc.5" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures 0.2.17", "cpufeatures 0.3.0",
"digest 0.11.0", "digest 0.11.0",
] ]
@ -1486,9 +1500,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.49.0" version = "1.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
@ -1528,9 +1542,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "1.0.3+spec-1.1.0" version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" checksum = "994b95d9e7bae62b34bab0e2a4510b801fa466066a6a8b2b57361fa1eba068ee"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde_core", "serde_core",
@ -1543,27 +1557,27 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "1.0.0+spec-1.1.0" version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
[[package]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.0.9+spec-1.1.0" version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9"
dependencies = [ dependencies = [
"winnow", "winnow",
] ]
[[package]] [[package]]
name = "toml_writer" name = "toml_writer"
version = "1.0.6+spec-1.1.0" version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
[[package]] [[package]]
name = "tracing" name = "tracing"
@ -1632,9 +1646,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.21.0" version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
dependencies = [ dependencies = [
"getrandom 0.4.1", "getrandom 0.4.1",
"js-sys", "js-sys",
@ -1843,9 +1857,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.14" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"

View File

@ -19,9 +19,19 @@ libfuzzer-sys = "0.4"
[dependencies.pumpkin-protocol] [dependencies.pumpkin-protocol]
path = ".." path = ".."
[dependencies.pumpkin-util]
path = "../../pumpkin-util"
[[bin]] [[bin]]
name = "decoder_java" name = "decoder_java"
path = "fuzz_targets/decoder_java.rs" path = "fuzz_targets/decoder_java.rs"
test = false test = false
doc = false doc = false
bench = false bench = false
[[bin]]
name = "decoder_bedrock"
path = "fuzz_targets/decoder_bedrock.rs"
test = false
doc = false
bench = false

View File

@ -0,0 +1,97 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use pumpkin_protocol::bedrock::packet_decoder::UDPNetworkDecoder;
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,
raknet::{
connection::SConnectionRequest,
open_connection::{SOpenConnectionRequest1, SOpenConnectionRequest2},
unconnected_ping::SUnconnectedPing,
},
request_chunk_radius::SRequestChunkRadius,
request_network_settings::SRequestNetworkSettings,
text::SText,
};
use pumpkin_protocol::serial::PacketRead;
use std::io::Cursor;
// ---------------------------------------------------------------------------
// Helper: Run every Serverbound packet's read method.
// ---------------------------------------------------------------------------
fn fuzz_serverbound_packets(payload: &[u8]) {
let mut cursor = Cursor::new(payload);
macro_rules! run_read {
($($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
run_read!(
SClientCacheStatus,
SCommandRequest,
SContainerClose,
SInteraction,
SLoadingScreen,
SLogin,
SPlayerAuthInput,
SRequestChunkRadius,
SRequestNetworkSettings,
SText,
);
// RakNet Handshake Packets (Usually read without version)
cursor.set_position(0);
let _ = SConnectionRequest::read(&mut cursor);
cursor.set_position(0);
let _ = SOpenConnectionRequest1::read(&mut cursor);
cursor.set_position(0);
let _ = SOpenConnectionRequest2::read(&mut cursor);
cursor.set_position(0);
let _ = SUnconnectedPing::read(&mut cursor);
}
// ---------------------------------------------------------------------------
// Fuzz Target
// ---------------------------------------------------------------------------
fuzz_target!(|data: &[u8]| {
if data.len() < 20 {
return;
}
// Split data for decoder configuration vs raw payload
let threshold_raw = data[0];
let key: [u8; 16] = data[1..17].try_into().unwrap();
let stream_data = &data[17..];
let mut decoder = UDPNetworkDecoder::new();
// Setup Decoder
if threshold_raw > 0 {
// Assuming your CompressionThreshold is a wrapper around u32
decoder.set_compression((threshold_raw as u32).try_into().unwrap());
}
decoder.set_encryption(&key);
// 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) {
// If framed correctly, fuzz the internal payload
fuzz_serverbound_packets(&raw_packet.payload);
}
// 2. Shotgun Fuzz
// Pass raw data directly to all readers to catch panics in the parsers
fuzz_serverbound_packets(stream_data);
});

View File

@ -1,96 +1,102 @@
#![no_main] #![no_main]
use libfuzzer_sys::fuzz_target; use libfuzzer_sys::fuzz_target;
use pumpkin_protocol::{ use pumpkin_protocol::ServerPacket;
ServerPacket, use pumpkin_protocol::java::{
java::{ packet_decoder::TCPNetworkDecoder,
packet_decoder::TCPNetworkDecoder, server::{
server::{ config::{
config::{ SClientInformationConfig, SConfigCookieResponse, SConfigResourcePack, SKnownPacks,
SClientInformationConfig, SConfigCookieResponse, SConfigResourcePack, SKnownPacks, SPluginMessage,
SPluginMessage,
},
handshake::SHandShake,
login::{SEncryptionResponse, SLoginCookieResponse, SLoginPluginResponse, SLoginStart},
play::{
SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClickSlot,
SClientCommand, SClientInformationPlay, SCloseContainer, SCommandSuggestion,
SConfirmTeleport, SCookieResponse, SCustomPayload, SInteract, SKeepAlive,
SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPlayPingRequest, SPlayerAbilities,
SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition,
SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SSetCommandBlock,
SSetCreativeSlot, SSetHeldItem, SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem,
SUseItemOn,
},
status::SStatusPingRequest,
}, },
handshake::SHandShake,
login::{SEncryptionResponse, SLoginCookieResponse, SLoginPluginResponse, SLoginStart},
play::{
SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClickSlot, SClientCommand,
SClientInformationPlay, SCloseContainer, SCommandSuggestion, SConfirmTeleport,
SCookieResponse, SCustomPayload, SInteract, SKeepAlive, SMoveVehicle, SPaddleBoat,
SPickItemFromBlock, SPlayPingRequest, SPlayerAbilities, SPlayerAction, SPlayerCommand,
SPlayerInput, SPlayerLoaded, SPlayerPosition, SPlayerPositionRotation, SPlayerRotation,
SPlayerSession, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, SSetPlayerGround,
SSwingArm, SUpdateSign, SUseItem, SUseItemOn,
},
status::SStatusPingRequest,
}, },
}; };
use pumpkin_util::version::MinecraftVersion;
use std::io::Cursor; use std::io::Cursor;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
const TARGET_VERSION: MinecraftVersion = MinecraftVersion::V_26_1;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helper: run every known ServerPacket::read against the same payload. // Helper: run every known ServerPacket::read against the same payload.
// We don't care about the result — only that nothing panics. // Uses a Cursor and the Version enum as required by the new signature.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
fn fuzz_all_deserializers(payload: &[u8]) { fn fuzz_all_deserializers(payload: &[u8]) {
// Handshake let mut cursor = Cursor::new(payload);
let _ = SHandShake::read(payload);
// Status macro_rules! run_read {
//let _ = SStatusRequest::read(payload); ($($packet:ty),* $(,)?) => {
let _ = SStatusPingRequest::read(payload); $(
cursor.set_position(0);
let _ = <$packet>::read(&mut cursor, &TARGET_VERSION);
)*
};
}
// Login run_read!(
let _ = SLoginStart::read(payload); // Handshake
let _ = SEncryptionResponse::read(payload); SHandShake,
let _ = SLoginPluginResponse::read(payload); // Status
//let _ = SLoginAcknowledged::read(payload); SStatusPingRequest,
let _ = SLoginCookieResponse::read(payload); // Login
SLoginStart,
// Config SEncryptionResponse,
let _ = SClientInformationConfig::read(payload); SLoginPluginResponse,
let _ = SPluginMessage::read(payload); SLoginCookieResponse,
//let _ = SAcknowledgeFinishConfig::read(payload); // Config
let _ = SKnownPacks::read(payload); SClientInformationConfig,
let _ = SConfigCookieResponse::read(payload); SPluginMessage,
let _ = SConfigResourcePack::read(payload); SKnownPacks,
SConfigCookieResponse,
// Play SConfigResourcePack,
let _ = SConfirmTeleport::read(payload); // Play
let _ = SChangeGameMode::read(payload); SConfirmTeleport,
let _ = SChatCommand::read(payload); SChangeGameMode,
let _ = SChatMessage::read(payload); SChatCommand,
let _ = SClientInformationPlay::read(payload); SChatMessage,
let _ = SClientCommand::read(payload); SClientInformationPlay,
let _ = SPlayerInput::read(payload); SClientCommand,
let _ = SMoveVehicle::read(payload); SPlayerInput,
let _ = SPaddleBoat::read(payload); SMoveVehicle,
let _ = SInteract::read(payload); SPaddleBoat,
let _ = SKeepAlive::read(payload); SInteract,
let _ = SPlayerPosition::read(payload); SKeepAlive,
let _ = SPlayerPositionRotation::read(payload); SPlayerPosition,
let _ = SPlayerRotation::read(payload); SPlayerPositionRotation,
let _ = SSetPlayerGround::read(payload); SPlayerRotation,
let _ = SPickItemFromBlock::read(payload); SSetPlayerGround,
let _ = SPlayerAbilities::read(payload); SPickItemFromBlock,
let _ = SPlayerAction::read(payload); SPlayerAbilities,
let _ = SSetCommandBlock::read(payload); SPlayerAction,
let _ = SPlayerCommand::read(payload); SSetCommandBlock,
let _ = SPlayerLoaded::read(payload); SPlayerCommand,
let _ = SPlayPingRequest::read(payload); SPlayerLoaded,
let _ = SClickSlot::read(payload); SPlayPingRequest,
let _ = SSetHeldItem::read(payload); SClickSlot,
let _ = SSetCreativeSlot::read(payload); SSetHeldItem,
let _ = SSwingArm::read(payload); SSetCreativeSlot,
let _ = SUpdateSign::read(payload); SSwingArm,
let _ = SUseItemOn::read(payload); SUpdateSign,
let _ = SUseItem::read(payload); SUseItemOn,
let _ = SCommandSuggestion::read(payload); SUseItem,
let _ = SCookieResponse::read(payload); SCommandSuggestion,
let _ = SCloseContainer::read(payload); SCookieResponse,
let _ = SChunkBatch::read(payload); SCloseContainer,
let _ = SPlayerSession::read(payload); SChunkBatch,
let _ = SCustomPayload::read(payload); SPlayerSession,
SCustomPayload,
);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -101,13 +107,6 @@ fuzz_target!(|data: &[u8]| {
return; return;
} }
// Byte layout:
// [0] = decoder mode (0=plain, 1=compressed, 2=encrypted, 3=both)
// [1] = split point % rest.len() — how much goes to the decoder vs
// the deserializers, letting the fuzzer explore both paths
// independently with different byte patterns
// [2..18] = 16-byte AES key candidate
// [18..] = raw bytes fed to both fuzzing paths
let mode = data[0] % 4; let mode = data[0] % 4;
let key = &data[2..18]; let key = &data[2..18];
let rest = &data[18..]; let rest = &data[18..];
@ -143,5 +142,6 @@ fuzz_target!(|data: &[u8]| {
let _ = decoder.get_raw_packet().await; let _ = decoder.get_raw_packet().await;
}); });
// --- Path 2: Individual Packet Deserializers ---------------------------
fuzz_all_deserializers(deser_bytes); fuzz_all_deserializers(deser_bytes);
}); });

View File

@ -9,7 +9,8 @@ use bytes::Bytes;
use tokio::io::{AsyncRead, BufReader, ReadBuf}; use tokio::io::{AsyncRead, BufReader, ReadBuf};
use crate::{ use crate::{
Aes128Cfb8Dec, CompressionThreshold, PacketDecodeError, RawPacket, StreamDecryptor, Aes128Cfb8Dec, CompressionThreshold, MAX_PACKET_SIZE, PacketDecodeError, RawPacket,
StreamDecryptor,
codec::var_uint::VarUInt, codec::var_uint::VarUInt,
ser::{NetworkReadExt, ReadingError}, ser::{NetworkReadExt, ReadingError},
}; };
@ -138,6 +139,9 @@ impl UDPNetworkDecoder {
})?; })?;
let packet_len = u64::from(packet_len.0); let packet_len = u64::from(packet_len.0);
if !(0..=MAX_PACKET_SIZE).contains(&packet_len) {
Err(PacketDecodeError::OutOfBounds)?;
}
let var_header = VarUInt::decode(&mut reader)?; let var_header = VarUInt::decode(&mut reader)?;
@ -159,8 +163,17 @@ impl UDPNetworkDecoder {
// Gamepacket ID: Remaining 10 bits (bits 4 to 13) // Gamepacket ID: Remaining 10 bits (bits 4 to 13)
let gamepacket_id = (header & 0x3FF) as u16; // 0x3FF is 10 bits set to 1 let gamepacket_id = (header & 0x3FF) as u16; // 0x3FF is 10 bits set to 1
let header_size = var_header.written_size();
if (packet_len as usize) < header_size {
return Err(PacketDecodeError::MalformedLength(
"Packet length is smaller than the header size".to_string(),
));
}
let payload_len = packet_len as usize - header_size;
let payload = reader let payload = reader
.read_boxed_slice(packet_len as usize - var_header.written_size()) .read_boxed_slice(payload_len)
.map_err(|err| PacketDecodeError::FailedDecompression(err.to_string()))?; .map_err(|err| PacketDecodeError::FailedDecompression(err.to_string()))?;
Ok(RawPacket { Ok(RawPacket {

View File

@ -1,5 +1,5 @@
use pumpkin_macros::packet; use pumpkin_macros::packet;
use std::io::{Error, Read}; use std::io::{Error, ErrorKind, Read};
use crate::{codec::var_uint::VarUInt, serial::PacketRead}; use crate::{codec::var_uint::VarUInt, serial::PacketRead};
@ -15,15 +15,30 @@ pub struct SLogin {
impl PacketRead for SLogin { impl PacketRead for SLogin {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> { fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
const MAX_TOKEN_SIZE: usize = 256 * 1024; // 256KB limit
let protocol_version = i32::read_be(reader)?; let protocol_version = i32::read_be(reader)?;
let _len = VarUInt::read(reader)?; let _len = VarUInt::read(reader)?;
let jwt_len = u32::read(reader)?; let jwt_len = u32::read_be(reader)? as usize;
let mut jwt = vec![0; jwt_len as _]; if jwt_len > MAX_TOKEN_SIZE {
return Err(Error::new(
ErrorKind::InvalidData,
"JWT length exceeds limit",
));
}
let mut jwt = vec![0u8; jwt_len];
reader.read_exact(&mut jwt)?; reader.read_exact(&mut jwt)?;
let raw_token_len = u32::read(reader)?; let raw_token_len = u32::read_be(reader)? as usize;
let mut raw_token = vec![0; raw_token_len as _]; if raw_token_len > MAX_TOKEN_SIZE {
return Err(Error::new(
ErrorKind::InvalidData,
"Raw token length exceeds limit",
));
}
let mut raw_token = vec![0u8; raw_token_len];
reader.read_exact(&mut raw_token)?; reader.read_exact(&mut raw_token)?;
Ok(Self { Ok(Self {

View File

@ -1,5 +1,5 @@
use std::{ use std::{
io::{Error, Read}, io::{Error, ErrorKind, Read},
net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
}; };
@ -133,8 +133,22 @@ impl<T: PacketRead, const N: usize> PacketRead for [T; N] {
impl PacketRead for String { impl PacketRead for String {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> { fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let vec = Vec::read(reader)?; const MAX_STRING_LENGTH: usize = 32767;
Ok(unsafe { Self::from_utf8_unchecked(vec) })
let len = u32::read(reader)? as usize;
if len > MAX_STRING_LENGTH {
return Err(Error::new(
ErrorKind::InvalidData,
format!("String length {len} exceeds maximum of {MAX_STRING_LENGTH}"),
));
}
let mut buf = vec![0u8; len];
reader.read_exact(&mut buf)?;
Self::from_utf8(buf)
.map_err(|_| Error::new(ErrorKind::InvalidData, "Invalid UTF-8 sequence"))
} }
} }

View File

@ -401,6 +401,135 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
Ok(player.experience_points.load(Ordering::Relaxed)) Ok(player.experience_points.load(Ordering::Relaxed))
} }
async fn set_experience_level(
&mut self,
player: Resource<Player>,
level: i32,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player.set_experience_level(level, true).await;
Ok(())
}
async fn set_experience_progress(
&mut self,
player: Resource<Player>,
progress: f32,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player
.set_experience(
player.experience_level.load(Ordering::Relaxed),
progress,
player.experience_points.load(Ordering::Relaxed),
)
.await;
Ok(())
}
async fn set_experience_points(
&mut self,
player: Resource<Player>,
points: i32,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player
.set_experience(
player.experience_level.load(Ordering::Relaxed),
player.experience_progress.load(),
points,
)
.await;
Ok(())
}
async fn add_experience_levels(
&mut self,
player: Resource<Player>,
levels: i32,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player.add_experience_levels(levels).await;
Ok(())
}
async fn add_experience_points(
&mut self,
player: Resource<Player>,
points: i32,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player.add_experience_points(points).await;
Ok(())
}
async fn is_sneaking(&mut self, player: Resource<Player>) -> Result<bool, String> {
let player = player_from_resource(self, &player)?;
Ok(player.get_entity().sneaking.load(Ordering::Relaxed))
}
async fn is_sprinting(&mut self, player: Resource<Player>) -> Result<bool, String> {
let player = player_from_resource(self, &player)?;
Ok(player.get_entity().sprinting.load(Ordering::Relaxed))
}
async fn is_on_ground(&mut self, player: Resource<Player>) -> Result<bool, String> {
let player = player_from_resource(self, &player)?;
Ok(player.get_entity().on_ground.load(Ordering::Relaxed))
}
async fn is_flying(&mut self, player: Resource<Player>) -> Result<bool, String> {
let player = player_from_resource(self, &player)?;
Ok(player.is_flying().await)
}
async fn set_flying(&mut self, player: Resource<Player>, flying: bool) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
{
let mut abilities = player.abilities.lock().await;
abilities.flying = flying;
};
player.send_abilities_update().await;
Ok(())
}
async fn get_abilities(
&mut self,
player: Resource<Player>,
) -> Result<pumpkin::plugin::player::PlayerAbilities, String> {
let player = player_from_resource(self, &player)?;
let abilities = player.abilities.lock().await;
Ok(pumpkin::plugin::player::PlayerAbilities {
invulnerable: abilities.invulnerable,
flying: abilities.flying,
allow_flying: abilities.allow_flying,
creative: abilities.creative,
allow_modify_world: abilities.allow_modify_world,
fly_speed: abilities.fly_speed,
walk_speed: abilities.walk_speed,
})
}
async fn set_abilities(
&mut self,
player: Resource<Player>,
abilities: pumpkin::plugin::player::PlayerAbilities,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
{
let mut a = player.abilities.lock().await;
a.invulnerable = abilities.invulnerable;
a.flying = abilities.flying;
a.allow_flying = abilities.allow_flying;
a.creative = abilities.creative;
a.allow_modify_world = abilities.allow_modify_world;
a.fly_speed = abilities.fly_speed;
a.walk_speed = abilities.walk_speed;
};
player.send_abilities_update().await;
Ok(())
}
async fn drop(&mut self, rep: Resource<Player>) -> wasmtime::Result<()> { async fn drop(&mut self, rep: Resource<Player>) -> wasmtime::Result<()> {
let _ = self let _ = self
.resource_table .resource_table

View File

@ -33,4 +33,22 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
pumpkin_util::Difficulty::Hard => Difficulty::Hard, pumpkin_util::Difficulty::Hard => Difficulty::Hard,
} }
} }
async fn get_player_count(
&mut self,
_server: Resource<pumpkin::plugin::server::Server>,
) -> u32 {
let server = self.server.as_ref().expect("server not available");
server.get_player_count() as u32
}
async fn get_mspt(&mut self, _server: Resource<pumpkin::plugin::server::Server>) -> f64 {
let server = self.server.as_ref().expect("server not available");
server.get_mspt()
}
async fn get_tps(&mut self, _server: Resource<pumpkin::plugin::server::Server>) -> f64 {
let server = self.server.as_ref().expect("server not available");
server.get_tps()
}
} }

View File

@ -1,5 +1,12 @@
use pumpkin_data::block_state::PistonBehavior;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::world::BlockFlags;
use wasmtime::component::Resource; use wasmtime::component::Resource;
use crate::plugin::loader::wasm::wasm_host::wit::v0_1_0::pumpkin::plugin::world::{
BlockFlags as WitBlockFlags, BlockPos as WitBlockPos, BlockState as WitBlockState,
PistonBehavior as WitPistonBehavior,
};
use crate::plugin::loader::wasm::wasm_host::{ use crate::plugin::loader::wasm::wasm_host::{
DowncastResourceExt, DowncastResourceExt,
state::{PluginHostState, WorldResource}, state::{PluginHostState, WorldResource},
@ -44,6 +51,85 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
.to_string() .to_string()
} }
async fn get_block_state_id(&mut self, world: Resource<World>, pos: WitBlockPos) -> u16 {
let world_ref = world.downcast_ref(self);
let internal_pos = BlockPos::new(pos.x, pos.y, pos.z);
world_ref.provider.get_block_state_id(&internal_pos).await
}
async fn get_block_state(&mut self, world: Resource<World>, pos: WitBlockPos) -> WitBlockState {
let world_ref = world.downcast_ref(self);
let internal_pos = BlockPos::new(pos.x, pos.y, pos.z);
// Fetch the actual BlockState struct from the world
// get_block_state typically returns &'static BlockState in Pumpkin
let state = world_ref.provider.get_block_state(&internal_pos).await;
WitBlockState {
id: state.id,
luminance: state.luminance,
opacity: state.opacity,
hardness: state.hardness,
is_air: state.is_air(),
is_liquid: state.is_liquid(),
is_solid: state.is_solid(),
is_full_cube: state.is_full_cube(),
has_random_ticks: state.has_random_ticks(),
piston_behavior: match state.piston_behavior {
PistonBehavior::Normal => WitPistonBehavior::Normal,
PistonBehavior::Destroy => WitPistonBehavior::Destroy,
PistonBehavior::Block => WitPistonBehavior::Block,
PistonBehavior::Ignore => WitPistonBehavior::Ignore,
PistonBehavior::PushOnly => WitPistonBehavior::PushOnly,
},
}
}
async fn set_block_state(
&mut self,
world: Resource<World>,
pos: WitBlockPos,
state: u16,
update_flags: WitBlockFlags,
) {
let world_ref = world.downcast_ref(self);
let internal_pos = BlockPos::new(pos.x, pos.y, pos.z);
// Map WIT flags to your internal bitflags
let mut internal_flags = BlockFlags::empty();
if update_flags.contains(WitBlockFlags::NOTIFY_NEIGHBORS) {
internal_flags |= BlockFlags::NOTIFY_NEIGHBORS;
}
if update_flags.contains(WitBlockFlags::NOTIFY_LISTENERS) {
internal_flags |= BlockFlags::NOTIFY_LISTENERS;
}
if update_flags.contains(WitBlockFlags::FORCE_STATE) {
internal_flags |= BlockFlags::FORCE_STATE;
}
if update_flags.contains(WitBlockFlags::SKIP_DROPS) {
internal_flags |= BlockFlags::SKIP_DROPS;
}
if update_flags.contains(WitBlockFlags::MOVED) {
internal_flags |= BlockFlags::MOVED;
}
if update_flags.contains(WitBlockFlags::SKIP_REDSTONE_WIRE_STATE_REPLACEMENT) {
internal_flags |= BlockFlags::SKIP_REDSTONE_WIRE_STATE_REPLACEMENT;
}
if update_flags.contains(WitBlockFlags::SKIP_BLOCK_ENTITY_REPLACED_CALLBACK) {
internal_flags |= BlockFlags::SKIP_BLOCK_ENTITY_REPLACED_CALLBACK;
}
if update_flags.contains(WitBlockFlags::SKIP_BLOCK_ADDED_CALLBACK) {
internal_flags |= BlockFlags::SKIP_BLOCK_ADDED_CALLBACK;
}
// Update the world
world_ref
.provider
.set_block_state(&internal_pos, state, internal_flags)
.await;
}
async fn drop(&mut self, rep: Resource<World>) -> wasmtime::Result<()> { async fn drop(&mut self, rep: Resource<World>) -> wasmtime::Result<()> {
let _ = self let _ = self
.resource_table .resource_table