-
Notifications
You must be signed in to change notification settings - Fork 100
MONGOCRYPT-834 fuzzing for oss-fuzz #1152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 */ | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Careful. Length appears incorrect. Contents can be checked with PyMongo using 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 */ | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Set algorithm and queryType to "range". I expect the call to 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; | ||||||
| } | ||||||
|
|
||||||
There was a problem hiding this comment.
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.mdto describe how we can regenerate these if needed.