Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How were these binary files generated? Consider documenting in a new doc/fuzzing.md to describe how we can regenerate these if needed.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added test/data/fuzz_mongocrypt_corpus/op0_with_extra
Binary file not shown.
Binary file added test/data/fuzz_mongocrypt_corpus/op1_decrypt_doc
Binary file not shown.
Binary file not shown.
Binary file added test/data/fuzz_mongocrypt_corpus/op1_with_extra
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added test/data/fuzz_mongocrypt_corpus/op2_with_extra
Binary file not shown.
Binary file not shown.
Binary file added test/data/fuzz_mongocrypt_corpus/op3_with_extra
Binary file not shown.
Binary file added test/data/fuzz_mongocrypt_corpus/op4_datakey
Binary file not shown.
Binary file added test/data/fuzz_mongocrypt_corpus/op4_with_extra
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added test/data/fuzz_mongocrypt_corpus/op5_with_extra
Binary file not shown.
Binary file not shown.
Binary file added test/data/fuzz_mongocrypt_corpus/op6_with_extra
Binary file not shown.
329 changes: 329 additions & 0 deletions test/fuzz_mongocrypt.c
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though it may not be strictly needed for OSS-Fuzz, suggest adding a CMake target to help test locally:

option (ENABLE_FUZZING "Build fuzzing targets. Requires a compiler supporting -fsanitize=fuzzer." OFF)
# ...
if (ENABLE_FUZZING)
   include (CheckCCompilerFlag)
   check_c_compiler_flag ("-fsanitize=fuzzer" COMPILER_SUPPORTS_FUZZER)
   if (NOT COMPILER_SUPPORTS_FUZZER)
      message (FATAL_ERROR
         "ENABLE_FUZZING is ON but the compiler does not support -fsanitize=fuzzer. "
      )
   endif ()

   add_executable (fuzz_mongocrypt EXCLUDE_FROM_ALL test/fuzz_mongocrypt.c)
   target_include_directories (fuzz_mongocrypt PRIVATE ./src)
   target_link_libraries (fuzz_mongocrypt PRIVATE mongocrypt_static _mongocrypt::libbson_for_static)
   target_compile_options (fuzz_mongocrypt PRIVATE -fsanitize=fuzzer)
   target_link_options (fuzz_mongocrypt PRIVATE -fsanitize=fuzzer)
endif ()

Tip: I can run this locally on macOS by using the compilers included in brew install llvm:

cmake \
  -DCMAKE_C_COMPILER="/opt/homebrew/opt/llvm/bin/clang" \
  -DCMAKE_CXX_COMPILER="/opt/homebrew/opt/llvm/bin/clang++" \
  -DENABLE_FUZZING=ON \
  -Bcmake-build

Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
/*
* Copyright 2024-present MongoDB, Inc.
*
* Licensed 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.
*/

/*
* OSS-Fuzz fuzzing entry point for libmongocrypt.
*
* Exercises the libmongocrypt state machine by initializing contexts for
* various operations and feeding fuzzed data through each state transition.
*/

#include "mongocrypt.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

/* MONGOCRYPT_KEY_LEN is 96 bytes (defined in mongocrypt-crypto-private.h). */
#define FUZZ_KEY_LEN 96

/* Maximum number of state transitions to prevent infinite loops. */
#define MAX_STATE_TRANSITIONS 32

/* Operation types selected by the first byte of fuzz input. */
enum {
OP_ENCRYPT = 0,
OP_DECRYPT = 1,
OP_EXPLICIT_ENCRYPT = 2,
OP_EXPLICIT_DECRYPT = 3,
OP_DATAKEY = 4,
OP_REWRAP_MANY_DATAKEY = 5,
OP_EXPLICIT_ENCRYPT_EXPRESSION = 6,
OP_COUNT = 7,
};

/* Helper: consume bytes from the fuzz input. */
static const uint8_t *fuzz_consume(const uint8_t **data, size_t *remaining, size_t n) {
if (*remaining < n) {
return NULL;
}
const uint8_t *ptr = *data;
*data += n;
*remaining -= n;
return ptr;
}

/* Helper: consume one byte. */
static int fuzz_consume_byte(const uint8_t **data, size_t *remaining) {
const uint8_t *p = fuzz_consume(data, remaining, 1);
return p ? *p : -1;
}

/*
* Drive the context state machine, feeding fuzzed data at each state.
* Returns when the context reaches DONE, ERROR, or we run out of input.
*/
static void drive_ctx(mongocrypt_ctx_t *ctx, const uint8_t **data, size_t *remaining) {
mongocrypt_ctx_state_t state;
int transitions = 0;

while (transitions++ < MAX_STATE_TRANSITIONS) {
state = mongocrypt_ctx_state(ctx);

switch (state) {
case MONGOCRYPT_CTX_NEED_MONGO_COLLINFO_WITH_DB:
case MONGOCRYPT_CTX_NEED_MONGO_COLLINFO:
case MONGOCRYPT_CTX_NEED_MONGO_MARKINGS:
case MONGOCRYPT_CTX_NEED_MONGO_KEYS: {
/* Feed fuzzed BSON-like data as a mongo response. */
size_t feed_len = 0;
const uint8_t *len_bytes = fuzz_consume(data, remaining, 2);
if (!len_bytes) {
return;
}
feed_len = (size_t)len_bytes[0] | ((size_t)len_bytes[1] << 8);
if (feed_len > *remaining) {
feed_len = *remaining;
}
if (feed_len > 0) {
const uint8_t *feed_data = fuzz_consume(data, remaining, feed_len);
if (feed_data) {
mongocrypt_binary_t *bin =
mongocrypt_binary_new_from_data((uint8_t *)feed_data, (uint32_t)feed_len);
if (bin) {
mongocrypt_ctx_mongo_feed(ctx, bin);
mongocrypt_binary_destroy(bin);
}
}
}
mongocrypt_ctx_mongo_done(ctx);
break;
}

case MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS: {
/* Feed fuzzed KMS provider credentials. */
size_t feed_len = 0;
const uint8_t *len_bytes = fuzz_consume(data, remaining, 2);
if (!len_bytes) {
return;
}
feed_len = (size_t)len_bytes[0] | ((size_t)len_bytes[1] << 8);
if (feed_len > *remaining) {
feed_len = *remaining;
}
if (feed_len > 0) {
const uint8_t *feed_data = fuzz_consume(data, remaining, feed_len);
if (feed_data) {
mongocrypt_binary_t *bin =
mongocrypt_binary_new_from_data((uint8_t *)feed_data, (uint32_t)feed_len);
if (bin) {
mongocrypt_ctx_provide_kms_providers(ctx, bin);
mongocrypt_binary_destroy(bin);
}
}
} else {
/* Provide empty doc to advance past this state. */
uint8_t empty_bson[] = {5, 0, 0, 0, 0}; /* empty BSON document */
mongocrypt_binary_t *bin = mongocrypt_binary_new_from_data(empty_bson, sizeof(empty_bson));
if (bin) {
mongocrypt_ctx_provide_kms_providers(ctx, bin);
mongocrypt_binary_destroy(bin);
}
}
break;
}

case MONGOCRYPT_CTX_NEED_KMS: {
/* Iterate KMS contexts and feed fuzzed response data. */
mongocrypt_kms_ctx_t *kms_ctx;
while ((kms_ctx = mongocrypt_ctx_next_kms_ctx(ctx)) != NULL) {
uint32_t bytes_needed = mongocrypt_kms_ctx_bytes_needed(kms_ctx);
if (bytes_needed == 0) {
continue;
}
size_t feed_len = bytes_needed;
if (feed_len > *remaining) {
feed_len = *remaining;
}
if (feed_len == 0) {
return;
}
const uint8_t *feed_data = fuzz_consume(data, remaining, feed_len);
if (feed_data) {
mongocrypt_binary_t *bin =
mongocrypt_binary_new_from_data((uint8_t *)feed_data, (uint32_t)feed_len);
if (bin) {
mongocrypt_kms_ctx_feed(kms_ctx, bin);
mongocrypt_binary_destroy(bin);
}
}
}
mongocrypt_ctx_kms_done(ctx);
break;
}

case MONGOCRYPT_CTX_READY: {
mongocrypt_binary_t *out = mongocrypt_binary_new();
if (out) {
mongocrypt_ctx_finalize(ctx, out);
mongocrypt_binary_destroy(out);
}
break;
}

case MONGOCRYPT_CTX_DONE:
case MONGOCRYPT_CTX_ERROR:
default: return;
}
}
}

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
/*
* Need at least 1 byte for op selection + some data to feed.
* Trivially small inputs won't exercise meaningful code paths.
*/
if (size < 5) {
return 0;
}

const uint8_t *cursor = data;
size_t remaining = size;

/* Consume first byte to select operation. */
int op = fuzz_consume_byte(&cursor, &remaining) % OP_COUNT;

/* Set up mongocrypt_t with a local KMS provider. */
mongocrypt_t *crypt = mongocrypt_new();
if (!crypt) {
return 0;
}

/* Configure local KMS provider with a fixed 96-byte key. */
uint8_t local_key[FUZZ_KEY_LEN];
memset(local_key, 0xAB, FUZZ_KEY_LEN);
mongocrypt_binary_t *key_bin = mongocrypt_binary_new_from_data(local_key, FUZZ_KEY_LEN);
if (!key_bin) {
mongocrypt_destroy(crypt);
return 0;
}
mongocrypt_setopt_kms_provider_local(crypt, key_bin);
mongocrypt_binary_destroy(key_bin);

if (!mongocrypt_init(crypt)) {
mongocrypt_destroy(crypt);
return 0;
}

mongocrypt_ctx_t *ctx = mongocrypt_ctx_new(crypt);
if (!ctx) {
mongocrypt_destroy(crypt);
return 0;
}

/* Create a binary view of remaining fuzz data for init calls. */
mongocrypt_binary_t *input_bin = NULL;
if (remaining > 0) {
input_bin = mongocrypt_binary_new_from_data((uint8_t *)cursor, (uint32_t)remaining);
}

bool init_ok = false;

switch (op) {
case OP_ENCRYPT:
if (input_bin) {
init_ok = mongocrypt_ctx_encrypt_init(ctx, "test", -1, input_bin);
}
break;

case OP_DECRYPT:
if (input_bin) {
init_ok = mongocrypt_ctx_decrypt_init(ctx, input_bin);
}
break;

case OP_EXPLICIT_ENCRYPT: {
/* Set a fixed key id (16-byte UUID). */
uint8_t key_id[16];
memset(key_id, 0x61, sizeof(key_id)); /* matches "YWFhYWFhYWFhYWFhYWFhYQ==" */
mongocrypt_binary_t *kid_bin = mongocrypt_binary_new_from_data(key_id, sizeof(key_id));
if (kid_bin) {
mongocrypt_ctx_setopt_key_id(ctx, kid_bin);
mongocrypt_binary_destroy(kid_bin);
}
mongocrypt_ctx_setopt_algorithm(ctx, "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", -1);
if (input_bin) {
init_ok = mongocrypt_ctx_explicit_encrypt_init(ctx, input_bin);
}
break;
}

case OP_EXPLICIT_DECRYPT:
if (input_bin) {
init_ok = mongocrypt_ctx_explicit_decrypt_init(ctx, input_bin);
}
break;

case OP_DATAKEY: {
/* Set up key encryption key for local provider. */
uint8_t kek_bson[] = {
/* {"provider": "local"} as BSON */
0x1b, 0x00, 0x00, 0x00, /* doc len = 27 */
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
0x1b, 0x00, 0x00, 0x00, /* doc len = 27 */
0x19, 0x00, 0x00, 0x00, /* doc len = 27 */

Careful. Length appears incorrect. Contents can be checked with PyMongo using bson.decode:

bson.decode(bytearray([
    0x19, 0x00, 0x00, 0x00,
    0x02,
    ord('p'), ord('r'), ord('o'), ord('v'), ord('i'), ord('d'), ord('e'), ord('r'), 0x00,
    0x06, 0x00, 0x00, 0x00,
    ord('l'), ord('o'), ord('c'), ord('a'), ord('l'), 0x00,
    0x00
]))
# {'provider': 'local'}

0x02, /* type: string */
'p', 'r', 'o', 'v', 'i', 'd', 'e', 'r', 0x00, /* key */
0x06, 0x00, 0x00, 0x00, /* string len = 6 */
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest running file through clang-format to align comments.

'l', 'o', 'c', 'a', 'l', 0x00, /* value */
0x00 /* doc terminator */
};
mongocrypt_binary_t *kek_bin = mongocrypt_binary_new_from_data(kek_bson, sizeof(kek_bson));
if (kek_bin) {
mongocrypt_ctx_setopt_key_encryption_key(ctx, kek_bin);
mongocrypt_binary_destroy(kek_bin);
}
init_ok = mongocrypt_ctx_datakey_init(ctx);
break;
}

case OP_REWRAP_MANY_DATAKEY:
if (input_bin) {
init_ok = mongocrypt_ctx_rewrap_many_datakey_init(ctx, input_bin);
}
break;

case OP_EXPLICIT_ENCRYPT_EXPRESSION: {
uint8_t key_id[16];
memset(key_id, 0x61, sizeof(key_id));
mongocrypt_binary_t *kid_bin = mongocrypt_binary_new_from_data(key_id, sizeof(key_id));
if (kid_bin) {
mongocrypt_ctx_setopt_key_id(ctx, kid_bin);
mongocrypt_binary_destroy(kid_bin);
}
mongocrypt_ctx_setopt_algorithm(ctx, "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", -1);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set algorithm and queryType to "range".

I expect the call to mongocrypt_ctx_explicit_encrypt_expression_init always fails. Encrypt expression requires algorithm and queryType both be set to range.

libmongocrypt checks the queryType for encryptExpression is "range" (1) and later that queryType matches algorithm (2).

if (input_bin) {
init_ok = mongocrypt_ctx_explicit_encrypt_expression_init(ctx, input_bin);
}
break;
}

default: break;
}

if (input_bin) {
mongocrypt_binary_destroy(input_bin);
}

/* If initialization succeeded, drive the state machine with fuzzed data. */
if (init_ok) {
drive_ctx(ctx, &cursor, &remaining);
}

mongocrypt_ctx_destroy(ctx);
mongocrypt_destroy(crypt);

return 0;
}