ryerraguntla commented on code in PR #3519: URL: https://github.com/apache/iggy/pull/3519#discussion_r3812874075
########## gateways/kafka/src/protocol/api.rs: ########## @@ -0,0 +1,558 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use bytes::{Buf, Bytes}; +use kafka_protocol::messages::api_versions_response::ApiVersion; +use kafka_protocol::messages::metadata_response::{MetadataResponseBroker, MetadataResponseTopic}; +use kafka_protocol::messages::{ + ApiVersionsRequest, ApiVersionsResponse, BrokerId, CreateTopicsRequest, FetchRequest, + ListOffsetsRequest, MetadataRequest, MetadataResponse, ProduceRequest, TopicName, +}; +use kafka_protocol::protocol::{Decodable, StrBytes}; + +use crate::error::{KafkaProtocolError, Result}; +use crate::protocol::responses::{ + encode_create_topics_error_response, encode_create_topics_response, + encode_fetch_error_response, encode_fetch_response, encode_list_offsets_error_response, + encode_list_offsets_response, encode_message, encode_produce_error_response, + encode_produce_response, +}; + +pub const API_KEY_PRODUCE: i16 = 0; +pub const API_KEY_FETCH: i16 = 1; +pub const API_KEY_LIST_OFFSETS: i16 = 2; +pub const API_KEY_METADATA: i16 = 3; +pub const API_KEY_API_VERSIONS: i16 = 18; +pub const API_KEY_CREATE_TOPICS: i16 = 19; + +pub const DEFAULT_KAFKA_PORT: u16 = 9093; + +pub const ERROR_NONE: i16 = 0; +pub const ERROR_UNKNOWN_TOPIC_OR_PARTITION: i16 = 3; +/// Retriable; Produce stub uses this until the Iggy bridge persists records. +pub const ERROR_NOT_LEADER_OR_FOLLOWER: i16 = 6; +pub const ERROR_UNSUPPORTED_VERSION: i16 = 35; +pub const ERROR_INVALID_PARTITIONS: i16 = 37; +pub const ERROR_INVALID_REPLICATION_FACTOR: i16 = 38; +/// `CreateTopics` stub: do not claim topics were created (no controller / no Iggy bridge). +pub const ERROR_NOT_CONTROLLER: i16 = 41; +pub const ERROR_INVALID_REQUEST: i16 = 42; + +/// Result of handling one Kafka request body. +#[derive(Debug)] +pub enum HandleOutcome { + /// Write this response body (with a response header). + Respond(Bytes), + /// Produce with `acks=0`: write nothing, keep the connection open. + NoResponse, + /// No parseable response exists for this request; close the TCP connection. + Close, +} + +impl HandleOutcome { + /// Return the response body, or panic with `msg` if the outcome is not [`Self::Respond`]. + /// + /// # Panics + /// + /// Panics when the outcome is [`Self::NoResponse`] or [`Self::Close`]. + #[must_use] + pub fn expect_response(self, msg: &str) -> Bytes { + match self { + Self::Respond(body) => body, + Self::NoResponse => panic!("{msg}: got NoResponse"), + Self::Close => panic!("{msg}: got Close"), + } + } + + #[must_use] + pub const fn is_no_response(&self) -> bool { + matches!(self, Self::NoResponse) + } + + #[must_use] + pub const fn is_close(&self) -> bool { + matches!(self, Self::Close) + } +} + +#[derive(Debug, Clone)] +pub struct BrokerAdvertise { + pub host: String, + pub port: i32, +} + +impl Default for BrokerAdvertise { + fn default() -> Self { + Self { + host: "127.0.0.1".to_string(), + port: i32::from(DEFAULT_KAFKA_PORT), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ApiVersionRange { + pub api_key: i16, + pub min_version: i16, + pub max_version: i16, +} + +static SUPPORTED_RANGES: &[ApiVersionRange] = &[ + ApiVersionRange { + api_key: API_KEY_PRODUCE, + min_version: 3, + max_version: 9, + }, + ApiVersionRange { + api_key: API_KEY_FETCH, + min_version: 4, + max_version: 12, + }, + ApiVersionRange { + api_key: API_KEY_LIST_OFFSETS, + min_version: 1, + max_version: 6, + }, + ApiVersionRange { + api_key: API_KEY_METADATA, + min_version: 0, + max_version: 9, + }, + ApiVersionRange { + api_key: API_KEY_API_VERSIONS, + min_version: 0, + max_version: 3, + }, + ApiVersionRange { + api_key: API_KEY_CREATE_TOPICS, + min_version: 2, + max_version: 5, + }, +]; + +#[must_use] +pub fn supported_api_ranges() -> &'static [ApiVersionRange] { + SUPPORTED_RANGES +} + +/// Handles one decoded request frame and returns how the connection should proceed. +pub fn handle_request( + api_key: i16, + api_version: i16, + body: Bytes, + broker: &BrokerAdvertise, +) -> HandleOutcome { + if api_key == API_KEY_PRODUCE { + return handle_produce_request(api_version, body); + } + handle_other_request(api_key, api_version, body, broker) +} + +/// Decode `T` from the whole request body and reject unconsumed trailing bytes. +/// +/// `kafka_protocol`'s `Decodable` stops once it has read the fields its schema defines; it does +/// not know (or care) whether the caller handed it an exact-length body, so the trailing-bytes +/// check has to live here. +fn decode_exhaustive<T: Decodable>(version: i16, mut body: Bytes) -> Result<T> { + let value = + T::decode(&mut body, version).map_err(|e| KafkaProtocolError::Malformed(e.to_string()))?; + if body.has_remaining() { + return Err(KafkaProtocolError::Malformed( + "unexpected trailing bytes in request body".to_string(), + )); + } + Ok(value) +} Review Comment: Added a pre-decode shape-validation pass (protocol/bounds_guard.rs) that walks every collection-length field against a 65,536-element cap — both per-array and cumulative across nested arrays — before any typed decode runs, so kafka_protocol's Vec::with_capacity(n) at types.rs:988/:1096 never sees an attacker-controlled count. Reproduced both payloads from this review against the fix: CreateTopics v5 FF FF FF FF 0F now closes cleanly with an 18-byte error response instead of attempting a 481GB allocation; Metadata v0 7F FF FF FF now closes the connection instead of attempting 143GB. All 6 decode sites (Produce/Fetch/ListOffsets/CreateTopics/ApiVersions/Metadata) are guarded — verified no unguarded kafka_protocol decode call remains. Unit tests pin both payloads verbatim (bounds_guard.rs:660,:666) -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
