diff --git a/.gitignore b/.gitignore index 7590faf..af8d40a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ build/ +fuzz/corpus +fuzz/logs .vs/ .vscode/settings.json CMakeSettings.json diff --git a/.vscode/launch.json b/.vscode/launch.json index 9a85195..29baf54 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,6 +17,14 @@ "cwd": "${workspaceRoot}", "args": [] }, + { + "type": "cppdbg", + "request": "launch", + "name": "fuzzer (lldb)", + "program": "${workspaceFolder}/fuzz/build/cppdap-fuzzer", + "cwd": "${workspaceRoot}", + "args": [] + }, { "name": "unittests (gdb)", "type": "cppdbg", diff --git a/CMakeLists.txt b/CMakeLists.txt index 3231185..2e2f28f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,7 @@ endfunction() option_if_not_defined(CPPDAP_WARNINGS_AS_ERRORS "Treat warnings as errors" OFF) option_if_not_defined(CPPDAP_BUILD_EXAMPLES "Build example applications" OFF) option_if_not_defined(CPPDAP_BUILD_TESTS "Build tests" OFF) +option_if_not_defined(CPPDAP_BUILD_FUZZER "Build fuzzer" OFF) option_if_not_defined(CPPDAP_ASAN "Build dap with address sanitizer" OFF) option_if_not_defined(CPPDAP_MSAN "Build dap with memory sanitizer" OFF) option_if_not_defined(CPPDAP_TSAN "Build dap with thread sanitizer" OFF) @@ -223,6 +224,26 @@ if(CPPDAP_BUILD_TESTS) target_link_libraries(cppdap-unittests cppdap "${CPPDAP_OS_LIBS}") endif(CPPDAP_BUILD_TESTS) +# fuzzer +if(CPPDAP_BUILD_FUZZER) + if (NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + message(FATAL_ERROR "CPPDAP_BUILD_FUZZER can currently only be used with the clang toolchain") + endif() + set(DAP_FUZZER_LIST + ${CPPDAP_LIST} + ${CMAKE_CURRENT_SOURCE_DIR}/fuzz/fuzz.cpp + ) + add_executable(cppdap-fuzzer ${DAP_FUZZER_LIST}) + target_compile_options(cppdap-fuzzer PUBLIC "-fsanitize=fuzzer,address") + target_link_libraries(cppdap-fuzzer "-fsanitize=fuzzer,address") + target_include_directories(cppdap-fuzzer PUBLIC + ${CPPDAP_INCLUDE_DIR} + ${CPPDAP_SRC_DIR} + ${CPPDAP_JSON_DIR}/include/ + ) + target_link_libraries(cppdap-fuzzer cppdap "${CPPDAP_OS_LIBS}") +endif(CPPDAP_BUILD_FUZZER) + # examples if(CPPDAP_BUILD_EXAMPLES) function(build_example target) diff --git a/fuzz/dictionary.txt b/fuzz/dictionary.txt new file mode 100644 index 0000000..bb30ef9 --- /dev/null +++ b/fuzz/dictionary.txt @@ -0,0 +1,225 @@ +"accessType" +"adapterData" +"adapterID" +"additionalModuleColumns" +"address" +"addressRange" +"algorithm" +"allThreadsContinued" +"allThreadsStopped" +"args" +"arguments" +"attach" +"attributeName" +"attributes" +"breakMode" +"breakpoint" +"breakpointLocations" +"breakpoints" +"cancel" +"cancellable" +"capabilities" +"category" +"checksum" +"checksums" +"clientID" +"clientName" +"column" +"columnsStartAt1" +"command" +"completions" +"completionTriggerCharacters" +"condition" +"configurationDone" +"context" +"continue" +"continued" +"count" +"cwd" +"data" +"dataBreakpointInfo" +"dataId" +"dateTimeStamp" +"default" +"description" +"disassemble" +"disconnect" +"endColumn" +"endLine" +"env" +"evaluate" +"evaluateName" +"exceptionBreakpointFilters" +"exceptionInfo" +"exceptionOptions" +"exitCode" +"exited" +"expensive" +"expression" +"expression" +"filter" +"filter" +"filters" +"format" +"frameId" +"fullTypeName" +"goto" +"gotoTargets" +"group" +"hex" +"hitCondition" +"id" +"includeAll" +"indexedVariables" +"indexedVariables" +"initialized" +"innerException" +"instruction" +"instructionBytes" +"instructionCount" +"instructionOffset" +"instructionPointerReference" +"isLocalProcess" +"isOptimized" +"isUserCode" +"kind" +"label" +"launch" +"length" +"levels" +"line" +"lines" +"linesStartAt1" +"loadedSource" +"loadedSources" +"locale" +"location" +"logMessage" +"memoryReference" +"message" +"module" +"moduleCount" +"moduleId" +"modules" +"name" +"namedVariables" +"names" +"negate" +"next" +"noDebug" +"offset" +"origin" +"output" +"parameterNames" +"parameters" +"parameterTypes" +"parameterValues" +"path" +"pathFormat" +"pause" +"percentage" +"pointerSize" +"presentationHint" +"preserveFocusHint" +"process" +"progressEnd" +"progressId" +"progressStart" +"progressUpdate" +"readMemory" +"reason" +"requestId" +"resolveSymbols" +"restart" +"restartFrame" +"reverseContinue" +"runInTerminal" +"scopes" +"selectionLength" +"selectionStart" +"sendTelemetry" +"seq" +"setBreakpoints" +"setDataBreakpoints" +"setExceptionBreakpoints" +"setExpression" +"setFunctionBreakpoints" +"setVariable" +"showUser" +"sortText" +"source" +"sourceModified" +"sourceReference" +"sources" +"stackTrace" +"start" +"startFrame" +"startMethod" +"startModule" +"stepBack" +"stepIn" +"stepInTargets" +"stepOut" +"stopped" +"supportedChecksumAlgorithms" +"supportsBreakpointLocationsRequest" +"supportsCancelRequest" +"supportsClipboardContext" +"supportsCompletionsRequest" +"supportsConditionalBreakpoints" +"supportsConfigurationDoneRequest" +"supportsDataBreakpoints" +"supportsDelayedStackTraceLoading" +"supportsDisassembleRequest" +"supportsEvaluateForHovers" +"supportsExceptionInfoRequest" +"supportsExceptionOptions" +"supportsFunctionBreakpoints" +"supportsGotoTargetsRequest" +"supportsHitConditionalBreakpoints" +"supportsLoadedSourcesRequest" +"supportsLogPoints" +"supportsMemoryReferences" +"supportsModulesRequest" +"supportsProgressReporting" +"supportsReadMemoryRequest" +"supportsRestartFrame" +"supportsRestartRequest" +"supportsRunInTerminalRequest" +"supportsSetExpression" +"supportsSetVariable" +"supportsStepBack" +"supportsStepInTargetsRequest" +"supportsTerminateRequest" +"supportsTerminateThreadsRequest" +"supportsValueFormattingOptions" +"supportsVariablePaging" +"supportsVariableType" +"supportTerminateDebuggee" +"symbol" +"symbolFilePath" +"symbolStatus" +"systemProcessId" +"targetId" +"terminate" +"terminated" +"terminateDebuggee" +"terminateThreads" +"text" +"thread" +"threadId" +"threadIds" +"threads" +"title" +"title" +"type" +"typeName" +"url" +"urlLabel" +"value" +"variables" +"variablesReference" +"verified" +"version" +"visibility" +"width" \ No newline at end of file diff --git a/fuzz/fuzz.cpp b/fuzz/fuzz.cpp new file mode 100644 index 0000000..6f459be --- /dev/null +++ b/fuzz/fuzz.cpp @@ -0,0 +1,173 @@ +// Copyright 2020 Google LLC +// +// 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 +// +// https://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. + +// cppdap fuzzer program. +// Run with: ${CPPDAP_PATH}/fuzz/run.sh +// Requires modern clang toolchain. + +#include "content_stream.h" +#include "string_buffer.h" + +#include "dap/protocol.h" +#include "dap/session.h" + +#include +#include + +namespace { + +// Event provides a basic wait and signal synchronization primitive. +class Event { + public: + // wait() blocks until the event is fired or the given timeout is reached. + template + inline void wait(const DURATION& duration) { + std::unique_lock lock(mutex); + cv.wait_for(lock, duration, [&] { return fired; }); + } + + // fire() sets signals the event, and unblocks any calls to wait(). + inline void fire() { + std::unique_lock lock(mutex); + fired = true; + cv.notify_all(); + } + + private: + std::mutex mutex; + std::condition_variable cv; + bool fired = false; +}; + +} // namespace + +// List of requests that we handle for fuzzing. +#define DAP_REQUEST_LIST() \ + DAP_REQUEST(dap::AttachRequest, dap::AttachResponse) \ + DAP_REQUEST(dap::BreakpointLocationsRequest, \ + dap::BreakpointLocationsResponse) \ + DAP_REQUEST(dap::CancelRequest, dap::CancelResponse) \ + DAP_REQUEST(dap::CompletionsRequest, dap::CompletionsResponse) \ + DAP_REQUEST(dap::ConfigurationDoneRequest, dap::ConfigurationDoneResponse) \ + DAP_REQUEST(dap::ContinueRequest, dap::ContinueResponse) \ + DAP_REQUEST(dap::DataBreakpointInfoRequest, dap::DataBreakpointInfoResponse) \ + DAP_REQUEST(dap::DisassembleRequest, dap::DisassembleResponse) \ + DAP_REQUEST(dap::DisconnectRequest, dap::DisconnectResponse) \ + DAP_REQUEST(dap::EvaluateRequest, dap::EvaluateResponse) \ + DAP_REQUEST(dap::ExceptionInfoRequest, dap::ExceptionInfoResponse) \ + DAP_REQUEST(dap::GotoRequest, dap::GotoResponse) \ + DAP_REQUEST(dap::GotoTargetsRequest, dap::GotoTargetsResponse) \ + DAP_REQUEST(dap::InitializeRequest, dap::InitializeResponse) \ + DAP_REQUEST(dap::LaunchRequest, dap::LaunchResponse) \ + DAP_REQUEST(dap::LoadedSourcesRequest, dap::LoadedSourcesResponse) \ + DAP_REQUEST(dap::ModulesRequest, dap::ModulesResponse) \ + DAP_REQUEST(dap::NextRequest, dap::NextResponse) \ + DAP_REQUEST(dap::PauseRequest, dap::PauseResponse) \ + DAP_REQUEST(dap::ReadMemoryRequest, dap::ReadMemoryResponse) \ + DAP_REQUEST(dap::RestartFrameRequest, dap::RestartFrameResponse) \ + DAP_REQUEST(dap::RestartRequest, dap::RestartResponse) \ + DAP_REQUEST(dap::ReverseContinueRequest, dap::ReverseContinueResponse) \ + DAP_REQUEST(dap::RunInTerminalRequest, dap::RunInTerminalResponse) \ + DAP_REQUEST(dap::ScopesRequest, dap::ScopesResponse) \ + DAP_REQUEST(dap::SetBreakpointsRequest, dap::SetBreakpointsResponse) \ + DAP_REQUEST(dap::SetDataBreakpointsRequest, dap::SetDataBreakpointsResponse) \ + DAP_REQUEST(dap::SetExceptionBreakpointsRequest, \ + dap::SetExceptionBreakpointsResponse) \ + DAP_REQUEST(dap::SetExpressionRequest, dap::SetExpressionResponse) \ + DAP_REQUEST(dap::SetFunctionBreakpointsRequest, \ + dap::SetFunctionBreakpointsResponse) \ + DAP_REQUEST(dap::SetVariableRequest, dap::SetVariableResponse) \ + DAP_REQUEST(dap::SourceRequest, dap::SourceResponse) \ + DAP_REQUEST(dap::StackTraceRequest, dap::StackTraceResponse) \ + DAP_REQUEST(dap::StepBackRequest, dap::StepBackResponse) \ + DAP_REQUEST(dap::StepInRequest, dap::StepInResponse) \ + DAP_REQUEST(dap::StepInTargetsRequest, dap::StepInTargetsResponse) \ + DAP_REQUEST(dap::StepOutRequest, dap::StepOutResponse) \ + DAP_REQUEST(dap::TerminateRequest, dap::TerminateResponse) \ + DAP_REQUEST(dap::TerminateThreadsRequest, dap::TerminateThreadsResponse) \ + DAP_REQUEST(dap::ThreadsRequest, dap::ThreadsResponse) \ + DAP_REQUEST(dap::VariablesRequest, dap::VariablesResponse) + +// Fuzzing main function. +// See http://llvm.org/docs/LibFuzzer.html for details. +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + // The first byte can optionally control fuzzing mode. + enum class ControlMode { + // Don't wrap the input data with a stream writer. Allows testing for stream + // writing. + TestStreamWriter, + + // Don't append a 'done' request. This may cause the test to take longer to + // complete (it may have to block on a timeout), but exercises the + // unrecognised-message cases. + DontAppendDoneRequest, + + // Number of control modes in this enum. + Count, + }; + + // Scan first byte for control mode. + bool useContentStreamWriter = true; + bool appendDoneRequest = true; + if (size > 0 && data[0] < static_cast(ControlMode::Count)) { + useContentStreamWriter = + data[0] != static_cast(ControlMode::TestStreamWriter); + appendDoneRequest = + data[0] != static_cast(ControlMode::DontAppendDoneRequest); + data++; + size--; + } + + // in contains the input data + auto in = std::make_shared(); + + dap::ContentWriter writer(in); + if (useContentStreamWriter) { + writer.write(std::string(reinterpret_cast(data), size)); + } else { + in->write(data, size); + } + + if (appendDoneRequest) { + writer.write(R"( + { + "seq": 10, + "type": "request", + "command": "done", + } + )"); + } + + // Each test is done if we receive a request, or report an error. + Event requestOrError; + +#define DAP_REQUEST(REQUEST, RESPONSE) \ + session->registerHandler([&](const REQUEST&) { \ + requestOrError.fire(); \ + return RESPONSE{}; \ + }); + + auto session = dap::Session::create(); + DAP_REQUEST_LIST(); + + session->onError([&](const char*) { requestOrError.fire(); }); + + auto out = std::make_shared(); + session->bind(dap::ReaderWriter::create(in, out)); + + // Give up after a second if we don't get a request or error reported. + requestOrError.wait(std::chrono::seconds(1)); + + return 0; +} \ No newline at end of file diff --git a/fuzz/run.sh b/fuzz/run.sh new file mode 100755 index 0000000..2d54039 --- /dev/null +++ b/fuzz/run.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e # Fail on any error. + +FUZZ_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )" +cd ${FUZZ_DIR} + +# Ensure we're testing with latest build +[ ! -d "build" ] && mkdir "build" +cd "build" +cmake ../.. -GNinja -DCPPDAP_BUILD_FUZZER=1 -DCMAKE_BUILD_TYPE=RelWithDebInfo +ninja + +cd ${FUZZ_DIR} +[ ! -d "corpus" ] && mkdir "corpus" +[ ! -d "logs" ] && mkdir "logs" +cd "logs" +rm crash-* fuzz-* || true +${FUZZ_DIR}/build/cppdap-fuzzer ${FUZZ_DIR}/corpus ${FUZZ_DIR}/seed -dict=${FUZZ_DIR}/dictionary.txt -jobs=128 diff --git a/fuzz/seed/empty_json b/fuzz/seed/empty_json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/fuzz/seed/empty_json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/fuzz/seed/request b/fuzz/seed/request new file mode 100644 index 0000000..7a183ad --- /dev/null +++ b/fuzz/seed/request @@ -0,0 +1,8 @@ +{ + "seq": 153, + "type": "request", + "command": "next", + "arguments": { + "threadId": 3 + } +} \ No newline at end of file diff --git a/include/dap/io.h b/include/dap/io.h index e1f378d..22a638f 100644 --- a/include/dap/io.h +++ b/include/dap/io.h @@ -52,6 +52,7 @@ class Writer : virtual public Closable { // ReaderWriter is an interface that combines the Reader and Writer interfaces. class ReaderWriter : public Reader, public Writer { + public: // create() returns a ReaderWriter that delegates the interface methods on to // the provided Reader and Writer. // isOpen() returns true if the Reader and Writer both return true for