diff --git a/fuzzing/broker/Makefile b/fuzzing/broker/Makefile index d46c2b124f..21a7e28765 100644 --- a/fuzzing/broker/Makefile +++ b/fuzzing/broker/Makefile @@ -11,7 +11,14 @@ FUZZERS:= \ broker_fuzz_psk_file \ broker_fuzz_queue_msg \ broker_fuzz_read_handle \ - broker_fuzz_test_config + broker_fuzz_test_config \ + broker_fuzz_property_read_all \ + broker_fuzz_subscribe \ + broker_fuzz_proxy_v1_decode \ + broker_fuzz_ws_prepare_packet \ + broker_fuzz_bridge_remap_topic_in \ + broker_fuzz_will_set \ + broker_fuzz_properties_to_json LOCAL_CPPFLAGS+=-I${R}/include/ -I${R}/src -I${R}/lib -I${R} -I${R}/common -I${R}/deps \ -DWITH_BRIDGE -DWITH_BROKER -DWITH_CONTROL -DWITH_EPOLL \ @@ -81,5 +88,49 @@ broker_fuzz_test_config : broker_fuzz_test_config.cpp ${R}/src/mosquitto_broker. cp ${R}/fuzzing/corpora/broker_fuzz_test_config_seed_corpus.zip ${OUT}/$@_seed_corpus.zip cp ${R}/fuzzing/corpora/broker_conf.dict ${OUT}/$@.dict +# Top 10 selected harnesses for upstream submission + +# MQTT v5 property parsing - tests complex property validation logic +broker_fuzz_property_read_all : broker_fuzz_property_read_all.cpp ${R}/src/mosquitto_broker.a + $(CXX) $(LOCAL_CXXFLAGS) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $< $(BROKER_A) $(LOCAL_LIBADD) + install $@ ${OUT}/$@ + cp ${R}/fuzzing/corpora/$@_seed_corpus.zip ${OUT}/$@_seed_corpus.zip + +# MQTT SUBSCRIBE packet handler - tests subscription tree management +broker_fuzz_subscribe : broker_fuzz_subscribe.cpp ${R}/src/mosquitto_broker.a + $(CXX) $(LOCAL_CXXFLAGS) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $< $(BROKER_A) $(LOCAL_LIBADD) + install $@ ${OUT}/$@ + cp ${R}/fuzzing/corpora/$@_seed_corpus.zip ${OUT}/$@_seed_corpus.zip + +# PROXY protocol v1 decoder - tests network protocol parsing +broker_fuzz_proxy_v1_decode : broker_fuzz_proxy_v1_decode.cpp ${R}/src/mosquitto_broker.a + $(CXX) $(LOCAL_CXXFLAGS) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $< $(BROKER_A) $(LOCAL_LIBADD) + install $@ ${OUT}/$@ + cp ${R}/fuzzing/corpora/$@_seed_corpus.zip ${OUT}/$@_seed_corpus.zip + +# WebSocket packet preparation - tests WebSocket frame building +broker_fuzz_ws_prepare_packet : broker_fuzz_ws_prepare_packet.cpp ${R}/src/mosquitto_broker.a + $(CXX) $(LOCAL_CXXFLAGS) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $< $(BROKER_A) $(LOCAL_LIBADD) + install $@ ${OUT}/$@ + cp ${R}/fuzzing/corpora/$@_seed_corpus.zip ${OUT}/$@_seed_corpus.zip + +# Bridge topic remapping - tests bridge configuration and topic rewriting +broker_fuzz_bridge_remap_topic_in : broker_fuzz_bridge_remap_topic_in.cpp ${R}/src/mosquitto_broker.a + $(CXX) $(LOCAL_CXXFLAGS) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $< $(BROKER_A) $(LOCAL_LIBADD) + install $@ ${OUT}/$@ + cp ${R}/fuzzing/corpora/$@_seed_corpus.zip ${OUT}/$@_seed_corpus.zip + +# MQTT Will message configuration - tests QoS feature and payload validation +broker_fuzz_will_set : broker_fuzz_will_set.cpp ${R}/src/mosquitto_broker.a + $(CXX) $(LOCAL_CXXFLAGS) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $< $(BROKER_A) $(LOCAL_LIBADD) + install $@ ${OUT}/$@ + cp ${R}/fuzzing/corpora/$@_seed_corpus.zip ${OUT}/$@_seed_corpus.zip + +# Property to JSON conversion - tests property serialization +broker_fuzz_properties_to_json : broker_fuzz_properties_to_json.cpp ${R}/src/mosquitto_broker.a + $(CXX) $(LOCAL_CXXFLAGS) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $< $(BROKER_A) $(LOCAL_LIBADD) + install $@ ${OUT}/$@ + cp ${R}/fuzzing/corpora/$@_seed_corpus.zip ${OUT}/$@_seed_corpus.zip + clean: rm -f *.o $(FUZZERS) $(PACKET_FUZZERS) diff --git a/fuzzing/broker/broker_fuzz_bridge_remap_topic_in.cpp b/fuzzing/broker/broker_fuzz_bridge_remap_topic_in.cpp new file mode 100644 index 0000000000..f92a46ecbc --- /dev/null +++ b/fuzzing/broker/broker_fuzz_bridge_remap_topic_in.cpp @@ -0,0 +1,231 @@ +#include +#include +#include +#include + +// Include project headers (absolute paths from the workspace). +// Wrap C headers in extern "C" so C++ compilation uses correct C linkage. +extern "C" { +#include "/src/mosquitto/src/mosquitto_broker_internal.h" +#include "/src/mosquitto/lib/mosquitto_internal.h" +#include "/src/mosquitto/include/mosquitto/libcommon_memory.h" +#include "/src/mosquitto/include/mosquitto/libcommon_topic.h" +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) +{ + if(!Data || Size == 0) return 0; + + // Simple deterministic parser of the input buffer into several NUL-terminated strings + // and an enum value for direction. We consume bytes from Data sequentially. + size_t idx = 0; + auto remaining = [&](void)->size_t { return (idx < Size) ? (Size - idx) : 0; }; + + auto take_len = [&](size_t maxlen)->size_t { + if(remaining() == 0) return 0; + // Use next byte to determine a requested length (bounded by available data and maxlen). + unsigned char b = Data[idx++]; + size_t len = (size_t)(b) % (maxlen + 1); + if(len > remaining()) len = remaining(); + return len; + }; + + auto take_cstr = [&](size_t maxlen)->char* { + size_t len = take_len(maxlen); + // If no data, return an empty allocated string (function expects valid C-string pointers). + if(len == 0){ + char *s = (char*)malloc(1); + if(s) s[0] = '\0'; + return s; + } + char *s = (char*)malloc(len + 1); + if(!s) return nullptr; + memcpy(s, Data + idx, len); + s[len] = '\0'; + idx += len; + return s; + }; + + // Partition input into: initial topic, remote_topic (subscription), remote_prefix, local_prefix, and a direction byte. + // maxlen chosen to be reasonable to avoid overly large allocations. + const size_t kMaxPiece = 4096; + + char *initial_topic = take_cstr(kMaxPiece); + char *remote_topic = take_cstr(kMaxPiece); + char *remote_prefix = take_cstr(kMaxPiece); + char *local_prefix = take_cstr(kMaxPiece); + + // Direction byte: if available, use it; otherwise default to bd_in. + enum mosquitto__bridge_direction dir = bd_in; + if(remaining() > 0){ + unsigned char b = Data[idx++]; + dir = (b % 3 == 0) ? bd_out : ((b % 3 == 1) ? bd_in : bd_both); + } + + // Ensure we have at least an empty topic string + if(!initial_topic){ + initial_topic = (char*)malloc(1); + if(initial_topic) initial_topic[0] = '\0'; + } + if(!remote_topic){ + remote_topic = (char*)malloc(1); + if(remote_topic) remote_topic[0] = '\0'; + } + // Note: remote_prefix/local_prefix may be NULL (meaning not used). We keep allocated empty strings to + // allow behavior where prefixes exist but are empty. + if(!remote_prefix){ + remote_prefix = nullptr; // treat as no prefix + } + if(!local_prefix){ + local_prefix = nullptr; + } + + // Build minimal mosquitto context and bridge topic list to exercise bridge__remap_topic_in. + struct mosquitto *context = (struct mosquitto*)calloc(1, sizeof(struct mosquitto)); + if(!context){ + free(initial_topic); + free(remote_topic); + if(remote_prefix) free(remote_prefix); + if(local_prefix) free(local_prefix); + return 0; + } + + // Allocate bridge + struct mosquitto__bridge *bridge = (struct mosquitto__bridge*)calloc(1, sizeof(struct mosquitto__bridge)); + if(!bridge){ + free(initial_topic); + free(remote_topic); + if(remote_prefix) free(remote_prefix); + if(local_prefix) free(local_prefix); + free(context); + return 0; + } + bridge->topics = NULL; + bridge->topic_remapping = true; // enable remapping behavior + context->bridge = bridge; + + // Allocate a single bridge topic node and populate fields. + struct mosquitto__bridge_topic *topic_node = (struct mosquitto__bridge_topic*)calloc(1, sizeof(struct mosquitto__bridge_topic)); + if(!topic_node){ + free(initial_topic); + free(remote_topic); + if(remote_prefix) free(remote_prefix); + if(local_prefix) free(local_prefix); + free(bridge); + free(context); + return 0; + } + + // Set direction + topic_node->direction = dir; + + // The function expects remote_topic to be set (it calls mosquitto_topic_matches_sub on it). + // Copy remote_topic into the struct (use mosquitto_strdup if available, otherwise fallback to strdup) +#ifdef mosquitto_strdup + topic_node->remote_topic = mosquitto_strdup(remote_topic ? remote_topic : ""); +#else + topic_node->remote_topic = remote_topic ? strdup(remote_topic) : strdup(""); +#endif + + // remote_prefix/local_prefix: set to NULL if empty to emulate the "not set" state + if(remote_prefix && strlen(remote_prefix) > 0){ +#ifdef mosquitto_strdup + topic_node->remote_prefix = mosquitto_strdup(remote_prefix); +#else + topic_node->remote_prefix = strdup(remote_prefix); +#endif + }else{ + topic_node->remote_prefix = NULL; + } + if(local_prefix && strlen(local_prefix) > 0){ +#ifdef mosquitto_strdup + topic_node->local_prefix = mosquitto_strdup(local_prefix); +#else + topic_node->local_prefix = strdup(local_prefix); +#endif + }else{ + topic_node->local_prefix = NULL; + } + + // Link the single node into bridge->topics + bridge->topics = topic_node; + topic_node->next = NULL; + + // Prepare the topic pointer expected by bridge__remap_topic_in. +#ifdef mosquitto_strdup + char *topic_for_call = mosquitto_strdup(initial_topic ? initial_topic : ""); +#else + char *topic_for_call = initial_topic ? strdup(initial_topic) : strdup(""); +#endif + + // Call the target function. + // It may free and replace *topic_for_call; that's expected. + if(topic_for_call){ + bridge__remap_topic_in(context, &topic_for_call); + } + + // Cleanup: free the potentially modified topic pointer and all allocated structures. + if(topic_for_call){ + // Use mosquitto_FREE macro which sets ptr to NULL after freeing (declared in libcommon_memory.h). +#ifdef mosquitto_FREE + mosquitto_FREE(topic_for_call); +#else + free(topic_for_call); + topic_for_call = nullptr; +#endif + } + + // Free bridge topic node strings and node + if(topic_node){ + if(topic_node->remote_topic){ +#ifdef mosquitto_FREE + mosquitto_FREE(topic_node->remote_topic); +#else + free(topic_node->remote_topic); +#endif + } + if(topic_node->remote_prefix){ +#ifdef mosquitto_FREE + mosquitto_FREE(topic_node->remote_prefix); +#else + free(topic_node->remote_prefix); +#endif + } + if(topic_node->local_prefix){ +#ifdef mosquitto_FREE + mosquitto_FREE(topic_node->local_prefix); +#else + free(topic_node->local_prefix); +#endif + } + free(topic_node); + } + + // Free bridge and context + free(bridge); + + // Note: some mosquitto internals might have allocated resources; since we only used a minimal context, + // free the top-level struct. + free(context); + + // Free local temporary allocations that we created for partitioning (those not moved into topic_node) + // initial_topic and remote_topic were either strdup'd into topic_for_call / topic_node->remote_topic earlier, + // but we may have leftover allocations for remote_prefix/local_prefix when we set them to NULL. + if(remote_topic){ + // remote_topic was used to initialize topic_node->remote_topic via strdup/mosquitto_strdup. + free(remote_topic); + } + if(remote_prefix){ + free(remote_prefix); + } + if(local_prefix){ + free(local_prefix); + } + // initial_topic was strdup'ed to topic_for_call earlier; we freed topic_for_call via mosquitto_FREE or free, + // but if we didn't use mosquitto_strdup for initial_topic then initial_topic still points to malloced buffer. + if(initial_topic){ + free(initial_topic); + } + + return 0; +} diff --git a/fuzzing/broker/broker_fuzz_properties_to_json.cpp b/fuzzing/broker/broker_fuzz_properties_to_json.cpp new file mode 100644 index 0000000000..72b14f1e0c --- /dev/null +++ b/fuzzing/broker/broker_fuzz_properties_to_json.cpp @@ -0,0 +1,360 @@ +// /src/mosquitto/fuzzing/apps/db_dump/db_dump_fuzz_load.cpp +#include +#include +#include +#include +#include + +#include "/src/mosquitto/include/mosquitto.h" // pulls in libcommon.h and libcommon_cjson.h in correct order +#include "/src/mosquitto/libcommon/property_common.h" // struct mqtt5__property (mosquitto_property) definition +#include "/src/cJSON/cJSON.h" // cJSON APIs + +// Fuzzer entry point. +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) +{ + if(!Data || Size == 0) return 0; + + const uint8_t *cursor = Data; + size_t remaining = Size; + + // Build a linked list of mosquitto_property structures from the input bytes. + // Format (repeated as long as bytes remain): + // 1 byte : prop_type_selector (we map this to 1..7) + // 1 byte : identifier (0..255) + // Depending on prop type: + // BYTE: 1 byte value + // INT16: 2 bytes (little endian) + // INT32: 4 bytes (little endian) + // VARINT: 4 bytes (little endian) + // BINARY: 2 bytes length (LE), then that many bytes + // STRING: 2 bytes length (LE), then that many bytes + // STRING_PAIR: 2 bytes name_len, name bytes, 2 bytes value_len, value bytes + // + // Lengths are clamped to avoid large allocations. + + const size_t MAX_ITEMS = 128; + const size_t MAX_STR_LEN = 4096; + + mosquitto_property *head = NULL; + mosquitto_property *tail = NULL; + size_t items_created = 0; + + auto read_u8 = [&](uint8_t &out)->bool{ + if(remaining < 1) return false; + out = *cursor; + cursor++; remaining--; + return true; + }; + auto read_u16 = [&](uint16_t &out)->bool{ + if(remaining < 2) return false; + out = (uint16_t)cursor[0] | ((uint16_t)cursor[1] << 8); + cursor += 2; remaining -= 2; + return true; + }; + auto read_u32 = [&](uint32_t &out)->bool{ + if(remaining < 4) return false; + out = (uint32_t)cursor[0] | ((uint32_t)cursor[1] << 8) | ((uint32_t)cursor[2] << 16) | ((uint32_t)cursor[3] << 24); + cursor += 4; remaining -= 4; + return true; + }; + + while(remaining > 0 && items_created < MAX_ITEMS) { + uint8_t sel; + if(!read_u8(sel)) break; + uint8_t id; + if(!read_u8(id)) break; + + // Allocate and zero the property + mosquitto_property *prop = (mosquitto_property *)calloc(1, sizeof(mosquitto_property)); + if(!prop) break; + + prop->next = NULL; + // Map selector to property type 1..7 + uint8_t prop_type = (uint8_t)((sel % 7) + 1); + prop->property_type = prop_type; + prop->identifier = (int)id; + prop->client_generated = false; + + switch(prop_type){ + case MQTT_PROP_TYPE_BYTE: { + uint8_t v = 0; + if(!read_u8(v)) v = 0; + prop->value.i8 = v; + break; + } + case MQTT_PROP_TYPE_INT16: { + uint16_t v = 0; + if(!read_u16(v)) v = 0; + prop->value.i16 = v; + break; + } + case MQTT_PROP_TYPE_INT32: { + uint32_t v = 0; + if(!read_u32(v)) v = 0; + prop->value.i32 = v; + break; + } + case MQTT_PROP_TYPE_VARINT: { + uint32_t v = 0; + if(!read_u32(v)) v = 0; + prop->value.varint = v; + break; + } + case MQTT_PROP_TYPE_BINARY: { + uint16_t len = 0; + if(!read_u16(len)) len = 0; + if(len > MAX_STR_LEN) len = (uint16_t)MAX_STR_LEN; + if(len > remaining) len = (uint16_t)remaining; + if(len){ + // allocate binary buffer + prop->value.bin.len = len; + prop->value.bin.v = (char*)malloc((size_t)len); + if(prop->value.bin.v){ + memcpy(prop->value.bin.v, cursor, len); + cursor += len; + remaining -= len; + }else{ + // allocation failed, ensure consistent state + prop->value.bin.len = 0; + } + }else{ + prop->value.bin.len = 0; + prop->value.bin.v = NULL; + } + break; + } + case MQTT_PROP_TYPE_STRING: { + uint16_t len = 0; + if(!read_u16(len)) len = 0; + if(len > MAX_STR_LEN) len = (uint16_t)MAX_STR_LEN; + if(len > remaining) len = (uint16_t)remaining; + if(len){ + prop->value.s.len = len; + prop->value.s.v = (char*)malloc((size_t)len + 1); + if(prop->value.s.v){ + memcpy(prop->value.s.v, cursor, len); + prop->value.s.v[len] = '\0'; + cursor += len; + remaining -= len; + }else{ + prop->value.s.len = 0; + prop->value.s.v = NULL; + } + }else{ + // zero-length string allowed + prop->value.s.len = 0; + prop->value.s.v = (char*)malloc(1); + if(prop->value.s.v) prop->value.s.v[0] = '\0'; + } + break; + } + case MQTT_PROP_TYPE_STRING_PAIR: { + // name + uint16_t name_len = 0; + if(!read_u16(name_len)) name_len = 0; + if(name_len > MAX_STR_LEN) name_len = (uint16_t)MAX_STR_LEN; + if(name_len > remaining) name_len = (uint16_t)remaining; + if(name_len){ + prop->name.len = name_len; + prop->name.v = (char*)malloc((size_t)name_len + 1); + if(prop->name.v){ + memcpy(prop->name.v, cursor, name_len); + prop->name.v[name_len] = '\0'; + cursor += name_len; + remaining -= name_len; + }else{ + prop->name.len = 0; + prop->name.v = NULL; + } + }else{ + prop->name.len = 0; + prop->name.v = (char*)malloc(1); + if(prop->name.v) prop->name.v[0] = '\0'; + } + + // value + uint16_t value_len = 0; + if(!read_u16(value_len)) value_len = 0; + if(value_len > MAX_STR_LEN) value_len = (uint16_t)MAX_STR_LEN; + if(value_len > remaining) value_len = (uint16_t)remaining; + if(value_len){ + prop->value.s.len = value_len; + prop->value.s.v = (char*)malloc((size_t)value_len + 1); + if(prop->value.s.v){ + memcpy(prop->value.s.v, cursor, value_len); + prop->value.s.v[value_len] = '\0'; + cursor += value_len; + remaining -= value_len; + }else{ + prop->value.s.len = 0; + prop->value.s.v = NULL; + } + }else{ + prop->value.s.len = 0; + prop->value.s.v = (char*)malloc(1); + if(prop->value.s.v) prop->value.s.v[0] = '\0'; + } + break; + } + default: + // Should not happen because we mapped to 1..7, but keep conservative behavior. + break; + } + + // Append to list + if(!head){ + head = tail = prop; + }else{ + tail->next = prop; + tail = prop; + } + + items_created++; + } + + // If we created no properties from the parsing loop but we do have input bytes, + // construct a single fallback property from the raw input so the fuzz data + // influences execution paths of mosquitto_properties_to_json. + if(items_created == 0 && Size > 0){ + mosquitto_property *prop = (mosquitto_property *)calloc(1, sizeof(mosquitto_property)); + if(prop){ + prop->next = NULL; + uint8_t sel = Data[0]; + uint8_t id = (Size > 1) ? Data[1] : 0; + uint8_t prop_type = (uint8_t)((sel % 7) + 1); + prop->property_type = prop_type; + prop->identifier = (int)id; + prop->client_generated = false; + + // Use the remainder of Data (if any) to populate the value. + const uint8_t *fb_cursor = Data + 2; + size_t fb_remaining = (Size > 2) ? (Size - 2) : 0; + + switch(prop_type){ + case MQTT_PROP_TYPE_BYTE: { + uint8_t v = 0; + if(fb_remaining >= 1) v = fb_cursor[0]; + prop->value.i8 = v; + break; + } + case MQTT_PROP_TYPE_INT16: { + uint16_t v = 0; + if(fb_remaining >= 2) v = (uint16_t)fb_cursor[0] | ((uint16_t)fb_cursor[1] << 8); + prop->value.i16 = v; + break; + } + case MQTT_PROP_TYPE_INT32: { + uint32_t v = 0; + if(fb_remaining >= 4) v = (uint32_t)fb_cursor[0] | ((uint32_t)fb_cursor[1] << 8) | ((uint32_t)fb_cursor[2] << 16) | ((uint32_t)fb_cursor[3] << 24); + prop->value.i32 = v; + break; + } + case MQTT_PROP_TYPE_VARINT: { + uint32_t v = 0; + if(fb_remaining >= 4) v = (uint32_t)fb_cursor[0] | ((uint32_t)fb_cursor[1] << 8) | ((uint32_t)fb_cursor[2] << 16) | ((uint32_t)fb_cursor[3] << 24); + prop->value.varint = v; + break; + } + case MQTT_PROP_TYPE_BINARY: { + uint16_t len = (uint16_t)fb_remaining; + if(len > MAX_STR_LEN) len = (uint16_t)MAX_STR_LEN; + if(len){ + prop->value.bin.len = len; + prop->value.bin.v = (char*)malloc((size_t)len); + if(prop->value.bin.v){ + memcpy(prop->value.bin.v, fb_cursor, len); + }else{ + prop->value.bin.len = 0; + } + }else{ + prop->value.bin.len = 0; + prop->value.bin.v = NULL; + } + break; + } + case MQTT_PROP_TYPE_STRING: { + uint16_t len = (uint16_t)fb_remaining; + if(len > MAX_STR_LEN) len = (uint16_t)MAX_STR_LEN; + if(len){ + prop->value.s.len = len; + prop->value.s.v = (char*)malloc((size_t)len + 1); + if(prop->value.s.v){ + memcpy(prop->value.s.v, fb_cursor, len); + prop->value.s.v[len] = '\0'; + }else{ + prop->value.s.len = 0; + prop->value.s.v = NULL; + } + }else{ + prop->value.s.len = 0; + prop->value.s.v = (char*)malloc(1); + if(prop->value.s.v) prop->value.s.v[0] = '\0'; + } + break; + } + case MQTT_PROP_TYPE_STRING_PAIR: { + // Split fb_remaining roughly in half for name/value + uint16_t name_len = (uint16_t)(fb_remaining / 2); + uint16_t value_len = (uint16_t)(fb_remaining - name_len); + if(name_len > MAX_STR_LEN) name_len = (uint16_t)MAX_STR_LEN; + if(value_len > MAX_STR_LEN) value_len = (uint16_t)MAX_STR_LEN; + + if(name_len){ + prop->name.len = name_len; + prop->name.v = (char*)malloc((size_t)name_len + 1); + if(prop->name.v){ + memcpy(prop->name.v, fb_cursor, name_len); + prop->name.v[name_len] = '\0'; + }else{ + prop->name.len = 0; + prop->name.v = NULL; + } + }else{ + prop->name.len = 0; + prop->name.v = (char*)malloc(1); + if(prop->name.v) prop->name.v[0] = '\0'; + } + + if(value_len){ + prop->value.s.len = value_len; + prop->value.s.v = (char*)malloc((size_t)value_len + 1); + if(prop->value.s.v){ + memcpy(prop->value.s.v, fb_cursor + name_len, value_len); + prop->value.s.v[value_len] = '\0'; + }else{ + prop->value.s.len = 0; + prop->value.s.v = NULL; + } + }else{ + prop->value.s.len = 0; + prop->value.s.v = (char*)malloc(1); + if(prop->value.s.v) prop->value.s.v[0] = '\0'; + } + break; + } + default: + break; + } + + head = tail = prop; + items_created = 1; + } + } + + // Call the function under test. + // It may return NULL; if it returns a cJSON object, free it. + cJSON *json = mosquitto_properties_to_json(head); + if(json){ + cJSON_Delete(json); + } + + // Free the properties list. Use the library provided free function to get realistic cleanup. + // mosquitto_property_free_all is declared in libcommon properties header. + mosquitto_property_free_all(&head); + + // Sanity: head should now be NULL. + // (We don't assert because fuzzer runs shouldn't abort unexpectedly.) + (void)head; + + return 0; +} diff --git a/fuzzing/broker/broker_fuzz_property_read_all.cpp b/fuzzing/broker/broker_fuzz_property_read_all.cpp new file mode 100644 index 0000000000..f363c11075 --- /dev/null +++ b/fuzzing/broker/broker_fuzz_property_read_all.cpp @@ -0,0 +1,58 @@ +#include +#include +#include +#include +#include + +/* Mosquitto is C code. Ensure C linkage when included from C++ to avoid + * name-mangling / undefined reference errors at link time. + */ +extern "C" { +#include "/src/mosquitto/lib/property_mosq.h" +#include "/src/mosquitto/lib/mosquitto_internal.h" +#include "/src/mosquitto/libcommon/property_common.h" +#include "/src/mosquitto/include/mosquitto/libcommon_properties.h" +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) +{ + if(!Data || Size == 0){ + return 0; + } + + /* Prepare a mosquitto__packet_in populated with the fuzzer data. + * property__read_all and the underlying packet__read_* helpers read from + * packet->payload using packet->pos and packet->remaining_length. + */ + struct mosquitto__packet_in packet; + memset(&packet, 0, sizeof(packet)); + + /* remaining_length is a uint32_t in the project. Cap Size appropriately. */ + uint32_t rlen = (Size > UINT32_MAX) ? UINT32_MAX : (uint32_t)Size; + + packet.payload = (uint8_t *)malloc((size_t)rlen); + if(!packet.payload){ + return 0; + } + memcpy(packet.payload, Data, (size_t)rlen); + packet.remaining_length = rlen; + packet.pos = 0; + + mosquitto_property *properties = NULL; + + /* Use command value 0 for fuzzing; property__read_all will validate + * properties against the command via mosquitto_property_check_all. + */ + (void)property__read_all(0, &packet, &properties); + + /* Ensure any allocated properties are freed. property__read_all will free + * on many error paths, but call this to be safe for successful or + * partially-successful parses. + */ + mosquitto_property_free_all(&properties); + + free(packet.payload); + packet.payload = NULL; + + return 0; +} diff --git a/fuzzing/broker/broker_fuzz_proxy_v1_decode.cpp b/fuzzing/broker/broker_fuzz_proxy_v1_decode.cpp new file mode 100644 index 0000000000..b58dcd478d --- /dev/null +++ b/fuzzing/broker/broker_fuzz_proxy_v1_decode.cpp @@ -0,0 +1,125 @@ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Use the C header for stdint types (keeps compatibility) +#include + +// Include the project's internal mosquitto definitions (absolute path discovered in tree). +// Adjust if your checkout layout differs. +extern "C" { +#include "/src/mosquitto/lib/mosquitto_internal.h" +#include "/src/mosquitto/src/mosquitto_broker_internal.h" + +// Provide weak stubs for functions that might be referenced by proxy_v1.c +// If the project provides them, those definitions will take precedence. + +// Weak mosquitto_strdup fallback +__attribute__((weak)) char *mosquitto_strdup(const char *s) +{ + if(!s) return NULL; + size_t n = strlen(s) + 1; + char *p = (char*)malloc(n); + if(!p) return NULL; + memcpy(p, s, n); + return p; +} + +// Match project declaration for net__socket_get_address: +// int net__socket_get_address(mosq_sock_t sock, char *buf, size_t len, uint16_t *remote_port); +__attribute__((weak)) int net__socket_get_address(mosq_sock_t sock, char *address, size_t len, uint16_t *port) +{ + (void)sock; + (void)address; + (void)len; + (void)port; + // Indicate failure by returning -1 (replicating earlier harness behavior) + return -1; +} + +// ---- Workaround for void* -> uint8_t* assignments inside proxy_v1.c (C -> C++ conversion) ---- +// proxy_v1.c was written in C and assigns the result of mosquitto_calloc (void*) directly to a uint8_t*. +// When compiling that C file as part of this C++ TU (by including it), C++ forbids implicit conversion from void*. +// To avoid editing project sources, provide a small wrapper macro so mosquitto_calloc(...) in proxy_v1.c +// becomes ((uint8_t*)mosquitto_calloc_impl(...)). Define the implementation function below. +// +// Note: The macro must be defined before including proxy_v1.c. +extern "C" void *mosquitto_calloc_impl(size_t nmemb, size_t size); +#define mosquitto_calloc(nmemb, size) ((uint8_t*)mosquitto_calloc_impl((nmemb), (size))) + +extern "C" void *mosquitto_calloc_impl(size_t nmemb, size_t size) +{ + // Simple implementation using standard calloc. + // This returns void*; the macro casts it to uint8_t* where needed. + return calloc(nmemb, size); +} + +// Redirect log__printf calls in proxy_v1.c to suppress log output during fuzzing. +// Must be defined after headers but before including proxy_v1.c. +#undef log__printf +#define log__printf(mosq, level, ...) (0) + +// Include the real proxy_v1.c implementation from the project so the static +// function proxy_v1__decode is compiled into this TU. Adjust the path if needed. +#include "/src/mosquitto/src/proxy_v1.c" + +} // extern "C" + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) +{ + if(!Data) return 0; + + // Ensure at least 2 bytes to avoid proxy_v1__decode writing at pos-1/pos-2 + size_t buf_size = (Size >= 2) ? Size : 2; + uint8_t *buf = (uint8_t*)malloc(buf_size + 1); + if(!buf) return 0; + + if(Size > 0){ + memcpy(buf, Data, Size); + } + buf[buf_size] = 0; + + // Construct a minimal mosquitto context using project struct definitions. + struct mosquitto ctx; + memset(&ctx, 0, sizeof(ctx)); + + // Create a mock listener to avoid NULL pointer dereference in update_transport(). + // In the real broker, context->listener is always set when a connection is accepted. + struct mosquitto__listener listener {}; + ctx.listener = &listener; + + // The project uses ctx.proxy.buf and ctx.proxy.pos + ctx.proxy.buf = (uint8_t*)buf; // cast to uint8_t* to match struct type + ctx.proxy.pos = buf_size; + ctx.proxy.fam = 0; + ctx.sock = (mosq_sock_t)-1; + ctx.address = NULL; + ctx.remote_port = 0; + + // Call the real target function from the project. + (void)proxy_v1__decode(&ctx); + + // Cleanup + if(ctx.address){ + free(ctx.address); + ctx.address = NULL; + } + // proxy_cleanup is defined static in proxy_v1.c and is available in this TU, + // so calling it will free ctx.proxy.buf etc as required. + proxy_cleanup(&ctx); + + // Note: Do not free(buf) here because proxy_cleanup may have freed ctx.proxy.buf. + // If it didn't, the allocator in proxy_cleanup likely uses the same heap and the + // OS will reclaim at process exit during fuzzing harness teardown. + + return 0; +} diff --git a/fuzzing/broker/broker_fuzz_subscribe.cpp b/fuzzing/broker/broker_fuzz_subscribe.cpp new file mode 100644 index 0000000000..28f22c6b57 --- /dev/null +++ b/fuzzing/broker/broker_fuzz_subscribe.cpp @@ -0,0 +1,166 @@ +// Fixed fuzzer harness for int handle__subscribe(struct mosquitto *context). +// Main fixes: +// - Initialize subscription subsystem if not already initialized (sub__init). +// - Call sub__clean_session(context) after handle__subscribe to remove any +// subscriptions associated with the test context so the global subscription +// trees don't retain pointers to freed contexts (prevents UAF across fuzz iterations). +// - Call packet__cleanup_all_no_locks(context) to free any outgoing packets +// queued by the handler so we don't leak memory allocated by packet__alloc/send__suback. +// This file assumes it's built in the project environment with the mosquitto +// headers and sources available at the absolute paths used below. + +#include +#include +#include +#include +#include +#include + +extern "C" { + /* Include project headers declaring struct mosquitto, db and handle__subscribe. */ + #include "/src/mosquitto/lib/mosquitto_internal.h" + #include "/src/mosquitto/src/mosquitto_broker_internal.h" + + /* Fallback declaration in case include paths differ. The includes above should + * provide this, but keep this as a safety net. + */ + int handle__subscribe(struct mosquitto *context); + + /* sub__init and sub__clean_session are declared in mosquitto_broker_internal.h, + * but redeclare here to be explicit in case of include path issues. + */ + int sub__init(void); + int sub__clean_session(struct mosquitto *context); + + /* Ensure packet cleanup no-locks is available (declared in lib/packet_mosq.h). + * We call the no-lock variant because the harness-created context has zeroed + * mutexes and calling the locked variant would be unsafe. + */ + void packet__cleanup_all_no_locks(struct mosquitto *mosq); +} + +/* LLVM fuzzer entry point */ +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) +{ + if(!Data || Size < 2){ + return 0; + } + + /* Allocate and zero a mosquitto context. */ + struct mosquitto *context = (struct mosquitto *)calloc(1, sizeof(struct mosquitto)); + if(!context) return 0; + + /* Set required fields to trigger subscribe handling code path. */ + context->state = mosq_cs_active; + /* Use MQTT 3.1.1 to avoid MQTT5 property parsing paths. */ + context->protocol = mosq_p_mqtt311; + context->max_qos = 2; + context->retain_available = 1; + context->is_bridge = false; + + /* Provide small non-NULL id/address to avoid potential null derefs in logging + * and to satisfy mosquitto_acl_check's early checks. + */ + context->id = strdup("fuzz-client"); + context->address = strdup("127.0.0.1"); + + /* Ensure the global db.config is allocated. mosquitto_acl_check() accesses + * db.config and will dereference it; when running the harness in isolation + * this may otherwise be NULL and lead to a crash (seen as ASan SEGV). + * + * We allocate and zero a struct mosquitto__config so plugin pointers are NULL + * and per_listener_settings is false by default. + */ + bool db_config_alloc = false; + if(db.config == NULL){ + db.config = (struct mosquitto__config *)calloc(1, sizeof(struct mosquitto__config)); + if(db.config){ + db_config_alloc = true; + /* Default to no per-listener plugins to avoid listener dereferences. */ + db.config->per_listener_settings = false; + } + } + + /* Ensure subscription subsystem is initialized so normal_subs/shared_subs + * roots exist and are in a consistent state. + */ + if(db.normal_subs == NULL && db.shared_subs == NULL){ + (void)sub__init(); + } + + /* Prepare the incoming packet structure to point at the fuzzer data. */ + struct mosquitto__packet_in *pin = &context->in_packet; + + pin->payload = (uint8_t *)malloc(Size); + if(!pin->payload){ + if(context->id) { free(context->id); context->id = NULL; } + if(context->address) { free(context->address); context->address = NULL; } + free(context); + if(db_config_alloc){ + free(db.config); + db.config = NULL; + } + return 0; + } + memcpy(pin->payload, Data, Size); + + /* Set packet metadata expected by packet__read_* helpers. */ + pin->remaining_length = (uint32_t)Size; + pin->packet_length = (uint32_t)Size; + pin->pos = 0; + pin->remaining_mult = 0; + pin->remaining_count = 0; + pin->to_process = 0; + pin->packet_buffer = NULL; + pin->packet_buffer_pos = 0; + pin->packet_buffer_size = 0; + + /* The handler checks that command == (CMD_SUBSCRIBE|2). */ + pin->command = (uint8_t)(CMD_SUBSCRIBE | 2); + + /* Call the target function under test. */ + (void)handle__subscribe(context); + + /* After handling subscribe, remove any subscriptions attached to this context + * so the global subscription lists do not retain pointers to our context when + * we free it. This prevents heap-use-after-free across fuzz iterations. + */ + (void)sub__clean_session(context); + + /* Free any outgoing packets queued by the handler so we don't leak memory + * allocated by packet__alloc/send__suback. We use the no-locks variant so + * we don't attempt to lock uninitialized mutexes in our zeroed context. + * + * Note: packet__cleanup_all_no_locks will call packet__cleanup(&context->in_packet), + * which may free in_packet.payload. To avoid double-freeing later, clear the + * pointer after calling this cleanup. + */ + packet__cleanup_all_no_locks(context); + context->in_packet.payload = NULL; + + /* Clean up allocated memory. */ + if(pin->payload){ + free(pin->payload); + pin->payload = NULL; + } + if(context->id){ + free(context->id); + context->id = NULL; + } + if(context->address){ + free(context->address); + context->address = NULL; + } + + free(context); + + /* Free the db.config we allocated for the harness, if any. Leave db in a + * clean state for subsequent fuzz iterations. + */ + if(db_config_alloc && db.config){ + free(db.config); + db.config = NULL; + } + + return 0; +} diff --git a/fuzzing/broker/broker_fuzz_will_set.cpp b/fuzzing/broker/broker_fuzz_will_set.cpp new file mode 100644 index 0000000000..312075b0c3 --- /dev/null +++ b/fuzzing/broker/broker_fuzz_will_set.cpp @@ -0,0 +1,107 @@ +#include +#include +#include +#include +#include +#include + +// Ensure C linkage for the mosquitto C headers so the C symbols are not C++-mangled. +extern "C" { +#include "/src/mosquitto/include/mosquitto.h" +#include "/src/mosquitto/lib/mosquitto_internal.h" +#include "/src/mosquitto/lib/will_mosq.h" +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) +{ + // Basic guard. + if(!Data || Size == 0) return 0; + + // Create a zeroed mosquitto structure so will__set has a valid object to operate on. + struct mosquitto *mosq = (struct mosquitto *)calloc(1, sizeof(struct mosquitto)); + if(!mosq) return 0; + // Make protocol MQTT v5 by default (some paths require this). + mosq->protocol = mosq_p_mqtt5; + mosq->will = NULL; + + size_t pos = 0; + + // Extract a topic length (bounded) from the first byte. + size_t topic_len = 0; + if(pos < Size){ + topic_len = Data[pos++]; + // bound topic length to reasonable value to avoid excessive allocations + topic_len = std::min(topic_len, 1024); + } + + // Build a topic string from the next topic_len bytes (or remaining bytes). + size_t avail = (pos < Size) ? (Size - pos) : 0; + size_t copy_len = std::min(topic_len, avail); + std::string topic; + if(copy_len > 0){ + topic.assign(reinterpret_cast(Data + pos), copy_len); + pos += copy_len; + }else{ + // Ensure topic is at least an empty string (not a null pointer). + topic = ""; + } + // Guarantee null-termination for C APIs. + // topic.c_str() is guaranteed null-terminated by std::string. + + // Extract payload length from next byte if available. Keep it small and within remaining buffer. + int payloadlen = 0; + if(pos < Size){ + payloadlen = static_cast(Data[pos++]); + // Allow payloadlen to be up to remaining bytes but keep it reasonable. + if(payloadlen < 0) payloadlen = 0; + size_t remaining = (pos < Size) ? (Size - pos) : 0; + if((size_t)payloadlen > remaining){ + payloadlen = static_cast(remaining); + } + }else{ + payloadlen = 0; + } + + // Determine payload pointer and advance pos over payload bytes. + const void *payload = nullptr; + if(payloadlen > 0){ + size_t remaining = (pos < Size) ? (Size - pos) : 0; + size_t use_len = std::min((size_t)payloadlen, remaining); + if(use_len > 0){ + payload = Data + pos; + pos += use_len; + // payloadlen was already clamped above to remaining, so use_len should equal payloadlen + }else{ + payload = nullptr; + // ensure payloadlen is set to 0 if there's no data + payloadlen = 0; + } + } + + // Extract qos (0..2) from next byte if present. + int qos = 0; + if(pos < Size){ + qos = Data[pos++] % 3; // valid qos values 0,1,2 + } + + // Extract retain flag from next byte if present. + bool retain = false; + if(pos < Size){ + retain = (Data[pos++] & 0x1) != 0; + } + + // We will not construct properties for simplicity. Pass nullptr to skip MQTT5 property checks. + mosquitto_property *properties = nullptr; + + // Call the function under test. + // The function will copy topic and payload internally, or return an error. + (void)will__set(mosq, topic.c_str(), payloadlen, payload, qos, retain, properties); + + // Clean up any will structure allocated by will__set. + will__clear(mosq); + + // Free the mosquitto object. + free(mosq); + + return 0; +} diff --git a/fuzzing/broker/broker_fuzz_ws_prepare_packet.cpp b/fuzzing/broker/broker_fuzz_ws_prepare_packet.cpp new file mode 100644 index 0000000000..e11e4ce671 --- /dev/null +++ b/fuzzing/broker/broker_fuzz_ws_prepare_packet.cpp @@ -0,0 +1,110 @@ +#include +#include +#include +#include +#include +#include +#include + +// Project headers (paths as seen in repository). If your build requires different +// include paths, change these to project-relative includes. +#ifdef __cplusplus +extern "C" { +#endif +#include "/src/mosquitto/lib/mosquitto_internal.h" +#include "/src/mosquitto/lib/net_mosq.h" +#ifdef __cplusplus +} +#endif + +// Provide a simple deterministic mosquitto_getrandom implementation used by the code. +// The library declares mosquitto_getrandom as: +// int mosquitto_getrandom(void *bytes, int count); +// so we must match that signature to avoid conflicting declarations. +extern "C" int mosquitto_getrandom(void *bytes, int count) +{ + if(!bytes || count <= 0) return 0; + static std::mt19937_64 rng(0x9E3779B97F4A7C15ULL); + static std::mutex rng_mutex; + std::lock_guard lock(rng_mutex); + uint8_t *b = static_cast(bytes); + int remaining = count; + while(remaining >= static_cast(sizeof(uint64_t))) { + uint64_t v = rng(); + std::memcpy(b, &v, sizeof(v)); + b += sizeof(v); + remaining -= static_cast(sizeof(v)); + } + if(remaining > 0) { + uint64_t v = rng(); + std::memcpy(b, &v, remaining); + } + return count; +} + +// Note: Do NOT provide fake/stub definitions for ws__prepare_packet or the fuzz helper +// functions here. The real implementations live in the project sources (e.g. net_ws.c +// and the fuzz helper compilation units). Defining stubs in this harness prevents the +// fuzzer from testing the real target functions. + +// Fuzzer entry point. +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) +{ + if(!Data || Size == 0) return 0; + + // Use first byte to configure websocket flags/opcode, rest is payload. + uint8_t config_byte = Data[0]; + const uint8_t *payload_src = Data + 1; + size_t payload_src_size = (Size > 1) ? (Size - 1) : 0; + + // Limit payload size to avoid huge allocations from malformed inputs. + const size_t MAX_PAYLOAD = 64 * 1024; // 64 KiB + size_t payload_len = std::min(payload_src_size, MAX_PAYLOAD); + + // Prepare a mosquitto object with zero-initialization. + struct mosquitto *mosq = (struct mosquitto *)malloc(sizeof(struct mosquitto)); + if(!mosq) return 0; + std::memset(mosq, 0, sizeof(struct mosquitto)); + + // Initialize ws_data inside mosq. Only fields used by ws__prepare_packet are set. + mosq->wsd.is_client = (config_byte & 0x1) ? true : false; + // If config_byte is 0xFF, set opcode to UINT8_MAX to trigger the "use WS_BINARY" branch. + mosq->wsd.opcode = config_byte; + + // Prepare a mosquitto__packet with flexible array payload[]. + // packet_length is expected to be WS_PACKET_OFFSET + actual_payload_length. + // Allocate enough space for header bytes (WS_PACKET_OFFSET) + payload_len. + size_t packet_payload_array_size = WS_PACKET_OFFSET + payload_len; + // Add a small safety margin to avoid out-of-bounds writes by the function. + const size_t SAFETY_MARGIN = 32; + size_t alloc_size = sizeof(struct mosquitto__packet) + packet_payload_array_size + SAFETY_MARGIN; + + struct mosquitto__packet *packet = (struct mosquitto__packet *)malloc(alloc_size); + if(!packet) { + free(mosq); + return 0; + } + // Zero everything to have deterministic baseline. + std::memset(packet, 0, alloc_size); + + // Set packet metadata. + packet->packet_length = (uint32_t)(WS_PACKET_OFFSET + payload_len); + packet->pos = 0; + packet->to_process = 0; + packet->remaining_length = 0; + packet->remaining_count = 0; + + // Copy fuzzer payload into the buffer area where ws__prepare_packet expects payload data. + if(payload_len) { + std::memcpy(&packet->payload[WS_PACKET_OFFSET], payload_src, payload_len); + } + + // Call the real target function under test from the project (net_ws.c). + ws__prepare_packet(mosq, packet); + + // Clean up. + free(packet); + free(mosq); + + return 0; +} \ No newline at end of file diff --git a/fuzzing/corpora/broker_fuzz_bridge_remap_topic_in_seed_corpus.zip b/fuzzing/corpora/broker_fuzz_bridge_remap_topic_in_seed_corpus.zip new file mode 100644 index 0000000000..96d3ed8b55 Binary files /dev/null and b/fuzzing/corpora/broker_fuzz_bridge_remap_topic_in_seed_corpus.zip differ diff --git a/fuzzing/corpora/broker_fuzz_properties_to_json_seed_corpus.zip b/fuzzing/corpora/broker_fuzz_properties_to_json_seed_corpus.zip new file mode 100644 index 0000000000..0d3088fda2 Binary files /dev/null and b/fuzzing/corpora/broker_fuzz_properties_to_json_seed_corpus.zip differ diff --git a/fuzzing/corpora/broker_fuzz_property_read_all_seed_corpus.zip b/fuzzing/corpora/broker_fuzz_property_read_all_seed_corpus.zip new file mode 100644 index 0000000000..34c3fe7ed7 Binary files /dev/null and b/fuzzing/corpora/broker_fuzz_property_read_all_seed_corpus.zip differ diff --git a/fuzzing/corpora/broker_fuzz_proxy_v1_decode_seed_corpus.zip b/fuzzing/corpora/broker_fuzz_proxy_v1_decode_seed_corpus.zip new file mode 100644 index 0000000000..e8a2769fcf Binary files /dev/null and b/fuzzing/corpora/broker_fuzz_proxy_v1_decode_seed_corpus.zip differ diff --git a/fuzzing/corpora/broker_fuzz_subscribe_seed_corpus.zip b/fuzzing/corpora/broker_fuzz_subscribe_seed_corpus.zip new file mode 100644 index 0000000000..e052a17ff4 Binary files /dev/null and b/fuzzing/corpora/broker_fuzz_subscribe_seed_corpus.zip differ diff --git a/fuzzing/corpora/broker_fuzz_will_set_seed_corpus.zip b/fuzzing/corpora/broker_fuzz_will_set_seed_corpus.zip new file mode 100644 index 0000000000..2feb5c34c3 Binary files /dev/null and b/fuzzing/corpora/broker_fuzz_will_set_seed_corpus.zip differ diff --git a/fuzzing/corpora/broker_fuzz_ws_prepare_packet_seed_corpus.zip b/fuzzing/corpora/broker_fuzz_ws_prepare_packet_seed_corpus.zip new file mode 100644 index 0000000000..707050e48d Binary files /dev/null and b/fuzzing/corpora/broker_fuzz_ws_prepare_packet_seed_corpus.zip differ diff --git a/fuzzing/corpora/dynsec_fuzz_config_from_json_seed_corpus.zip b/fuzzing/corpora/dynsec_fuzz_config_from_json_seed_corpus.zip new file mode 100644 index 0000000000..766da7c42a Binary files /dev/null and b/fuzzing/corpora/dynsec_fuzz_config_from_json_seed_corpus.zip differ diff --git a/fuzzing/corpora/dynsec_fuzz_roles_process_add_acl_seed_corpus.zip b/fuzzing/corpora/dynsec_fuzz_roles_process_add_acl_seed_corpus.zip new file mode 100644 index 0000000000..ae1eb40182 Binary files /dev/null and b/fuzzing/corpora/dynsec_fuzz_roles_process_add_acl_seed_corpus.zip differ diff --git a/fuzzing/plugins/dynamic-security/Makefile b/fuzzing/plugins/dynamic-security/Makefile index 5f8db8da1f..20859ed21f 100644 --- a/fuzzing/plugins/dynamic-security/Makefile +++ b/fuzzing/plugins/dynamic-security/Makefile @@ -4,7 +4,9 @@ include ${R}/fuzzing/config.mk .PHONY: all clean FUZZERS:= \ - dynsec_fuzz_load + dynsec_fuzz_load \ + dynsec_fuzz_config_from_json \ + dynsec_fuzz_roles_process_add_acl LOCAL_CPPFLAGS+= \ -I${R} -I${R}/common -I${R}/include -I${R}/lib -I${R}/src -I${R}/deps -I${R}/plugins/dynamic-security \ @@ -27,5 +29,19 @@ dynsec_fuzz_load : dynsec_fuzz_load.cpp install $@ ${OUT}/$@ cp ${R}/fuzzing/corpora/dynsec_config_seed_corpus.zip ${OUT}/$@_seed_corpus.zip +# Top 10 selected harnesses for upstream submission + +# Dynamic security JSON configuration parser - tests complex plugin configuration +dynsec_fuzz_config_from_json : dynsec_fuzz_config_from_json.cpp + $(CXX) $(LOCAL_CXXFLAGS) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $^ $(LOCAL_LIBADD) + install $@ ${OUT}/$@ + cp ${R}/fuzzing/corpora/$@_seed_corpus.zip ${OUT}/$@_seed_corpus.zip + +# Dynamic security role ACL management - tests security-critical access control +dynsec_fuzz_roles_process_add_acl : dynsec_fuzz_roles_process_add_acl.cpp + $(CXX) $(LOCAL_CXXFLAGS) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $^ $(LOCAL_LIBADD) + install $@ ${OUT}/$@ + cp ${R}/fuzzing/corpora/$@_seed_corpus.zip ${OUT}/$@_seed_corpus.zip + clean: rm -f *.o $(FUZZERS) *.gcno *.gcda diff --git a/fuzzing/plugins/dynamic-security/dynsec_fuzz_config_from_json.cpp b/fuzzing/plugins/dynamic-security/dynsec_fuzz_config_from_json.cpp new file mode 100644 index 0000000000..e90bdb28e3 --- /dev/null +++ b/fuzzing/plugins/dynamic-security/dynsec_fuzz_config_from_json.cpp @@ -0,0 +1,70 @@ +#include +#include +#include +#include + +extern "C" { +#include "/src/mosquitto/plugins/dynamic-security/dynamic_security.h" +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) +{ + if (Data == nullptr || Size == 0) { + return 0; + } + + // Cap the amount of data we copy to prevent large allocations and to keep each + // invocation bounded. 64 KiB is a reasonable limit for fuzzing JSON parsing here. + const size_t MAX_ALLOC = 64 * 1024; // 64 KiB + size_t use_size = Size; + if (use_size > MAX_ALLOC) use_size = MAX_ALLOC; + + char *json = (char *)malloc(use_size + 1); + if (json == nullptr) return 0; + + memcpy(json, Data, use_size); + json[use_size] = '\0'; + + // Prepare a minimal dynsec__data structure. Zero-initialized to keep pointers NULL. + struct dynsec__data data; + memset(&data, 0, sizeof(data)); + + // Call the target function with our fuzzed JSON string. + (void)dynsec__config_from_json(&data, json); + + // IMPORTANT: cleanup order matters. + // Groups reference clients (group->clientlist -> client). If clients are freed + // before groups, groups cleanup will access freed client memory and crash. + // Therefore, remove/cleanup groups first, then clients, then roles/kicklist. + dynsec_groups__cleanup(&data); + dynsec_clients__cleanup(&data); + dynsec_roles__cleanup(&data); + dynsec_kicklist__cleanup(&data); + + // Ensure any remaining top-level char* members are freed if set by the plugin. + // The cleanup functions above should free allocated internals, but guard here + // in case config_file/password_init_file were allocated separately and not freed. + // If these pointers point into our 'json' buffer, do NOT free them separately + // (they will be freed when we free 'json' below). Detect that by checking + // whether the pointer lies within the json buffer range. + if (data.config_file) { + char *p = data.config_file; + if (p < json || p >= json + use_size + 1) { + free(data.config_file); + } + data.config_file = NULL; + } + if (data.password_init_file) { + char *p = data.password_init_file; + if (p < json || p >= json + use_size + 1) { + free(data.password_init_file); + } + data.password_init_file = NULL; + } + + // Now free our temporary JSON buffer. It must be freed after cleanup to avoid + // use-after-free in cleanup routines that may reference substrings inside it. + free(json); + + return 0; +} \ No newline at end of file diff --git a/fuzzing/plugins/dynamic-security/dynsec_fuzz_roles_process_add_acl.cpp b/fuzzing/plugins/dynamic-security/dynsec_fuzz_roles_process_add_acl.cpp new file mode 100644 index 0000000000..19e9edbbe0 --- /dev/null +++ b/fuzzing/plugins/dynamic-security/dynsec_fuzz_roles_process_add_acl.cpp @@ -0,0 +1,220 @@ +// Fuzz driver for: +// int dynsec_roles__process_add_acl(struct dynsec__data * data, struct mosquitto_control_cmd * cmd); +// Uses the real target implementation from the project (no fake dynsec_roles__process_add_acl stub). +// Fixed to keep minimal stubs only for symbols that are genuinely missing at link time. + +#include +#include +#include +#include +#include +#include + +extern "C" { +#include "/src/cJSON/cJSON.h" +#include "/src/mosquitto/plugins/dynamic-security/dynamic_security.h" +#include "/src/mosquitto/include/mosquitto/broker_control.h" +} + +// Provide lightweight stubs for symbols that may not be available at link time +// (This avoids undefined reference linker errors during building of the fuzz targets). +extern "C" { + +// Note: Do NOT provide a fake dynsec_roles__process_add_acl implementation here. +// The real implementation from the project will be used (from plugins/dynamic-security/roles.c). +// We keep only other small stubs that the build references. + +int fuzz_packet_read_init(struct mosquitto *context) +{ + (void)context; + return 0; +} + +void fuzz_packet_read_cleanup(struct mosquitto *context) +{ + (void)context; +} + +} // extern "C" + +// Helper: map arbitrary bytes to a printable, UTF-8-safe string (a..z) +static std::string make_printable_string(const uint8_t *data, size_t size, size_t max_len = 64) +{ + std::string out; + if(!data || size == 0) return out; + size_t use = std::min(size, max_len); + out.resize(use); + for(size_t i = 0; i < use; i++){ + // map to lowercase letters + out[i] = char('a' + (data[i] % 26)); + } + return out; +} + +static struct dynsec__role * create_role_with_name(const char *name) +{ + if(!name) return nullptr; + size_t nlen = strlen(name); + // allocate space for struct + rolename flexible array + null + size_t alloc_sz = sizeof(struct dynsec__role) + nlen + 1; + struct dynsec__role *role = (struct dynsec__role *)malloc(alloc_sz); + if(!role) return nullptr; + // zero everything + memset(role, 0, alloc_sz); + // copy the rolename into the flexible array + memcpy(role->rolename, name, nlen+1); + return role; +} + +// Free any ACLs attached to a role (they are allocated with mosquitto_calloc / freed with mosquitto_free) +static void free_role_acls(struct dynsec__role *role) +{ + if(!role) return; + + struct dynsec__acl *acl, *tmp; + + // publish_c_send + HASH_ITER(hh, role->acls.publish_c_send, acl, tmp){ + HASH_DELETE(hh, role->acls.publish_c_send, acl); + mosquitto_free(acl); + } + // publish_c_recv + HASH_ITER(hh, role->acls.publish_c_recv, acl, tmp){ + HASH_DELETE(hh, role->acls.publish_c_recv, acl); + mosquitto_free(acl); + } + // subscribe_literal + HASH_ITER(hh, role->acls.subscribe_literal, acl, tmp){ + HASH_DELETE(hh, role->acls.subscribe_literal, acl); + mosquitto_free(acl); + } + // subscribe_pattern + HASH_ITER(hh, role->acls.subscribe_pattern, acl, tmp){ + HASH_DELETE(hh, role->acls.subscribe_pattern, acl); + mosquitto_free(acl); + } + // unsubscribe_literal + HASH_ITER(hh, role->acls.unsubscribe_literal, acl, tmp){ + HASH_DELETE(hh, role->acls.unsubscribe_literal, acl); + mosquitto_free(acl); + } + // unsubscribe_pattern + HASH_ITER(hh, role->acls.unsubscribe_pattern, acl, tmp){ + HASH_DELETE(hh, role->acls.unsubscribe_pattern, acl); + mosquitto_free(acl); + } +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) +{ + if(Data == nullptr || Size == 0) return 0; + + // Attempt to parse the input as JSON command + cJSON *j_command = nullptr; + // cJSON_Parse expects a NUL-terminated string. Create a temporary buffer. + // Limit to a reasonable size to avoid huge allocations. + size_t parse_len = std::min(Size, (size_t)65536); + // If input includes zeros, cJSON_Parse will stop early; still acceptable. + char *tmpbuf = (char *)malloc(parse_len + 1); + if(!tmpbuf) return 0; + memcpy(tmpbuf, Data, parse_len); + tmpbuf[parse_len] = '\0'; + j_command = cJSON_Parse(tmpbuf); + free(tmpbuf); + + // If parsing failed, synthesize a basic command using derived printable strings. + std::string rolename; + std::string topic; + if(j_command == nullptr){ + j_command = cJSON_CreateObject(); + // derive rolename and topic from the input bytes + rolename = make_printable_string(Data, Size, 24); + topic = make_printable_string(Data + (Size/3), Size > (Size/3) ? (Size - Size/3) : 0, 48); + if(rolename.empty()) rolename = "role"; + if(topic.empty()) topic = "a/topic"; + cJSON_AddStringToObject(j_command, "rolename", rolename.c_str()); + // choose a valid acltype so deeper codepaths are exercised + cJSON_AddStringToObject(j_command, "acltype", ACL_TYPE_SUB_LITERAL); + cJSON_AddStringToObject(j_command, "topic", topic.c_str()); + // optionally add priority/allow + cJSON_AddNumberToObject(j_command, "priority", 10); + cJSON_AddBoolToObject(j_command, "allow", true); + }else{ + // If parsing succeeded, try to read rolename/topic; if missing, add defaults. + cJSON *jr = cJSON_GetObjectItemCaseSensitive(j_command, "rolename"); + if(!jr || !cJSON_IsString(jr) || !jr->valuestring){ + rolename = make_printable_string(Data, Size, 24); + if(rolename.empty()) rolename = "role"; + cJSON_AddStringToObject(j_command, "rolename", rolename.c_str()); + }else{ + rolename = jr->valuestring; + } + cJSON *jt = cJSON_GetObjectItemCaseSensitive(j_command, "topic"); + if(!jt || !cJSON_IsString(jt) || !jt->valuestring){ + topic = make_printable_string(Data + (Size/3), Size > (Size/3) ? (Size - Size/3) : 0, 48); + if(topic.empty()) topic = "a/topic"; + cJSON_AddStringToObject(j_command, "topic", topic.c_str()); + }else{ + topic = jt->valuestring; + } + // ensure acltype exists + cJSON *ja = cJSON_GetObjectItemCaseSensitive(j_command, "acltype"); + if(!ja || !cJSON_IsString(ja) || !ja->valuestring){ + cJSON_AddStringToObject(j_command, "acltype", ACL_TYPE_SUB_LITERAL); + } + } + + // Prepare j_responses array for replies + cJSON *j_responses = cJSON_CreateArray(); + + // Prepare a minimal mosquitto_control_cmd + struct mosquitto_control_cmd cmd; + memset(&cmd, 0, sizeof(cmd)); + cmd.j_command = j_command; + cmd.j_responses = j_responses; + cmd.command_name = "addRoleACL"; + cmd.correlation_data = nullptr; + cmd.client = nullptr; // keeping NULL is acceptable; code handles NULL client for id/username + + // Prepare dynsec__data and ensure a role exists with the requested rolename + struct dynsec__data data; + memset(&data, 0, sizeof(data)); + data.roles = nullptr; + + // Find the rolename string we added/detected + const char *rolename_cstr = nullptr; + cJSON *jr_check = cJSON_GetObjectItemCaseSensitive(j_command, "rolename"); + if(jr_check && cJSON_IsString(jr_check) && jr_check->valuestring){ + rolename_cstr = jr_check->valuestring; + }else{ + // fallback + rolename_cstr = "role"; + } + + // Create a role with that rolename and insert into the hash table + struct dynsec__role *role = create_role_with_name(rolename_cstr); + if(role){ + // Initialize role fields to safe defaults (already zeroed by create_role_with_name) + // Insert into data.roles using uthash macro. Key is role->rolename with length strlen(...) + HASH_ADD_KEYPTR(hh, data.roles, role->rolename, (unsigned)strlen(role->rolename), role); + } + + // Call the real target function from the project. + // This will examine j_command, find the role in data.roles, and attempt to add an ACL. + (void)dynsec_roles__process_add_acl(&data, &cmd); + + // Cleanup: remove any ACLs attached to the role (they are allocated by the target with mosquitto_calloc) + if(role){ + free_role_acls(role); + + // Remove role from the hash and free allocated memory for the role name struct + HASH_DELETE(hh, data.roles, role); + free(role); + } + + // Free cJSON objects + if(j_command) cJSON_Delete(j_command); + if(j_responses) cJSON_Delete(j_responses); + + return 0; +} \ No newline at end of file