Skip to content

Commit

Permalink
Fixes #1613: Added fuzz testing for the http2 decoder. Fixes for two …
Browse files Browse the repository at this point in the history
…issues that the fuzz tester found.
  • Loading branch information
ganeshmurthy committed Sep 26, 2024
1 parent 7462d96 commit de72015
Show file tree
Hide file tree
Showing 71 changed files with 524 additions and 10 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ set(CMAKE_ENABLE_EXPORTS TRUE)
option(CMAKE_INTERPROCEDURAL_OPTIMIZATION "Perform link time optimization" ON)
option(ENABLE_WARNING_ERROR "Consider compiler warnings to be errors" ON)
option(ENABLE_PROFILE_GUIDED_OPTIMIZATION "Perform profile guided optimization" OFF)
option(ENABLE_FUZZ_TESTING "Enable building fuzzers and regression testing with libFuzzer" ON)

# preserve frame pointers for ease of debugging and profiling
# see https://fedoraproject.org/wiki/Changes/fno-omit-frame-pointer
Expand Down
3 changes: 3 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@
<exclude>share/index.html</exclude>
<exclude>scripts/git-clang-format</exclude>
<exclude>tests/nginx/**</exclude>
<exclude>tests/fuzz/fuzz_http2_decoder/**</exclude>
<exclude>tests/fuzz/fuzz_http1_decoder/**</exclude>
<exclude>**/README.md</exclude>
</excludes>
</configuration>
</plugin>
Expand Down
1 change: 1 addition & 0 deletions run.py.in
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ env_vars = {
'PYTHONPATH': os.pathsep.join(python_path),
'PATH': os.pathsep.join(dedup(["${CMAKE_BINARY_DIR}",
os.path.join("${CMAKE_BINARY_DIR}", 'tests'),
os.path.join("${CMAKE_BINARY_DIR}", 'tests/fuzz'),
os.path.join("${CMAKE_BINARY_DIR}", 'router'),
os.path.join("${CMAKE_SOURCE_DIR}", 'tools'),
os.path.join("${CMAKE_BINARY_DIR}", 'tools'),
Expand Down
40 changes: 36 additions & 4 deletions src/decoders/http2/http2_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ qd_http2_frame_type get_frame_type(const uint8_t frame_type)
return FRAME_TYPE_OTHER;
}

uint8_t get_pad_length(const uint8_t *data)
{
uint8_t pad_length = (uint8_t) ((data)[0]);
return pad_length;
}

uint32_t get_stream_identifier(const uint8_t *data)
{
uint32_t stream_id = (((uint32_t) (data)[0]) << 24) | (((uint32_t) (data)[1]) << 16) | (((uint32_t) (data)[2]) << 8) | ((uint32_t) (data)[3]);
Expand Down Expand Up @@ -321,15 +327,17 @@ bool get_request_headers(qd_http2_decoder_t *decoder)
qd_log(LOG_HTTP2_DECODER, QD_LOG_DEBUG, "[C%"PRIu64"] get_request_headers - end_stream=%i, end_headers=%i, is_padded=%i, has_priority=%i, stream_id=%" PRIu32, decoder->conn_state->conn_id, end_stream, end_headers, is_padded, has_priority, stream_id);
scratch_buffer->offset += HTTP2_FRAME_STREAM_ID_LENGTH;
decoder->frame_length_processed += HTTP2_FRAME_STREAM_ID_LENGTH;

uint8_t pad_length = 0;
if(is_padded) {
pad_length = get_pad_length(scratch_buffer->bytes + scratch_buffer->offset);
// Move one byte to account for pad length
scratch_buffer->offset += 1;
decoder->frame_length_processed += 1;
}

if(has_priority) {
// Skip the Stream Dependency field if the priority flag is set
// Stream Dependency field is 4 octets.
scratch_buffer->offset += 4;
decoder->frame_length_processed += 4;

Expand All @@ -338,6 +346,29 @@ bool get_request_headers(qd_http2_decoder_t *decoder)
decoder->frame_length_processed += 1;
}

//
// Before the call to decompress_headers(), we need to make sure that there is some data left in the scratch buffer before we decompress it.
//
int buffer_data_size = scratch_buffer->size - scratch_buffer->offset;
printf("get_request_headers buffer_data_size=%i\n", buffer_data_size);
int contains_pad_length = scratch_buffer->size - pad_length;
int pad_length_offset = scratch_buffer->size - pad_length - scratch_buffer->offset;
bool valid_pad_length = contains_pad_length > 0;
bool valid_buffer_data = buffer_data_size > 0;
bool valid_pad_length_offset = pad_length_offset > 0;
if(decoder->frame_payload_length == 0 || !valid_buffer_data || !valid_pad_length || !valid_pad_length_offset) {
qd_log(LOG_HTTP2_DECODER, QD_LOG_DEBUG, "[C%"PRIu64"] get_request_headers - failure, moving decoder state to HTTP2_DECODE_ERROR", decoder->conn_state->conn_id);
static char error[130];
snprintf(error, sizeof(error), "get_request_headers - either request or response header was received with zero payload or contains bogus data, stopping decoder");
reset_decoder_frame_info(decoder);
reset_scratch_buffer(&decoder->scratch_buffer);
parser_error(decoder, error);
return false;
}

// Take out the padding bytes from the end of the scratch buffer
scratch_buffer->size = scratch_buffer->size - pad_length;

// We are now finally at a place which matters to us - The Header block fragment. We will look thru and decompress it so we can get the request/response headers.
int rv = decompress_headers(decoder, stream_id, scratch_buffer->bytes + scratch_buffer->offset, scratch_buffer->size - scratch_buffer->offset);
if(rv < 0) {
Expand Down Expand Up @@ -380,11 +411,12 @@ static bool parse_request_header(qd_http2_decoder_t *decoder, const uint8_t **da
parser_error(decoder, "scratch buffer size exceeded 65535 bytes, stopping decoder");
return false;
}
decoder->frame_length_processed += bytes_to_copy;
*data += bytes_to_copy;
*length -= bytes_to_copy;
if(decoder->frame_length_processed == decoder->frame_length) {
get_request_headers(decoder);
if((decoder->frame_length_processed + bytes_to_copy) == decoder->frame_length) {
bool header_success = get_request_headers(decoder);
if(!header_success)
return false;
}
if (*length > 0) {
return true; // More bytes remain to be processed, continue processing.
Expand Down
5 changes: 4 additions & 1 deletion tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ set(unit_test_SOURCES
hash_test.c
thread_test.c
platform_test.c
static_assert_test.c
)

add_executable(unit_tests ${unit_test_SOURCES})
Expand Down Expand Up @@ -253,3 +252,7 @@ add_subdirectory(cpp)
if(BUILD_BENCHMARKS)
add_subdirectory(c_benchmarks)
endif()

if (ENABLE_FUZZ_TESTING)
add_subdirectory(fuzz)
endif (ENABLE_FUZZ_TESTING)
56 changes: 56 additions & 0 deletions tests/fuzz/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
add_definitions(${C_STANDARD_FLAGS} ${COMPILE_WARNING_FLAGS})

option(FUZZ_REGRESSION_TESTS "Run fuzz tests with regression test driver" ON)
option(FUZZ_LONG_TESTS "Run fuzz tests that take a long time" OFF)
set(FUZZER AFL CACHE STRING "Fuzzing engine to use") # Set AFL as the default fuzzer
set(FUZZING_LIB_LibFuzzer FuzzingEngine)
set(FUZZING_LIB_AFL -fsanitize=fuzzer)

add_library(StandaloneFuzzTargetMain STATIC StandaloneFuzzTargetMain.c StandaloneFuzzTargetInit.c)

if (FUZZ_REGRESSION_TESTS)
message(STATUS "FUZZ_REGRESSION_TESTS")
set(FUZZING_LIBRARY StandaloneFuzzTargetMain)
else ()
message(STATUS "NO FUZZ_REGRESSION_TESTS")
set(FUZZING_LIBRARY ${FUZZING_LIB_${FUZZER}})
endif ()

macro(add_fuzz_test test)
add_executable (${test} ${ARGN})
target_link_libraries (${test} ${FUZZING_LIBRARY} skupper-router)
set_target_properties(fuzz_http2_decoder PROPERTIES LINKER_LANGUAGE CXX)

if(FUZZ_REGRESSION_TESTS)
file(GLOB_RECURSE files ${CMAKE_CURRENT_SOURCE_DIR}/${test}/*)
unset(file_lines)
foreach(f IN LISTS files)
set(file_lines "${file_lines}${f}\n")
endforeach()
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/${test}-files" "${file_lines}")
add_test(${test} ${TEST_WRAP} ${test} "@${CMAKE_CURRENT_BINARY_DIR}/${test}-files")
else(FUZZ_REGRESSION_TESTS)
add_test(${test} ${TEST_WRAP} ${test} "${CMAKE_CURRENT_SOURCE_DIR}/${test}")
endif(FUZZ_REGRESSION_TESTS)
endmacro(add_fuzz_test test)

add_fuzz_test(fuzz_http2_decoder fuzz_http2_decoder.c)
#add_fuzz_test(fuzz_http1_decoder fuzz_http1_decoder.c)
60 changes: 60 additions & 0 deletions tests/fuzz/Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright 2017 Google 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.
#
################################################################################

FROM gcr.io/oss-fuzz-base/base-builder
RUN apt-get update

# Ensure we work from right python version
# Minimum python version required by qpid-proton and skupper-router is Python 3.9
RUN apt-get install -y python3.9 python3.9-dev && \
ln --force -s /usr/bin/python3.9 /usr/local/bin/python3 && \
apt-get install -y python3-pip
RUN apt-get install -y libuv1-dev wget cmake emacs python3-dev libwebsockets-dev libtool zlib1g-dev cmake libsasl2-dev libssl-dev sasl2-bin libnghttp2-dev

# LibwebSockets library is required by skupper-router
# We are using v4.2-stable instead v4.3-stable because of this lws compilation error - https://github.com/warmcat/libwebsockets/issues/3163
RUN git clone https://github.com/warmcat/libwebsockets.git --branch v4.2-stable
WORKDIR /src
RUN mkdir libwebsockets/build && cd libwebsockets/build && cmake .. -DLWS_LINK_TESTAPPS_DYNAMIC=ON -DLWS_WITH_LIBUV=OFF -DLWS_WITHOUT_BUILTIN_GETIFADDRS=ON -DLWS_WITHOUT_BUILTIN_SHA1=ON -DLWS_WITH_STATIC=OFF -DLWS_IPV6=ON -DLWS_WITH_HTTP2=OFF -DLWS_WITHOUT_CLIENT=OFF -DLWS_WITHOUT_SERVER=OFF -DLWS_WITHOUT_TESTAPPS=ON -DLWS_WITHOUT_TEST_SERVER=ON -DLWS_WITHOUT_TEST_SERVER_EXTPOLL=ON -DLWS_WITHOUT_TEST_PING=ON -DLWS_WITHOUT_TEST_CLIENT=ON && make install

RUN git clone https://github.com/apache/qpid-proton.git
WORKDIR /src/qpid-proton
RUN mkdir build && cd build && cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF -DENABLE_LINKTIME_OPTIMIZATION=OFF -DBUILD_TLS=ON -DSSL_IMPL=openssl -DBUILD_TOOLS=OFF -DBUILD_EXAMPLES=OFF -DBUILD_TESTING=OFF && make install

WORKDIR /src
RUN git clone https://github.com/ganeshmurthy/skupper-router.git --branch FUZZ-TESTING

WORKDIR /src/skupper-router

# refresh the build directory if it exists already
RUN rm build -rf || true

# /usr/local/bin/compile compiles libFuzzer or AmericanFuzzyLop(afl), then calls /src/build.sh and sets correct environment variables for it
RUN echo cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF -DFUZZ_REGRESSION_TESTS=OFF -DCMAKE_C_FLAGS=-DQD_MEMORY_DEBUG -DRUNTIME_CHECK=asan > /src/build.sh

# build and run the test. Choose AFL for fuzzer
RUN mkdir build
WORKDIR /src/skupper-router/build
RUN FUZZING_LANGUAGE='' FUZZING_ENGINE=afl /usr/local/bin/compile
WORKDIR /src/skupper-router/build/tests/fuzz
RUN make

# This starts the AFL fuzzer that runs in an infinite loop. Let it run for about 30 minutes and press Ctrl + C to kill it.
# The AFL program displays the stats on stdout upon termination.
# Uncomment ONE of the following lines to start the AFL fuzzer for the http2 or the http1 decoder. Before starting the AFL fuzzer, make sure you have some seed datta in the respective corpus folders.
#ENTRYPOINT LD_LIBRARY_PATH=/usr/local/lib/clang/18/lib/x86_64-unknown-linux-gnu/ AFL_MAP_SIZE=10000000 AFL_DEBUG=1 AFL_SKIP_CPUFREQ=1 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 afl-fuzz -i /src/skupper-router/tests/fuzz/fuzz_http2_decoder/corpus/ -o findings_dir /src/skupper-router/build/tests/fuzz/fuzz_http2_decoder; bash
#ENTRYPOINT LD_LIBRARY_PATH=/usr/local/lib/clang/18/lib/x86_64-unknown-linux-gnu/ AFL_MAP_SIZE=10000000 AFL_DEBUG=1 AFL_SKIP_CPUFREQ=1 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 afl-fuzz -i /src/skupper-router/tests/fuzz/fuzz_http1_decoder/corpus/ -o findings_dir /src/skupper-router/build/tests/fuzz/fuzz_http1_decoder; bash
CMD ["/bin/bash"]
31 changes: 31 additions & 0 deletions tests/fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Fuzz testing http1 and http2 adaptors in skupper-router

##Basics -
For a glossary of fuzzing terms please see - https://github.com/google/fuzzing/blob/master/docs/glossary.md
libFuzzer(https://llvm.org/docs/LibFuzzer.html) and AFL(https://github.com/google/AFL) are popular fuzzing engines that can be used for fuzz testing the http1 and the http2 adaptors.
OSS-Fuzz (https://github.com/google/oss-fuzz) supports both libFuzzer and AFL.

##skupper-router/tests/fuzz/CMakeLists.txt
The CMakeLists.txt by defaults the fuzzing engine to AFL.
**set(FUZZER AFL CACHE STRING "Fuzzing engine to use")**
You can change the default fuzzing engine to libFuzzer like this -
**set(FUZZER LibFuzzer CACHE STRING "Fuzzing engine to use")**
The StandaloneFuzzTargetMain is set as the FUZZING_LIBRARY when regression testing is required (FUZZ_REGRESSION_TESTS=ON)
The regression tests are run as part of the skupper-router test suite in github CI.
The corpus files used by the regression tests are generated by running the fuzzer of your choice inside the container built using the Containerfile which is found in the skupper-router/tests/fuzz folder.

##Containerfile
skupper-router/tests/fuzzContainerfile creates an environment where you can run the fuzzer of your choice. You will need to have seed corpus files which the fuzzer will use and build upon to create numerous additional corpus files.
If the code crashes, the input that led to the crash is saved. The crash files and the corpus files are downloaded from the container and used in regression testing.

##Building and running Containerfile
To build the Containerfile from the skupper-router/tests/fuzz/folder
podman build -t oss-fuzz/skupper-router --file=Containerfile .
To run the container
podman run --net host -i -t oss-fuzz/skupper-router

Notice in the Containerfile the two commented ENTRYPOINT lines.
Once you are inside the container, run the AFL fuzzer as seen in the commented ENTRYPOINT lines. For example, to run the http2 fuzzing -
LD_LIBRARY_PATH=/usr/local/lib/clang/18/lib/x86_64-unknown-linux-gnu/ AFL_MAP_SIZE=10000000 AFL_DEBUG=1 AFL_SKIP_CPUFREQ=1 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 afl-fuzz -i /src/skupper-router/tests/fuzz/fuzz_http2_decoder/corpus/ -o findings_dir /src/skupper-router/build/tests/fuzz/fuzz_http2_decoder
Let the fuzzer run for about an hour. Since the fuzzer runs infinitely, to stop the fuzzer, press Ctrl + C. Check for the findings_dir for crashes and additional corpus files. Download the crash and corpus files from the container and run them locally against your code to help fix the crashes.

36 changes: 36 additions & 0 deletions tests/fuzz/StandaloneFuzzTargetInit.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
#include <qpid/dispatch/alloc_pool.h>
#include <qpid/dispatch/log.h>

void qd_log_initialize(void);
void qd_error_initialize(void);

#include "libFuzzingEngine.h"

int LLVMFuzzerInitialize(int *argc, char ***argv)
{
qd_alloc_initialize();
qd_log_initialize();
qd_error_initialize();
return 0;
}

Loading

0 comments on commit de72015

Please sign in to comment.