diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 352e674f9..69cb6f297 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -66,6 +66,13 @@ buildvariants: tasks: - name: .lint + - name: fuzzer + display_name: "Fuzzer" + run_on: + - ubuntu2204-large + tasks: + "run-fuzzer" + - name: rhel-8 display_name: "RHEL 8" run_on: @@ -777,6 +784,10 @@ tasks: commands: - func: "check rustdoc" + - name: "run-fuzzer" + commands: + - func: "run fuzzer" + # Driver test suite runs for the full set of versions and topologies are in # suite-tasks.yml, generated by .evergreen/generate_tasks. @@ -1264,6 +1275,17 @@ functions: - AWS_SECRET_ACCESS_KEY - AWS_SESSION_TOKEN + "run fuzzer": + - command: shell.exec + type: test + params: + shell: bash + working_dir: src + script: | + ${PREPARE_SHELL} + .evergreen/install-fuzzer.sh + .evergreen/run-fuzzer.sh + "run aws auth test with regular aws credentials": - command: subprocess.exec type: test diff --git a/.evergreen/install-fuzzer.sh b/.evergreen/install-fuzzer.sh new file mode 100755 index 000000000..a9b6df30a --- /dev/null +++ b/.evergreen/install-fuzzer.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -o errexit + +source ./.evergreen/env.sh + +cargo install cargo-fuzz diff --git a/.evergreen/run-fuzzer.sh b/.evergreen/run-fuzzer.sh new file mode 100755 index 000000000..2062811d3 --- /dev/null +++ b/.evergreen/run-fuzzer.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -o errexit + +source ./.evergreen/env.sh + +mkdir -p artifacts + +# Function to run fuzzer and collect crashes +run_fuzzer() { + target=$1 + echo "Running fuzzer for $target" + # Run fuzzer and redirect crashes to artifacts directory + RUST_BACKTRACE=1 cargo +nightly fuzz run $target -- \ + -rss_limit_mb=4096 \ + -max_total_time=360 \ + -artifact_prefix=artifacts/ \ + -print_final_stats=1 +} + +run_fuzzer header_length diff --git a/Cargo.toml b/Cargo.toml index 3ca3de6fd..5e412e2c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,11 +67,14 @@ in-use-encryption-unstable = ["in-use-encryption"] # The tracing API is unstable and may have backwards-incompatible changes in minor version updates. # TODO: pending https://github.com/tokio-rs/tracing/issues/2036 stop depending directly on log. tracing-unstable = ["dep:tracing", "dep:log"] +fuzzing = ["dep:arbitrary", "arbitrary/derive", "dep:byteorder"] [dependencies] async-trait = "0.1.42" base64 = "0.13.0" -bitflags = "1.1.0" +bitflags = { version = "1.1.0" } +arbitrary = { version = "1.3", optional = true } +byteorder = { version = "1.4", optional = true } bson = { git = "https://github.com/mongodb/bson-rust", branch = "main", version = "2.11.0" } chrono = { version = "0.4.7", default-features = false, features = [ "clock", diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 000000000..1a45eee77 --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 000000000..73cdd9b09 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "mongodb-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +mongodb = { path = "..", features = ["fuzzing"] } + +[[bin]] +name = "header_length" +path = "fuzz_targets/header_length.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/header_length.rs b/fuzz/fuzz_targets/header_length.rs new file mode 100644 index 000000000..f77c6cb92 --- /dev/null +++ b/fuzz/fuzz_targets/header_length.rs @@ -0,0 +1,28 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; +use mongodb::cmap::conn::wire::{ + header::{Header, OpCode}, + message::Message, +}; + +fuzz_target!(|data: &[u8]| { + if data.len() < Header::LENGTH { + return; + } + if let Ok(mut header) = Header::from_slice(data) { + // read_from_slice will adjust the data for the header length, this first check will + // almost always have length mismatches, but length mismatches are a possible attack + // vector. + // This will also often have the wrong opcode, but that is also a possible attack vector. + if let Ok(message) = Message::read_from_slice(data, header.clone()) { + let _ = message; + } + // try again with the header.length set to the data length and the header.opcode == + // OpCode::Message to catch other attack vectors. + header.length = data.len() as i32 - Header::LENGTH as i32; + header.op_code = OpCode::Message; + if let Ok(message) = Message::read_from_slice(data, header) { + let _ = message; + } + } +}); diff --git a/src/bson_util.rs b/src/bson_util.rs index 4a5aeae9e..d15d2b670 100644 --- a/src/bson_util.rs +++ b/src/bson_util.rs @@ -158,12 +158,14 @@ fn num_decimal_digits(mut n: usize) -> usize { /// Read a document's raw BSON bytes from the provided reader. pub(crate) fn read_document_bytes(mut reader: R) -> Result> { - let length = reader.read_i32_sync()?; + let length = Checked::new(reader.read_i32_sync()?); - let mut bytes = Vec::with_capacity(length as usize); - bytes.write_all(&length.to_le_bytes())?; + let mut bytes = Vec::with_capacity(length.try_into()?); + bytes.write_all(&length.try_into::()?.to_le_bytes())?; - reader.take(length as u64 - 4).read_to_end(&mut bytes)?; + reader + .take((length - 4).try_into()?) + .read_to_end(&mut bytes)?; Ok(bytes) } diff --git a/src/client/options/parse.rs b/src/client/options/parse.rs index 8aed36ae4..aba7b21e4 100644 --- a/src/client/options/parse.rs +++ b/src/client/options/parse.rs @@ -80,6 +80,7 @@ impl Action for ParseConnectionString { "srvMaxHosts and loadBalanced=true cannot both be present", )); } + // max is u32, so this is safe. config.hosts = crate::sdam::choose_n(&config.hosts, max as usize) .cloned() .collect(); diff --git a/src/cmap.rs b/src/cmap.rs index b0a02f8ed..8c108ebe1 100644 --- a/src/cmap.rs +++ b/src/cmap.rs @@ -1,7 +1,13 @@ #[cfg(test)] pub(crate) mod test; +#[cfg(feature = "fuzzing")] +#[allow(missing_docs)] +pub mod conn; + +#[cfg(not(feature = "fuzzing"))] pub(crate) mod conn; + mod connection_requester; pub(crate) mod establish; mod manager; diff --git a/src/cmap/conn.rs b/src/cmap/conn.rs index f1ddc5910..fb5f76b42 100644 --- a/src/cmap/conn.rs +++ b/src/cmap/conn.rs @@ -1,6 +1,12 @@ mod command; pub(crate) mod pooled; mod stream_description; + +#[cfg(feature = "fuzzing")] +#[allow(missing_docs)] +pub mod wire; + +#[cfg(not(feature = "fuzzing"))] pub(crate) mod wire; use std::{sync::Arc, time::Instant}; diff --git a/src/cmap/conn/wire.rs b/src/cmap/conn/wire.rs index 76ef2cfab..24d4cc643 100644 --- a/src/cmap/conn/wire.rs +++ b/src/cmap/conn/wire.rs @@ -1,8 +1,26 @@ +#[cfg(feature = "fuzzing")] +#[allow(missing_docs)] +pub mod header; +#[cfg(not(feature = "fuzzing"))] mod header; +#[cfg(feature = "fuzzing")] +#[allow(missing_docs)] +pub mod message; +#[cfg(not(feature = "fuzzing"))] pub(crate) mod message; +#[cfg(feature = "fuzzing")] +#[allow(missing_docs)] +pub mod util; +#[cfg(not(feature = "fuzzing"))] mod util; -pub(crate) use self::{ - message::{Message, MessageFlags}, - util::next_request_id, -}; +pub(crate) use self::util::next_request_id; + +#[cfg(feature = "fuzzing")] +pub use self::message::Message; + +#[cfg(feature = "fuzzing")] +pub use crate::fuzz::message_flags::MessageFlags; + +#[cfg(not(feature = "fuzzing"))] +pub(crate) use message::{Message, MessageFlags}; diff --git a/src/cmap/conn/wire/header.rs b/src/cmap/conn/wire/header.rs index 791395f4c..721d3bdaf 100644 --- a/src/cmap/conn/wire/header.rs +++ b/src/cmap/conn/wire/header.rs @@ -1,9 +1,27 @@ +use crate::error::{ErrorKind, Result}; use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use crate::error::{ErrorKind, Result}; +#[cfg(feature = "fuzzing")] +use arbitrary::Arbitrary; +#[cfg(feature = "fuzzing")] +use byteorder::{LittleEndian, ReadBytesExt}; +#[cfg(feature = "fuzzing")] +use std::io::Cursor; + +/// The wire protocol op codes. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[cfg(feature = "fuzzing")] +#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] +pub enum OpCode { + Reply = 1, + Query = 2004, + Message = 2013, + Compressed = 2012, +} /// The wire protocol op codes. #[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[cfg(not(feature = "fuzzing"))] pub(crate) enum OpCode { Reply = 1, Query = 2004, @@ -13,7 +31,7 @@ pub(crate) enum OpCode { impl OpCode { /// Attempt to infer the op code based on the numeric value. - fn from_i32(i: i32) -> Result { + pub fn from_i32(i: i32) -> Result { match i { 1 => Ok(OpCode::Reply), 2004 => Ok(OpCode::Query), @@ -28,7 +46,19 @@ impl OpCode { } /// The header for any wire protocol message. -#[derive(Debug)] +#[derive(Debug, Clone)] +#[cfg(feature = "fuzzing")] +#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] +pub struct Header { + pub length: i32, + pub request_id: i32, + pub response_to: i32, + pub op_code: OpCode, +} + +/// The header for any wire protocol message. +#[derive(Debug, Clone)] +#[cfg(not(feature = "fuzzing"))] pub(crate) struct Header { pub length: i32, pub request_id: i32, @@ -37,9 +67,61 @@ pub(crate) struct Header { } impl Header { + #[cfg(feature = "fuzzing")] + pub const LENGTH: usize = 4 * std::mem::size_of::(); + + #[cfg(not(feature = "fuzzing"))] pub(crate) const LENGTH: usize = 4 * std::mem::size_of::(); - /// Serializes the Header and writes the bytes to `w`. + // generates a Header from a randomly generated slice of bytes, as long as the slice is at least + // 16 bytes long this is used for fuzzing + #[cfg(feature = "fuzzing")] + pub fn from_slice(data: &[u8]) -> Result { + if data.len() < Self::LENGTH { + return Err(ErrorKind::InvalidResponse { + message: format!( + "Header requires {} bytes but only got {}", + Self::LENGTH, + data.len() + ), + } + .into()); + } + let mut cursor = Cursor::new(data); + + let length = ReadBytesExt::read_i32::(&mut cursor).map_err(|e| { + ErrorKind::InvalidResponse { + message: format!("Failed to read length: {}", e), + } + })?; + + let request_id = ReadBytesExt::read_i32::(&mut cursor).map_err(|e| { + ErrorKind::InvalidResponse { + message: format!("Failed to read request_id: {}", e), + } + })?; + + let response_to = ReadBytesExt::read_i32::(&mut cursor).map_err(|e| { + ErrorKind::InvalidResponse { + message: format!("Failed to read response_to: {}", e), + } + })?; + + let op_code = + OpCode::from_i32(ReadBytesExt::read_i32::(&mut cursor).map_err( + |e| ErrorKind::InvalidResponse { + message: format!("Failed to read op_code: {}", e), + }, + )?)?; + + Ok(Self { + length, + request_id, + response_to, + op_code, + }) + } + pub(crate) async fn write_to(&self, stream: &mut W) -> Result<()> { stream.write_all(&self.length.to_le_bytes()).await?; stream.write_all(&self.request_id.to_le_bytes()).await?; @@ -51,7 +133,6 @@ impl Header { Ok(()) } - /// Reads bytes from `r` and deserializes them into a header. pub(crate) async fn read_from( reader: &mut R, ) -> Result { diff --git a/src/cmap/conn/wire/message.rs b/src/cmap/conn/wire/message.rs index c746c8b95..d6f125449 100644 --- a/src/cmap/conn/wire/message.rs +++ b/src/cmap/conn/wire/message.rs @@ -1,7 +1,6 @@ use std::io::Read; -use bitflags::bitflags; -use bson::{doc, Array, Document}; +use bson::{doc, Array, Document, RawDocumentBuf}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; #[cfg(any( @@ -10,39 +9,118 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; feature = "snappy-compression" ))] use crate::options::Compressor; + +#[cfg(feature = "fuzzing")] +use arbitrary::Arbitrary; + +#[cfg(feature = "fuzzing")] +use crate::fuzz::{ + message_flags::MessageFlags, + raw_document::{FuzzDocumentSequenceImpl, FuzzRawDocumentImpl}, +}; + +#[cfg(not(feature = "fuzzing"))] +bitflags::bitflags! { + pub(crate) struct MessageFlags: u32 { + const CHECKSUM_PRESENT = 0b_0000_0000_0000_0000_0000_0000_0000_0001; + const MORE_TO_COME = 0b_0000_0000_0000_0000_0000_0000_0000_0010; + const EXHAUST_ALLOWED = 0b_0000_0000_0000_0001_0000_0000_0000_0000; + } +} + use crate::{ - bson::RawDocumentBuf, bson_util, checked::Checked, - cmap::{conn::wire::util::SyncCountReader, Command}, + cmap::conn::{ + wire::{ + header::{Header, OpCode}, + next_request_id, + util::SyncCountReader, + }, + Command, + }, compression::decompress::decompress_message, error::{Error, ErrorKind, Result}, - runtime::SyncLittleEndianRead, }; -use super::{ - header::{Header, OpCode}, - next_request_id, -}; +#[cfg(feature = "fuzzing")] +fn generate_raw_document(u: &mut arbitrary::Unstructured) -> arbitrary::Result { + let doc = FuzzRawDocumentImpl::arbitrary(u)?; + Ok(doc.into()) +} + +#[cfg(feature = "fuzzing")] +fn generate_document_sequences( + u: &mut arbitrary::Unstructured, +) -> arbitrary::Result> { + let seq = FuzzDocumentSequenceImpl::arbitrary(u)?; + Ok(vec![DocumentSequence { + identifier: String::arbitrary(u)?, + documents: seq.0.into_iter().map(Into::into).collect(), + }]) +} + +const DEFAULT_MAX_MESSAGE_SIZE_BYTES: i32 = 48 * 1024 * 1024; /// Represents an OP_MSG wire protocol operation. -#[derive(Debug)] -pub(crate) struct Message { - /// OP_MSG payload type 0. +#[derive(Debug, Clone)] +#[cfg(feature = "fuzzing")] +#[cfg_attr(feature = "fuzzing", derive(Arbitrary))] +pub struct Message { + /// OP_MSG flags + pub flags: MessageFlags, + + /// OP_MSG payload type 0 (the main document) + #[cfg_attr(feature = "fuzzing", arbitrary(with = generate_raw_document))] pub(crate) document_payload: RawDocumentBuf, - /// OP_MSG payload type 1. + /// OP_MSG payload type 1 (document sequences) + #[cfg_attr(feature = "fuzzing", arbitrary(with = generate_document_sequences))] pub(crate) document_sequences: Vec, - pub(crate) response_to: i32, + /// Optional CRC32C checksum + pub(crate) checksum: Option, + + /// Request ID for the message + pub(crate) request_id: Option, - pub(crate) flags: MessageFlags, + /// Response to request ID + pub response_to: i32, + /// Whether the message should be compressed + #[cfg(any( + feature = "zstd-compression", + feature = "zlib-compression", + feature = "snappy-compression" + ))] + pub(crate) should_compress: bool, +} + +/// Represents an OP_MSG wire protocol operation. +#[derive(Debug, Clone)] +#[cfg(not(feature = "fuzzing"))] +pub(crate) struct Message { + /// OP_MSG flags + pub flags: MessageFlags, + + /// OP_MSG payload type 0 (the main document) + #[cfg_attr(feature = "fuzzing", arbitrary(with = generate_raw_document))] + pub(crate) document_payload: RawDocumentBuf, + + /// OP_MSG payload type 1 (document sequences) + #[cfg_attr(feature = "fuzzing", arbitrary(with = generate_document_sequences))] + pub(crate) document_sequences: Vec, + + /// Optional CRC32C checksum pub(crate) checksum: Option, + /// Request ID for the message pub(crate) request_id: Option, - /// Whether the message should be compressed by the driver. + /// Response to request ID + pub response_to: i32, + + /// Whether the message should be compressed #[cfg(any( feature = "zstd-compression", feature = "zlib-compression", @@ -52,6 +130,14 @@ pub(crate) struct Message { } #[derive(Clone, Debug)] +#[cfg(feature = "fuzzing")] +pub struct DocumentSequence { + pub identifier: String, + pub documents: Vec, +} + +#[derive(Clone, Debug)] +#[cfg(not(feature = "fuzzing"))] pub(crate) struct DocumentSequence { pub(crate) identifier: String, pub(crate) documents: Vec, @@ -77,7 +163,11 @@ impl TryFrom for Message { Ok(Self { document_payload, - document_sequences: command.document_sequences, + document_sequences: command + .document_sequences + .into_iter() + .map(Into::into) + .collect(), response_to: 0, flags, checksum: None, @@ -92,7 +182,44 @@ impl TryFrom for Message { } } +#[cfg(feature = "fuzzing")] +impl From for Vec { + fn from(seq: FuzzDocumentSequenceImpl) -> Self { + vec![DocumentSequence { + identifier: "documents".to_string(), + documents: seq.0.into_iter().map(Into::into).collect(), + }] + } +} + impl Message { + #[cfg(feature = "fuzzing")] + pub fn read_from_slice(data: &[u8], header: Header) -> Result { + // this case should not actually be hit during fuzzing since we create the Header + // from the data first, but this is a good sanity check, ensuring that we will + // not panic if we do hit this case. + if data.len() < Header::LENGTH { + return Err(ErrorKind::InvalidResponse { + message: format!("Message data too short: {} bytes", data.len()), + } + .into()); + } + let data = &data[Header::LENGTH..]; + if header.op_code == OpCode::Message { + return Self::read_op_common(data, data.len(), &header); + } + Err(Error::new( + ErrorKind::InvalidResponse { + message: format!( + "Invalid op code for fuzzing, expected {} but got {}", + OpCode::Message as u32, + header.op_code as u32 + ), + }, + Option::>::None, + )) + } + /// Gets this message's command as a Document. If serialization fails, returns a document /// containing the error. pub(crate) fn get_command_document(&self) -> Document { @@ -166,6 +293,8 @@ impl Message { mut reader: T, header: &Header, ) -> Result { + use crate::runtime::SyncLittleEndianRead; + let length = Checked::::try_from(header.length)?; let length_remaining = length - Header::LENGTH; let mut buffer = vec![0u8; length_remaining.get()?]; @@ -208,6 +337,7 @@ impl Message { } fn read_op_common(mut reader: &[u8], length_remaining: usize, header: &Header) -> Result { + use crate::runtime::SyncLittleEndianRead; let mut length_remaining = Checked::new(length_remaining); let flags = MessageFlags::from_bits_truncate(reader.read_u32_sync()?); length_remaining -= std::mem::size_of::(); @@ -383,17 +513,6 @@ impl Message { } } -const DEFAULT_MAX_MESSAGE_SIZE_BYTES: i32 = 48 * 1024 * 1024; - -bitflags! { - /// Represents the bitwise flags for an OP_MSG as defined in the spec. - pub(crate) struct MessageFlags: u32 { - const CHECKSUM_PRESENT = 0b_0000_0000_0000_0000_0000_0000_0000_0001; - const MORE_TO_COME = 0b_0000_0000_0000_0000_0000_0000_0000_0010; - const EXHAUST_ALLOWED = 0b_0000_0000_0000_0001_0000_0000_0000_0000; - } -} - /// Represents a section as defined by the OP_MSG spec. #[derive(Debug)] enum MessageSection { @@ -403,7 +522,8 @@ enum MessageSection { impl MessageSection { /// Reads bytes from `reader` and deserializes them into a MessageSection. - fn read(reader: &mut R) -> Result { + fn read(reader: &mut SyncCountReader) -> Result { + use crate::runtime::SyncLittleEndianRead; let payload_type = reader.read_u8_sync()?; if payload_type == 0 { diff --git a/src/fuzz/message_flags.rs b/src/fuzz/message_flags.rs new file mode 100644 index 000000000..62f0e85da --- /dev/null +++ b/src/fuzz/message_flags.rs @@ -0,0 +1,16 @@ +use arbitrary::Arbitrary; + +bitflags::bitflags! { + pub struct MessageFlags: u32 { + const CHECKSUM_PRESENT = 0b_0000_0000_0000_0000_0000_0000_0000_0001; + const MORE_TO_COME = 0b_0000_0000_0000_0000_0000_0000_0000_0010; + const EXHAUST_ALLOWED = 0b_0000_0000_0000_0001_0000_0000_0000_0000; + } +} + +impl<'a> Arbitrary<'a> for MessageFlags { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let bits = u.arbitrary::()?; + Ok(MessageFlags::from_bits(bits).unwrap_or(MessageFlags::empty())) + } +} diff --git a/src/fuzz/mod.rs b/src/fuzz/mod.rs new file mode 100644 index 000000000..d74bd32dd --- /dev/null +++ b/src/fuzz/mod.rs @@ -0,0 +1,11 @@ +//! Fuzzing support module for MongoDB wire protocol messages + +#[cfg(feature = "fuzzing")] +pub mod message_flags; +#[cfg(feature = "fuzzing")] +pub mod raw_document; + +#[cfg(feature = "fuzzing")] +pub use message_flags::MessageFlags; +#[cfg(feature = "fuzzing")] +pub use raw_document::{FuzzDocumentSequenceImpl, FuzzRawDocumentImpl}; diff --git a/src/fuzz/raw_document.rs b/src/fuzz/raw_document.rs new file mode 100644 index 000000000..ee5f3e148 --- /dev/null +++ b/src/fuzz/raw_document.rs @@ -0,0 +1,23 @@ +use arbitrary::Arbitrary; +use bson::RawDocumentBuf; + +#[derive(Debug, Arbitrary)] +pub struct FuzzRawDocumentImpl { + bytes: Vec, +} + +impl From for RawDocumentBuf { + fn from(doc: FuzzRawDocumentImpl) -> Self { + RawDocumentBuf::from_bytes(doc.bytes) + .unwrap_or_else(|_| RawDocumentBuf::from_bytes(vec![5, 0, 0, 0, 0]).unwrap()) + } +} + +#[derive(Debug, Arbitrary)] +pub struct FuzzDocumentSequenceImpl(pub Vec); + +impl From for Vec { + fn from(seq: FuzzDocumentSequenceImpl) -> Self { + seq.0.into_iter().map(Into::into).collect() + } +} diff --git a/src/gridfs/download.rs b/src/gridfs/download.rs index 7569538b2..50251bedd 100644 --- a/src/gridfs/download.rs +++ b/src/gridfs/download.rs @@ -201,6 +201,7 @@ async fn get_bytes( let expected_len = FilesCollectionDocument::expected_chunk_length_from_vals(file_len, chunk_size_bytes, n); + // expected_len is u32, so we can safely cast it to usize if chunk_bytes.len() != (expected_len as usize) { return Err(ErrorKind::GridFs(GridFsErrorKind::WrongSizeChunk { actual_size: chunk_bytes.len(), diff --git a/src/lib.rs b/src/lib.rs index 62f7d1a39..6ff6fd675 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,10 @@ mod bson_util; pub mod change_stream; pub(crate) mod checked; mod client; +#[cfg(feature = "fuzzing")] +#[allow(missing_docs)] +pub mod cmap; +#[cfg(not(feature = "fuzzing"))] mod cmap; mod coll; mod collation; @@ -35,6 +39,9 @@ mod cursor; mod db; pub mod error; pub mod event; +#[cfg(feature = "fuzzing")] +#[allow(missing_docs)] +pub mod fuzz; pub mod gridfs; mod hello; pub(crate) mod id_set; diff --git a/src/sdam/description/topology.rs b/src/sdam/description/topology.rs index 23ea188ef..44b4334f5 100644 --- a/src/sdam/description/topology.rs +++ b/src/sdam/description/topology.rs @@ -391,6 +391,7 @@ impl TopologyDescription { } } if let Some(max) = self.srv_max_hosts { + // max is u32, so this is safe. let max = max as usize; if max > 0 && max < self.servers.len() + new.len() { new = choose_n(&new, max.saturating_sub(self.servers.len()))