From ad2b83903ce5ba6cf304ebc05ac32bb9ca771530 Mon Sep 17 00:00:00 2001 From: Flor Elisa Chacon Ochoa Date: Fri, 19 Jun 2026 22:14:21 -0700 Subject: [PATCH 1/2] Add fuzzing for WSLC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce libFuzzer/ASAN-based fuzzing for WSLC, run continuously via OneFuzz in the nightly pipeline. Adds three harnesses covering the CLI argument parser, the C SDK API, and the WinRT API. Harnesses (src/windows/fuzzing/): - WslcCliArgumentFuzzing: exercises ParseArgumentsStateMachine - WslcSdkFuzzing: exercises the C SDK session/container flow - WslcWinRtFuzzing: same, but using the WinRT projection - FuzzingHarness.h: shared FuzzInput reader plus a replay main (gated on WSL_ENABLE_FUZZING_REPLAY) for reproducing crashes locally - generate-seeds.ps1: deterministic seed corpus generator, used for local replay; SDK and WinRT share the same binary format - OneFuzzConfig.json.in, setup.ps1: OneFuzz config and VM setup Build (WSL_BUILD_FUZZING): - Compiles the tree with /fsanitize=address so the service and SDK data paths are instrumented; harness targets add /fsanitize=fuzzer - Drops /guard:cf, /Qspectre, /CETCOMPAT (incompatible with ASAN) - wslinstall is an MSI custom action that can't easily load the ASAN runtime at install time. It isn't fuzzed, so build it without ASAN. - Resolves clang_rt.asan_dynamic next to the compiler and ships it into INSTALLDIR so instrumented binaries can load the runtime; the path is also exported as a pipeline variable to stage next to the harnesses. Pipeline (.pipelines/fuzzing-stage.yml): builds the harnesses and MSI, stages the shared runtime, and submits each target to OneFuzz. Seeding is left unwired for now (libFuzzer starts from an empty corpus); a SeedCorpusContainer can be added later. Local dev notes added to README.md and UserConfig.cmake.sample. Fully vibe-coded with Copilot 🤖 --- .pipelines/fuzzing-stage.yml | 76 +++++++++ .pipelines/wsl-build-nightly-onebranch.yml | 2 + CMakeLists.txt | 50 +++++- UserConfig.cmake.sample | 9 + msipackage/CMakeLists.txt | 7 + msipackage/package.wix.in | 4 + src/windows/common/CMakeLists.txt | 25 ++- src/windows/fuzzing/CMakeLists.txt | 69 ++++++++ src/windows/fuzzing/FuzzingHarness.h | 107 ++++++++++++ src/windows/fuzzing/OneFuzzConfig.json.in | 6 + .../fuzzing/OneFuzzConfigEntry.json.in | 30 ++++ src/windows/fuzzing/README.md | 97 +++++++++++ .../fuzzing/WslcCliArgumentFuzzing.cpp | 105 ++++++++++++ src/windows/fuzzing/WslcSdkFuzzing.cpp | 158 ++++++++++++++++++ src/windows/fuzzing/WslcWinRtFuzzing.cpp | 98 +++++++++++ src/windows/fuzzing/generate-seeds.ps1 | 118 +++++++++++++ src/windows/fuzzing/setup.ps1 | 18 ++ src/windows/wslc/CMakeLists.txt | 10 +- src/windows/wslinstall/CMakeLists.txt | 14 +- 19 files changed, 986 insertions(+), 17 deletions(-) create mode 100644 .pipelines/fuzzing-stage.yml create mode 100644 src/windows/fuzzing/CMakeLists.txt create mode 100644 src/windows/fuzzing/FuzzingHarness.h create mode 100644 src/windows/fuzzing/OneFuzzConfig.json.in create mode 100644 src/windows/fuzzing/OneFuzzConfigEntry.json.in create mode 100644 src/windows/fuzzing/README.md create mode 100644 src/windows/fuzzing/WslcCliArgumentFuzzing.cpp create mode 100644 src/windows/fuzzing/WslcSdkFuzzing.cpp create mode 100644 src/windows/fuzzing/WslcWinRtFuzzing.cpp create mode 100644 src/windows/fuzzing/generate-seeds.ps1 create mode 100644 src/windows/fuzzing/setup.ps1 diff --git a/.pipelines/fuzzing-stage.yml b/.pipelines/fuzzing-stage.yml new file mode 100644 index 0000000000..eb37a97986 --- /dev/null +++ b/.pipelines/fuzzing-stage.yml @@ -0,0 +1,76 @@ +# Fuzzing stage — builds fuzz harnesses with ASAN/SanCov and submits to OneFuzz. + +parameters: + - name: fuzzTargets + type: object + default: + - WslcCliArgumentFuzzing + - WslcSdkFuzzing + - WslcWinRtFuzzing + +stages: + - stage: fuzzing + dependsOn: [] + jobs: + - job: fuzzing_x64 + displayName: "Build and submit WSLC fuzz targets (x64)" + timeoutInMinutes: 120 + pool: + type: windows + + variables: + ob_outputDirectory: '$(Build.SourcesDirectory)\out' + fuzzingBinDir: '$(Build.SourcesDirectory)\bin\x64\Release' + fuzzingStagingDir: '$(ob_outputDirectory)\fuzzing' + + steps: + - task: CMake@1 + displayName: "CMake configure (fuzzing)" + inputs: + workingDirectory: "." + cmakeArgs: . --fresh -A x64 -DCMAKE_BUILD_TYPE=Release -DCMAKE_SYSTEM_VERSION=10.0.26100.0 -DWSL_BUILD_FUZZING=true + + # Build the fuzzing targets, and the MSI for setting up the VMs + - task: CMake@1 + displayName: "CMake build (fuzzing)" + inputs: + workingDirectory: "." + cmakeArgs: --build . --config Release --target ${{ join(' ', parameters.fuzzTargets) }} msipackage -- -m + + - ${{ each target in parameters.fuzzTargets }}: + - task: PowerShell@2 + displayName: "Stage ${{ target }} binaries" + inputs: + targetType: inline + script: | + $binDir = "$(fuzzingBinDir)" + $dropDir = "$(fuzzingStagingDir)" + New-Item -ItemType Directory -Path $dropDir -Force + Copy-Item "$binDir\${{ target }}.*" $dropDir + + - task: PowerShell@2 + displayName: "Stage shared fuzzing artifacts" + inputs: + targetType: inline + script: | + $binDir = "$(fuzzingBinDir)" + $dropDir = "$(fuzzingStagingDir)" + + # CMake places OneFuzzConfig.json, setup.ps1, and per-harness extra dependencies + # (e.g. wslcsdk.dll/pdb) under the fuzzing output dir. + Copy-Item "$binDir\fuzzing\*" $dropDir -Force + + # Shared runtime deps. The ASAN runtime path was exported by the CMake configure + # step as the asanRuntimeDll job variable. + Copy-Item "$binDir\wsl.msi" $dropDir + Copy-Item "$(asanRuntimeDll)" $dropDir -ErrorAction Stop + + Get-ChildItem $dropDir -Recurse | Format-Table Name, Length + + - task: onefuzz-task@0 + displayName: "Submit to OneFuzz" + inputs: + onefuzzOSes: 'Windows' + env: + onefuzzDropDirectory: $(fuzzingStagingDir) + SYSTEM_ACCESSTOKEN: $(System.AccessToken) diff --git a/.pipelines/wsl-build-nightly-onebranch.yml b/.pipelines/wsl-build-nightly-onebranch.yml index 1434dd233d..efeb0d2a07 100644 --- a/.pipelines/wsl-build-nightly-onebranch.yml +++ b/.pipelines/wsl-build-nightly-onebranch.yml @@ -47,6 +47,8 @@ extends: vsoOrg: microsoft vsoProject: Microsoft.WSL + - template: fuzzing-stage.yml@self + - template: test-stage.yml@self parameters: rs_prerelease_only: false diff --git a/CMakeLists.txt b/CMakeLists.txt index c88fbb8e90..4a6b1db852 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,6 +179,10 @@ if (NOT DEFINED WSL_INCLUDE_SDK_CSHARP) set(WSL_INCLUDE_SDK_CSHARP false) endif () +if (NOT DEFINED WSL_BUILD_FUZZING) + set(WSL_BUILD_FUZZING false) +endif () + find_commit_hash(COMMIT_HASH) if (NOT PACKAGE_VERSION) @@ -314,18 +318,48 @@ endif () string(REPLACE "/Zi" "" CMAKE_CXX_FLAGS_DEBUG ${CMAKE_CXX_FLAGS_DEBUG}) # make sure /Zi is removed from the debug flags set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj /W3 /WX /ZH:SHA_256") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /Z7 -DDEBUG -DDBG") -set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /Zi /guard:cf /Qspectre") +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /Zi") + +if (WSL_BUILD_FUZZING) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address") + add_compile_definitions(_DISABLE_VECTOR_ANNOTATION _DISABLE_STRING_ANNOTATION) + + # All instrumented binaries import the dynamic ASAN runtime. + # We resolve its path so that we can include it in the MSI; we can find it next to the compiler. + # We also export its path as a pipeline variable so we can find it there. + if (${TARGET_PLATFORM} STREQUAL "arm64") + set(ASAN_ARCH "aarch64") + else() + set(ASAN_ARCH "x86_64") + endif() + get_filename_component(MSVC_BIN_DIR ${CMAKE_CXX_COMPILER} DIRECTORY) + set(ASAN_RUNTIME_DLL ${MSVC_BIN_DIR}/clang_rt.asan_dynamic-${ASAN_ARCH}.dll) + execute_process(COMMAND ${CMAKE_COMMAND} -E echo "##vso[task.setvariable variable=asanRuntimeDll]${ASAN_RUNTIME_DLL}") +endif () + +if (WSL_ENABLE_FUZZING_REPLAY) + add_compile_definitions(WSL_ENABLE_FUZZING_REPLAY) +endif () # Linker flags -set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /debug:full /debugtype:cv,fixup /guard:cf /DYNAMICBASE") -set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /debug:full /debugtype:cv,fixup /guard:cf /DYNAMICBASE") +set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /debug:full /debugtype:cv,fixup /DYNAMICBASE") +set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /debug:full /debugtype:cv,fixup /DYNAMICBASE") if (DEFINED SOURCELINK_JSON) set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /SOURCELINK:\"${SOURCELINK_JSON}\"") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SOURCELINK:\"${SOURCELINK_JSON}\"") endif() -if (${TARGET_PLATFORM} STREQUAL "x64") - set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /CETCOMPAT") - set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /CETCOMPAT") + +# When WSL_BUILD_FUZZING is enabled, the entire build is compiled with ASAN. +# /fsanitize=address is incompatible with /guard:cf, /Qspectre, and /CETCOMPAT, +# so those are omitted for fuzzing builds. +if (NOT WSL_BUILD_FUZZING) + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /guard:cf /Qspectre") + set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /guard:cf") + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /guard:cf") + if (${TARGET_PLATFORM} STREQUAL "x64") + set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /CETCOMPAT") + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /CETCOMPAT") + endif() endif() # Common link libraries @@ -554,6 +588,10 @@ if (WSL_BUILD_WSL_SETTINGS) add_subdirectory(src/windows/wslsettings) endif() +if (WSL_BUILD_FUZZING) + add_subdirectory(src/windows/fuzzing) +endif() + add_subdirectory(src/linux/netlinkutil) add_subdirectory(src/linux/mountutil) add_subdirectory(src/linux/plan9) diff --git a/UserConfig.cmake.sample b/UserConfig.cmake.sample index cedc89c3b5..b03d6ae4c9 100644 --- a/UserConfig.cmake.sample +++ b/UserConfig.cmake.sample @@ -56,3 +56,12 @@ endif() # # error - block the commit when formatting issues are found # # fix - automatically fix formatting and re-stage files # set(WSL_PRE_COMMIT_MODE "warn") + +# # Uncomment to build fuzzing targets with ASAN and SanitizerCoverage for OneFuzz. +# # Harness binaries are written to bin/// alongside OneFuzzConfig.json. +# # See src/windows/fuzzing/README.md for details. +# set(WSL_BUILD_FUZZING true) +# +# # Enable fuzzing replay mode: builds harnesses without libFuzzer so they can be run standalone +# # with a corpus file as argument. Useful for reproducing crashes locally. +# set(WSL_ENABLE_FUZZING_REPLAY true) diff --git a/msipackage/CMakeLists.txt b/msipackage/CMakeLists.txt index 99586c9727..f8f76487e6 100644 --- a/msipackage/CMakeLists.txt +++ b/msipackage/CMakeLists.txt @@ -32,6 +32,13 @@ foreach(binary ${LINUX_BINARIES}) list(APPEND BINARIES_DEPENDENCIES "${BIN}/${binary}") endforeach() +# When building with ASAN (fuzzing), the instrumented binaries import the dynamic ASAN runtime. +# Ship it alongside them in INSTALLDIR so they can load at runtime. +# ASAN_RUNTIME_DLL is resolved in the top-level CMakeLists. +if (WSL_BUILD_FUZZING) + list(APPEND BINARIES_DEPENDENCIES ${ASAN_RUNTIME_DLL}) +endif() + if (${WSL_BUILD_THIN_PACKAGE}) set(COMPRESS_PACKAGE "no") else() diff --git a/msipackage/package.wix.in b/msipackage/package.wix.in index 18b79acead..017cec96b7 100644 --- a/msipackage/package.wix.in +++ b/msipackage/package.wix.in @@ -38,6 +38,10 @@ + + + + diff --git a/src/windows/common/CMakeLists.txt b/src/windows/common/CMakeLists.txt index 886e2b6b95..13c7097c78 100644 --- a/src/windows/common/CMakeLists.txt +++ b/src/windows/common/CMakeLists.txt @@ -144,14 +144,29 @@ set(HEADERS WSLCSessionDefaults.h ) -add_library(common STATIC ${SOURCES} ${HEADERS}) -add_dependencies(common wslserviceidl localization wslservicemc wslinstalleridl yaml-cpp) +# Adds a target for the common static library. +# This is factored into a function so that we can have multiple variants with different options. +function(add_common_library TARGET_NAME) + add_library(${TARGET_NAME} STATIC ${SOURCES} ${HEADERS}) + add_dependencies(${TARGET_NAME} wslserviceidl localization wslservicemc wslinstalleridl yaml-cpp) + target_precompile_headers(${TARGET_NAME} PRIVATE precomp.h) + set_target_properties(${TARGET_NAME} PROPERTIES FOLDER windows) + target_include_directories(${TARGET_NAME} PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/../service/mc/${TARGET_PLATFORM}/${CMAKE_BUILD_TYPE}) +endfunction() -target_precompile_headers(common PRIVATE precomp.h) -set_target_properties(common PROPERTIES FOLDER windows) -target_include_directories(common PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/../service/mc/${TARGET_PLATFORM}/${CMAKE_BUILD_TYPE}) +add_common_library(common) # WSLCUserSettings.cpp uses yaml-cpp headers. set_source_files_properties(WSLCUserSettings.cpp PROPERTIES INCLUDE_DIRECTORIES "${yaml-cpp_SOURCE_DIR}/include" COMPILE_DEFINITIONS "YAML_CPP_STATIC_DEFINE") + +# For Fuzzing builds, there are some targets that cannot be built with ASAN instrumentation. +# These targets use a non-instrumented copy of the common library. +if (WSL_BUILD_FUZZING) + # Remove ASAN from all targets in this directory, then add it back only for the base variant. + string(REPLACE "/fsanitize=address" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + target_compile_options(common PRIVATE /fsanitize=address) + + add_common_library(common_noasan) +endif() diff --git a/src/windows/fuzzing/CMakeLists.txt b/src/windows/fuzzing/CMakeLists.txt new file mode 100644 index 0000000000..3e81a78c3c --- /dev/null +++ b/src/windows/fuzzing/CMakeLists.txt @@ -0,0 +1,69 @@ +set(FUZZING_OUT_DIR ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${CMAKE_BUILD_TYPE}/fuzzing) + +function(add_fuzzing_harness TARGET) + cmake_parse_arguments(ARG "" "TARGET_NAME" "LINK_LIBRARIES;EXTRA_DEPENDENCIES;INCLUDE_DIRECTORIES" ${ARGN}) + + add_executable(${TARGET} ${TARGET}.cpp) + target_precompile_headers(${TARGET} REUSE_FROM common) + + if (NOT WSL_ENABLE_FUZZING_REPLAY) + target_compile_options(${TARGET} PRIVATE /fsanitize=fuzzer) + endif() + + if(ARG_LINK_LIBRARIES) + target_link_libraries(${TARGET} PRIVATE ${ARG_LINK_LIBRARIES}) + endif() + + if(ARG_INCLUDE_DIRECTORIES) + target_include_directories(${TARGET} PRIVATE ${ARG_INCLUDE_DIRECTORIES}) + endif() + + set_target_properties(${TARGET} PROPERTIES FOLDER fuzzing) + + if(ARG_TARGET_NAME) + set(TARGET_NAME ${ARG_TARGET_NAME}) + else() + set(TARGET_NAME ${TARGET}) + endif() + + # Add extra dependencies to the entry, and stage them into the shared fuzzing output dir + set(EXTRA_DEPENDENCIES_JSON "") + foreach(dep IN LISTS ARG_EXTRA_DEPENDENCIES) + string(APPEND EXTRA_DEPENDENCIES_JSON ", \"${dep}\"") + + # Dependency file will be in the main binary output dir after build + add_custom_command(TARGET ${TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${CMAKE_BUILD_TYPE}/${dep} + ${FUZZING_OUT_DIR}/${dep}) + endforeach() + + file(READ ${CMAKE_CURRENT_SOURCE_DIR}/OneFuzzConfigEntry.json.in ENTRY_TEMPLATE) + string(CONFIGURE "${ENTRY_TEMPLATE}" ENTRY_JSON) + + # Append this harness's entry to OneFuzzConfig.json (written after all harnesses). + list(APPEND ONEFUZZ_ENTRIES "${ENTRY_JSON}") + set(ONEFUZZ_ENTRIES "${ONEFUZZ_ENTRIES}" PARENT_SCOPE) +endfunction() + +configure_file(setup.ps1 ${FUZZING_OUT_DIR}/setup.ps1 COPYONLY) + +add_fuzzing_harness(WslcCliArgumentFuzzing + TARGET_NAME cliArgumentParser + LINK_LIBRARIES wslclib) + +add_fuzzing_harness(WslcSdkFuzzing + TARGET_NAME sdkApiFlow + LINK_LIBRARIES wslcsdk + INCLUDE_DIRECTORIES ${CMAKE_SOURCE_DIR}/src/windows/WslcSDK + EXTRA_DEPENDENCIES "wslcsdk.dll" "wslcsdk.pdb") + +add_fuzzing_harness(WslcWinRtFuzzing + TARGET_NAME winrtApiFlow + LINK_LIBRARIES wslcsdk + INCLUDE_DIRECTORIES ${CMAKE_SOURCE_DIR}/src/windows/WslcSDK ${CMAKE_BINARY_DIR}/src/windows/WslcSDK/winrt/${TARGET_PLATFORM}/${CMAKE_BUILD_TYPE} + EXTRA_DEPENDENCIES "wslcsdk.dll" "wslcsdk.pdb") + +# Write OneFuzzConfig.json with one entry per harness. +list(JOIN ONEFUZZ_ENTRIES ",\n" ONEFUZZ_ENTRIES) +configure_file(OneFuzzConfig.json.in ${FUZZING_OUT_DIR}/OneFuzzConfig.json) diff --git a/src/windows/fuzzing/FuzzingHarness.h b/src/windows/fuzzing/FuzzingHarness.h new file mode 100644 index 0000000000..92baaa95cf --- /dev/null +++ b/src/windows/fuzzing/FuzzingHarness.h @@ -0,0 +1,107 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +// Shared utilities for fuzzing harnesses. +// Provides: +// * FuzzInput - a cursor over the raw libFuzzer buffer +// * main() - for replaying corpus inputs without the libFuzzer runtime (e.g. for debugging or OneFuzz repros) + +#pragma once + +#include +#include +#include +#include + +// Cursor over fuzz input bytes. Harnesses construct this from LLVMFuzzerTestOneInput args +// and call typed Read methods. All reads are bounds-checked and return default values on exhaustion. +class FuzzInput +{ +public: + FuzzInput(const uint8_t* data, size_t size) : m_data(data), m_size(size), m_offset(0) + { + } + + bool Empty() const + { + return m_offset >= m_size; + } + + size_t Remaining() const + { + return m_offset < m_size ? m_size - m_offset : 0; + } + + // Read a fixed-size value (uint8_t, uint16_t, uint32_t, etc.) + template + T Read() + { + T value{}; + if (m_offset + sizeof(T) <= m_size) + { + std::memcpy(&value, m_data + m_offset, sizeof(T)); + m_offset += sizeof(T); + } + return value; + } + + // Read a null-terminated narrow string (up to maxLen chars) + std::string ReadString(size_t maxLen = 64) + { + std::string result; + while (m_offset < m_size && result.size() < maxLen) + { + char ch = static_cast(m_data[m_offset++]); + if (ch == '\0') + { + break; + } + result.push_back(ch); + } + return result; + } + + // Read a null-terminated wide string (byte pairs as little-endian wchar_t, up to maxLen chars) + std::wstring ReadWideString(size_t maxLen = 64) + { + std::wstring result; + while (m_offset + 1 < m_size && result.size() < maxLen) + { + wchar_t ch = static_cast(m_data[m_offset] | (m_data[m_offset + 1] << 8)); + m_offset += 2; + if (ch == L'\0') + { + break; + } + result.push_back(ch); + } + return result; + } + +private: + const uint8_t* m_data; + size_t m_size; + size_t m_offset; +}; + +#ifdef WSL_ENABLE_FUZZING_REPLAY + +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size); +extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv); + +// Replay mode: run the harness on each argument as a corpus file. +int main(int argc, char** argv) +{ + LLVMFuzzerInitialize(&argc, &argv); + for (int i = 1; i < argc; ++i) + { + std::ifstream stream(argv[i], std::ios_base::in | std::ios_base::binary); + std::string contents((std::istreambuf_iterator(stream)), std::istreambuf_iterator()); + LLVMFuzzerTestOneInput(reinterpret_cast(contents.data()), contents.size()); + } + return 0; +} + +#endif // WSL_ENABLE_FUZZING_REPLAY diff --git a/src/windows/fuzzing/OneFuzzConfig.json.in b/src/windows/fuzzing/OneFuzzConfig.json.in new file mode 100644 index 0000000000..fc8229d8ec --- /dev/null +++ b/src/windows/fuzzing/OneFuzzConfig.json.in @@ -0,0 +1,6 @@ +{ + "ConfigVersion": 3, + "Entries": [ +${ONEFUZZ_ENTRIES} + ] +} diff --git a/src/windows/fuzzing/OneFuzzConfigEntry.json.in b/src/windows/fuzzing/OneFuzzConfigEntry.json.in new file mode 100644 index 0000000000..a6dde8e68e --- /dev/null +++ b/src/windows/fuzzing/OneFuzzConfigEntry.json.in @@ -0,0 +1,30 @@ + { + "JobNotificationEmail": "florch@microsoft.com", + "Skip": false, + "RebootAfterSetup": true, + "Fuzzer": { + "$type": "libfuzzer", + "FuzzingHarnessExecutableName": "${TARGET}.exe" + }, + "OneFuzzJobs": [ + { + "ProjectName": "wslc-fuzzing", + "TargetName": "${TARGET_NAME}" + } + ], + "JobDependencies": [ + "${TARGET}.exe", + "${TARGET}.pdb", + "clang_rt.asan_dynamic*.dll", + "setup.ps1", + "wsl.msi" + ${EXTRA_DEPENDENCIES_JSON} + ], + "AdoTemplate": { + "Org": "microsoft", + "Project": "OS", + "AssignedTo": "florch@microsoft.com", + "AreaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\InstaDev", + "IterationPath": "OS" + } + } \ No newline at end of file diff --git a/src/windows/fuzzing/README.md b/src/windows/fuzzing/README.md new file mode 100644 index 0000000000..6000471438 --- /dev/null +++ b/src/windows/fuzzing/README.md @@ -0,0 +1,97 @@ +# WSLC Fuzzing + +Fuzzing harnesses for WSLC, built with AddressSanitizer (ASAN) and libFuzzer for integration with OneFuzz. + +## How It Works + +When `WSL_BUILD_FUZZING=true`: +- The entire build is compiled with `/fsanitize=address` — ASAN detects memory errors at runtime. +- Fuzzing harness executables get `/fsanitize=fuzzer` — this links the libFuzzer runtime and enables SanitizerCoverage for coverage-guided mutation. + +Reference: https://learn.microsoft.com/cpp/sanitizers/asan-building + +## Adding a New Harness + +1. Create `TargetName.cpp` implementing `LLVMFuzzerTestOneInput` and including `FuzzingHarness.h` +2. Register it in `CMakeLists.txt`: + +```cmake +add_fuzzing_harness(MyNewFuzzing + TARGET_NAME myTarget + LINK_LIBRARIES somelib + EXTRA_DEPENDENCIES "extra.dll" "extra.pdb") +``` + +The function handles compiler flags, PCH, and contributes one entry to `OneFuzzConfig.json`. +The entry is rendered from `OneFuzzConfigEntry.json.in`; the surrounding file comes from `OneFuzzConfig.json.in`. + +## Building + +Enable fuzzing in `UserConfig.cmake`: + +```cmake +set(WSL_BUILD_FUZZING true) +``` + +Then: + +```powershell +cmake . --fresh +cmake --build . --config Release --target WslcCliArgumentFuzzing -- -m +``` + +The fuzzing build cannot be mixed with a non-ASAN build in the same tree. Use `--fresh` when toggling `WSL_BUILD_FUZZING`. + +## Harnesses + +| Harness | Target | Description | +|---------|--------|-------------| +| `WslcCliArgumentFuzzing` | `ParseArgumentsStateMachine` | CLI argument parser — standalone, no service needed | +| `WslcSdkFuzzing` | WSLC SDK C API | Session + container creation flow via C functions | +| `WslcWinRtFuzzing` | WSLC SDK WinRT API | Same flow via C++/WinRT projection | + +## OneFuzz Integration + +The nightly pipeline (`.pipelines/fuzzing-stage.yml`) builds the harnesses + MSI, stages them into a flat drop, and submits them with `onefuzz-task@0`. +`OneFuzzConfig.json` holds one entry per harness; shared dependencies (`setup.ps1`, `wsl.msi`, the ASAN runtime) sit alongside the binaries in the drop. + +### VM Setup + +`setup.ps1` runs on the OneFuzz VM before fuzzing. It enables Hyper-V, enables WSL, and installs `wsl.msi` from the build drop. `RebootAfterSetup` is enabled so features activate before the harness runs. + +### Seed Corpus (optional, not yet wired up) + +The harnesses run without a seed corpus — libFuzzer starts from an empty corpus and generates its own inputs. +Seeds only speed up the initial coverage ramp (most useful for the structured SDK/WinRT +inputs). + +To add seeding later, give each entry a `SeedCorpusContainer` in `OneFuzzConfigEntry.json.in` and upload the generated seeds to that container using the OneFuzz CLI. +`generate-seeds.ps1` produces a corpus per target; the same directories are used for local replay. + +### Drop Layout + +``` +fuzzing/ +├── OneFuzzConfig.json +├── clang_rt.asan_dynamic-x86_64.dll +├── setup.ps1 +├── wsl.msi +├── .exe +├── .pdb +├── +├── ... +``` + +## Local Testing + +To build a standalone replay binary (no libFuzzer runtime), add to your `UserConfig.cmake`: + +```cmake +set(WSL_ENABLE_FUZZING_REPLAY true) +``` + +Then rebuild. The harness will include a `main()` that replays corpus files: + +```powershell +WslcCliArgumentFuzzing.exe path\to\crash-input +``` diff --git a/src/windows/fuzzing/WslcCliArgumentFuzzing.cpp b/src/windows/fuzzing/WslcCliArgumentFuzzing.cpp new file mode 100644 index 0000000000..ce342220fe --- /dev/null +++ b/src/windows/fuzzing/WslcCliArgumentFuzzing.cpp @@ -0,0 +1,105 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +// libFuzzer harness for the WSLC CLI argument parser (ParseArgumentsStateMachine). +// +// This target exercises the state machine that parses all `wslc.exe` command-line +// arguments. It is fully standalone — no COM session or WSL service is required. + +#include "precomp.h" + +#include "arguments/Argument.h" +#include "arguments/ArgumentParser.h" +#include "arguments/ArgumentTypes.h" +#include "core/Invocation.h" +#include "FuzzingHarness.h" + +#include +#include +#include + +using namespace wsl::windows::wslc; +using namespace wsl::windows::wslc::argument; + +// Split remaining fuzz bytes into null-delimited wide strings (argv-style). +static std::vector SplitToArgs(FuzzInput& input) +{ + std::vector args; + while (!input.Empty()) + { + auto s = input.ReadWideString(SIZE_MAX); + if (!s.empty()) + { + args.push_back(std::move(s)); + } + } + return args; +} + +// Build a representative set of arguments covering all Kinds (Flag, Value, Positional, Forward). +static std::vector GetFuzzArguments() +{ + return { + Argument::Create(ArgType::Help), + Argument::Create(ArgType::All), + Argument::Create(ArgType::Detach), + Argument::Create(ArgType::Force), + Argument::Create(ArgType::Interactive), + Argument::Create(ArgType::Quiet), + Argument::Create(ArgType::TTY), + Argument::Create(ArgType::Verbose), + Argument::Create(ArgType::Version), + Argument::Create(ArgType::Name), + Argument::Create(ArgType::Env, std::nullopt, NO_LIMIT), + Argument::Create(ArgType::Volume, std::nullopt, NO_LIMIT), + Argument::Create(ArgType::Publish, std::nullopt, NO_LIMIT), + Argument::Create(ArgType::WorkDir), + Argument::Create(ArgType::User), + Argument::Create(ArgType::Hostname), + Argument::Create(ArgType::Memory), + Argument::Create(ArgType::Signal), + Argument::Create(ArgType::Output), + Argument::Create(ArgType::ContainerId), + Argument::Create(ArgType::ImageId), + Argument::Create(ArgType::Command), + Argument::Create(ArgType::ForwardArgs), + }; +} + +extern "C" int LLVMFuzzerInitialize(int*, char***) +{ + return 0; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if (size == 0 || size > 4096) + { + return -1; + } + + try + { + FuzzInput input{data, size}; + auto args = SplitToArgs(input); + if (args.empty()) + { + return -1; + } + + Invocation inv{std::move(args)}; + ArgMap execArgs; + auto arguments = GetFuzzArguments(); + + ParseArgumentsStateMachine sm{inv, execArgs, std::move(arguments)}; + + while (sm.Step()) + { + } + } + catch (...) + { + // Expected — most fuzzed inputs will fail. ASAN crashes bypass this. + } + + return 0; +} diff --git a/src/windows/fuzzing/WslcSdkFuzzing.cpp b/src/windows/fuzzing/WslcSdkFuzzing.cpp new file mode 100644 index 0000000000..65afc325ef --- /dev/null +++ b/src/windows/fuzzing/WslcSdkFuzzing.cpp @@ -0,0 +1,158 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +// libFuzzer harness for the WSLC SDK C API. +// +// Exercises the session and container settings flow with fuzz-controlled inputs. +// Requires WSL service running on the host (installed via setup.ps1 on OneFuzz VMs). + +#include "precomp.h" + +#include "wslcsdk.h" + +#include +#include +#include +#include + +// Helper to read a null-terminated narrow string from the fuzz buffer. +// Advances offset past the string (including null) or to end of buffer. +static std::string ReadString(const uint8_t* data, size_t size, size_t& offset, size_t maxLen = 64) +{ + std::string result; + while (offset < size && result.size() < maxLen) + { + char ch = static_cast(data[offset++]); + if (ch == '\0') + { + break; + } + result.push_back(ch); + } + return result; +} + +// Helper to read a null-terminated wide string from the fuzz buffer. +static std::wstring ReadWideString(const uint8_t* data, size_t size, size_t& offset, size_t maxLen = 64) +{ + std::wstring result; + while (offset + 1 < size && result.size() < maxLen) + { + wchar_t ch = static_cast(data[offset] | (data[offset + 1] << 8)); + offset += 2; + if (ch == L'\0') + { + break; + } + result.push_back(ch); + } + return result; +} + +template +static T ReadValue(const uint8_t* data, size_t size, size_t& offset) +{ + T value{}; + if (offset + sizeof(T) <= size) + { + std::memcpy(&value, data + offset, sizeof(T)); + offset += sizeof(T); + } + return value; +} + +extern "C" int LLVMFuzzerInitialize(int*, char***) +{ + return 0; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if (size < 10 || size > 4096) + { + return -1; + } + + size_t offset = 0; + + // Read session parameters from corpus + std::wstring sessionName = ReadWideString(data, size, offset); + std::wstring storagePath = ReadWideString(data, size, offset); + + // Initialize session settings + WslcSessionSettings sessionSettings{}; + HRESULT hr = WslcInitSessionSettings(sessionName.c_str(), storagePath.c_str(), &sessionSettings); + if (FAILED(hr)) + { + return -1; + } + + // Configure session with minimal resources + WslcSetSessionSettingsCpuCount(&sessionSettings, 1); + WslcSetSessionSettingsMemory(&sessionSettings, 512); + WslcSetSessionSettingsTimeout(&sessionSettings, 5000); + + // Read container parameters + std::string imageName = ReadString(data, size, offset); + std::string containerName = ReadString(data, size, offset); + std::string hostName = ReadString(data, size, offset); + + // Read port mappings + uint8_t portCount = (offset < size) ? data[offset++] : 0; + std::vector ports(portCount); + for (uint8_t i = 0; i < portCount; ++i) + { + ports[i].windowsPort = ReadValue(data, size, offset); + ports[i].containerPort = ReadValue(data, size, offset); + ports[i].protocol = static_cast(ReadValue(data, size, offset) % 2); + } + + // Read flags + auto containerFlags = static_cast(ReadValue(data, size, offset)); + + // Attempt to create session — may fail if service isn't available + WslcSession session = nullptr; + hr = WslcCreateSession(&sessionSettings, &session, nullptr); + if (FAILED(hr) || !session) + { + return 0; + } + + // Initialize container settings + WslcContainerSettings containerSettings{}; + hr = WslcInitContainerSettings(imageName.c_str(), &containerSettings); + if (FAILED(hr)) + { + WslcTerminateSession(session); + WslcReleaseSession(session); + return 0; + } + + if (!containerName.empty()) + { + WslcSetContainerSettingsName(&containerSettings, containerName.c_str()); + } + if (!hostName.empty()) + { + WslcSetContainerSettingsHostName(&containerSettings, hostName.c_str()); + } + if (portCount > 0) + { + WslcSetContainerSettingsPortMappings(&containerSettings, ports.data(), portCount); + } + WslcSetContainerSettingsFlags(&containerSettings, containerFlags); + + // Attempt container creation — exercises validation and service communication + WslcContainer container = nullptr; + hr = WslcCreateContainer(session, &containerSettings, &container, nullptr); + if (SUCCEEDED(hr) && container) + { + WslcReleaseContainer(container); + } + + WslcTerminateSession(session); + WslcReleaseSession(session); + + return 0; +} + +#include "FuzzingHarness.h" diff --git a/src/windows/fuzzing/WslcWinRtFuzzing.cpp b/src/windows/fuzzing/WslcWinRtFuzzing.cpp new file mode 100644 index 0000000000..2efb446aba --- /dev/null +++ b/src/windows/fuzzing/WslcWinRtFuzzing.cpp @@ -0,0 +1,98 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +// libFuzzer harness for the WSLC SDK WinRT API. +// +// Exercises the WinRT projection of the session and container settings flow +// with fuzz-controlled inputs. Requires WSL service running on the host. + +#include "precomp.h" + +#include +#include +#include +#include + +#include "FuzzingHarness.h" + +#include +#include + +using namespace winrt; +using namespace winrt::Microsoft::WSL::Containers; +using namespace winrt::Windows::Foundation; + +extern "C" int LLVMFuzzerInitialize(int*, char***) +{ + winrt::init_apartment(); + return 0; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if (size < 10 || size > 4096) + { + return -1; + } + + FuzzInput input{data, size}; + + try + { + // Read session parameters from corpus + std::wstring sessionName = input.ReadWideString(); + std::wstring storagePath = input.ReadWideString(); + + // Build session settings via WinRT + SessionSettings sessionSettings{hstring{sessionName}, hstring{storagePath}}; + sessionSettings.CpuCount(1); + sessionSettings.MemoryMB(1024); + sessionSettings.Timeout(winrt::Windows::Foundation::TimeSpan{std::chrono::milliseconds{5000}}); + + // Attempt to create session + Session session{sessionSettings}; + session.Start(); + + // Read container parameters from corpus + std::string imageName = input.ReadString(); + ContainerSettings containerSettings{hstring{winrt::to_hstring(imageName)}}; + + std::string containerName = input.ReadString(); + if (!containerName.empty()) + { + containerSettings.Name(hstring{winrt::to_hstring(containerName)}); + } + + std::string hostName = input.ReadString(); + if (!hostName.empty()) + { + containerSettings.HostName(hstring{winrt::to_hstring(hostName)}); + } + + // Fuzz port mappings + uint8_t portCount = input.Read(); + auto portMappings = containerSettings.PortMappings(); + for (uint8_t i = 0; i < portCount; ++i) + { + uint16_t windowsPort = input.Read(); + uint16_t containerPort = input.Read(); + auto protocol = static_cast(input.Read() % 2); + portMappings.Append(ContainerPortMapping{windowsPort, containerPort, protocol}); + } + + // Fuzz flags + auto flags = static_cast(input.Read()); + containerSettings.Flags(flags); + + // Attempt container creation + auto container = session.CreateContainer(containerSettings); + + // Cleanup + session.Terminate(); + } + catch (...) + { + // Expected — most fuzzed inputs will fail. ASAN crashes bypass this. + } + + return 0; +} diff --git a/src/windows/fuzzing/generate-seeds.ps1 b/src/windows/fuzzing/generate-seeds.ps1 new file mode 100644 index 0000000000..79f078cd2d --- /dev/null +++ b/src/windows/fuzzing/generate-seeds.ps1 @@ -0,0 +1,118 @@ +# Copyright (C) Microsoft Corporation. All rights reserved. +# +# Generates seed corpus files for each fuzzing harness. +# Output is deterministic (no randomness) — run in the pipeline before staging. +# Output goes into per-target subdirectories matching harness names. + +param( + [Parameter(Mandatory)] + [string]$OutputDir +) + +# Write a wide (UTF-16LE) null-terminated string +function Write-WideString([System.IO.BinaryWriter]$writer, [string]$s) { + $writer.Write([System.Text.Encoding]::Unicode.GetBytes($s + "`0")) +} + +# Write a narrow (UTF-8) null-terminated string +function Write-NarrowString([System.IO.BinaryWriter]$writer, [string]$s) { + $writer.Write([System.Text.Encoding]::UTF8.GetBytes($s + "`0")) +} + +# Create a seed binary file, invoking $body with a BinaryWriter +function New-SeedFile([string]$dir, [string]$file, [scriptblock]$body) { + New-Item -ItemType Directory -Path $dir -Force -ErrorAction Stop | Out-Null + + # File::Create() resolves relative paths differently than PowerShell, so we use a full path. + $path = Join-Path (Resolve-Path $dir) $file + + $stream = [System.IO.File]::Create($path) + $writer = [System.IO.BinaryWriter]::new($stream) + try { + & $body $writer + } + finally { + $writer.Close() + } +} + +# --- CLI harness seeds --- +# Format: concatenated null-terminated wide strings (argv-style, split by SplitToArgs) + +function New-CliSeed([string]$name, [string[]]$cliArgs) { + New-SeedFile "$OutputDir\WslcCliArgumentFuzzing\seeds" "$name.bin" { + param($writer) + foreach ($arg in $cliArgs) { + Write-WideString $writer $arg + } + } +} + +New-CliSeed "install" @("--install", "Ubuntu") +New-CliSeed "run-command" @("--name", "MyDistro", "--user", "root", "--", "ls", "-la") +New-CliSeed "flags" @("--all", "--verbose", "--force") +New-CliSeed "env-volume" @("--env", "FOO=bar", "--env", "BAZ=qux", "--volume", "C:\Users:/mnt/users", "--workdir", "/home/user") +New-CliSeed "publish" @("--publish", "8080:80", "--publish", "443:443", "--hostname", "devbox") + +# --- SDK and WinRT harness seeds (same binary format) --- +# Format: [widestr sessionName] [widestr storagePath] +# [str imageName] [str containerName] [str hostName] +# [u8 portCount] [portCount x (u16 windowsPort, u16 containerPort, u8 protocol)] +# [u32 flags] + +$sdkSeedDir = "$OutputDir\WslcSdkFuzzing\seeds" + +function New-SdkSeed { + param( + [string]$Name, + [string]$SessionName, + [string]$StoragePath, + [string]$ImageName, + [string]$ContainerName, + [string]$HostName, + [hashtable[]]$Ports = @(), + [uint32]$Flags = 0 + ) + New-SeedFile $sdkSeedDir "$Name.bin" { + param($writer) + Write-WideString $writer $SessionName + Write-WideString $writer $StoragePath + Write-NarrowString $writer $ImageName + Write-NarrowString $writer $ContainerName + Write-NarrowString $writer $HostName + $writer.Write([byte]$Ports.Count) + foreach ($port in $Ports) { + $writer.Write([uint16]$port.WindowsPort) + $writer.Write([uint16]$port.ContainerPort) + $writer.Write([byte]$port.Protocol) + } + $writer.Write([uint32]$Flags) + } +} + +New-SdkSeed -Name "basic-session" ` + -SessionName "test-session" -StoragePath "C:\temp\storage" ` + -ImageName "ubuntu:latest" -ContainerName "my-container" -HostName "localhost" ` + -Ports @{WindowsPort=8080; ContainerPort=80; Protocol=0}, @{WindowsPort=443; ContainerPort=443; Protocol=1} ` + -Flags 0 + +New-SdkSeed -Name "empty-strings" ` + -SessionName "" -StoragePath "" ` + -ImageName "" -ContainerName "" -HostName "" + +New-SdkSeed -Name "long-names" ` + -SessionName ("A" * 63) -StoragePath ("C:\very\long\path\" + ("x" * 40)) ` + -ImageName ("registry.example.com/org/" + ("img" * 10) + ":v1.2.3") ` + -ContainerName ("container-" + ("name" * 10)) -HostName ("host-" + ("name" * 10)) ` + -Ports @{WindowsPort=9090; ContainerPort=9090; Protocol=0} ` + -Flags ([uint32]::MaxValue) + +# WinRT uses the same seeds +$winrtSeedDir = "$OutputDir\WslcWinRtFuzzing\seeds" +New-Item -ItemType Directory -Path $winrtSeedDir -Force | Out-Null +Copy-Item -Path "$sdkSeedDir\*" -Destination $winrtSeedDir -Force + +Write-Host "Seeds generated in: $OutputDir" +Get-ChildItem -Recurse -File -Path $OutputDir | ForEach-Object { + Write-Host " $(Resolve-Path $_.FullName -Relative)" +} diff --git a/src/windows/fuzzing/setup.ps1 b/src/windows/fuzzing/setup.ps1 new file mode 100644 index 0000000000..c213a3c3fb --- /dev/null +++ b/src/windows/fuzzing/setup.ps1 @@ -0,0 +1,18 @@ +# OneFuzz VM setup for WSLC fuzzing. +# Installs Hyper-V and WSL so the service is available for SDK harnesses. +# RebootAfterSetup must be true in OneFuzzConfig.json for features to take effect. + +Set-Location -Path $PSScriptRoot +$ErrorActionPreference = "Stop" + +Write-Host "Enabling Hyper-V..." +Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All -NoRestart + +Write-Host "Enabling WSL..." +Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart + +$msiPath = Join-Path $PSScriptRoot "wsl.msi" +Write-Host "Installing WSL from $msiPath..." +Start-Process msiexec -ArgumentList "/i `"$msiPath`" /quiet /norestart" -Wait + +Write-Host "Setup complete. Reboot required." diff --git a/src/windows/wslc/CMakeLists.txt b/src/windows/wslc/CMakeLists.txt index d4a4ba6236..d448472d71 100644 --- a/src/windows/wslc/CMakeLists.txt +++ b/src/windows/wslc/CMakeLists.txt @@ -6,8 +6,11 @@ list(TRANSFORM WSLC_SUBDIR_PATHS APPEND /*.cpp OUTPUT_VARIABLE SOURCE_PATTERNS) file(GLOB_RECURSE HEADERS CONFIGURE_DEPENDS ${HEADER_PATTERNS}) file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS ${SOURCE_PATTERNS}) +set(WSLC_MAIN ${CMAKE_CURRENT_SOURCE_DIR}/core/Main.cpp) +list(REMOVE_ITEM SOURCES ${WSLC_MAIN}) + # Object library for WSLC components. -# Used to build the executable and also unit testing components. +# Used by the wslc executable, unit tests, and fuzzing harnesses. add_library(wslclib OBJECT ${SOURCES} ${HEADERS}) target_include_directories(wslclib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${WSLC_SUBDIR_PATHS}) @@ -18,18 +21,17 @@ target_link_libraries(wslclib advapi32 crypt32) - target_precompile_headers(wslclib REUSE_FROM common) set_target_properties(wslclib PROPERTIES FOLDER windows) # Create wslc.exe # N.B. Linking wslclib (OBJECT library) brings both object files and # transitive dependencies. Do not also use $. -add_executable(wslc) +add_executable(wslc ${WSLC_MAIN}) target_link_libraries(wslc wslclib) +target_precompile_headers(wslc REUSE_FROM common) set_target_properties(wslc PROPERTIES FOLDER windows) -# For prettier source tree browsing source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES} ${HEADERS}) diff --git a/src/windows/wslinstall/CMakeLists.txt b/src/windows/wslinstall/CMakeLists.txt index 4f9fcf4399..1aca465e30 100644 --- a/src/windows/wslinstall/CMakeLists.txt +++ b/src/windows/wslinstall/CMakeLists.txt @@ -2,16 +2,26 @@ set(SOURCES DllMain.cpp version.rc) +if (WSL_BUILD_FUZZING) + # wslinstall is an MSI custom action DLL loaded from a temp directory at install time, + # where the dynamic ASAN runtime can't be resolved. Build it without ASAN so it has no + # dependency on the runtime. This also requires linking a non-instrumented common library. + string(REPLACE "/fsanitize=address" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + set(WSLINSTALL_COMMON common_noasan) +else() + set(WSLINSTALL_COMMON common) +endif() + add_library(wslinstall SHARED ${SOURCES} wslinstall.def) target_include_directories(wslinstall PUBLIC ${CMAKE_CURRENT_LIST_DIR}) set_target_properties(wslinstall PROPERTIES FOLDER windows) -target_precompile_headers(wslinstall REUSE_FROM common) +target_precompile_headers(wslinstall REUSE_FROM ${WSLINSTALL_COMMON}) target_link_libraries(wslinstall ${COMMON_LINK_LIBRARIES} ${MSI_LINK_LIBRARIES} - common + ${WSLINSTALL_COMMON} legacy_stdio_definitions Crypt32.lib sfc.lib) \ No newline at end of file From 67934ea2c57777d9e74c80d2dddf5a7911016a1a Mon Sep 17 00:00:00 2001 From: Flor Elisa Chacon Ochoa Date: Mon, 22 Jun 2026 18:47:42 -0700 Subject: [PATCH 2/2] PR comments --- .pipelines/build-job.yml | 42 +++++++---- .pipelines/fuzzing-stage.yml | 18 +++-- .pipelines/variables.yml | 3 + CMakeLists.txt | 4 +- src/windows/fuzzing/CMakeLists.txt | 10 +++ .../fuzzing/OneFuzzConfigEntry.json.in | 4 +- src/windows/fuzzing/WslcSdkFuzzing.cpp | 73 ++++--------------- 7 files changed, 68 insertions(+), 86 deletions(-) create mode 100644 .pipelines/variables.yml diff --git a/.pipelines/build-job.yml b/.pipelines/build-job.yml index f883f0b4bb..577b58517c 100644 --- a/.pipelines/build-job.yml +++ b/.pipelines/build-job.yml @@ -64,20 +64,32 @@ jobs: pool: ${{ parameters.pool }} variables: - NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS: 60 - NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS: 60 - ob_outputDirectory: '$(Build.SourcesDirectory)\out' - ob_artifactBaseName: 'drop_wsl' - ob_artifactSuffix: '${{ parameters.artifactSuffix }}' - packageStagingDir: '$(Build.SourcesDirectory)\packageStagingDir' - ob_sdl_codeSignValidation_excludes: -|**\*.ps1;-|**\testbin\** - ${{ if parameters.includeCodeQL }}: - Codeql.PublishDatabaseLog: true - Codeql.SourceRoot: src - ${{ if eq(parameters.isRelease, true) }}: - packageInputDirArg: '-DPACKAGE_INPUT_DIR=$(packageStagingDir)' - ${{ else }}: - packageInputDirArg: '' + - template: variables.yml + - name: NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS + value: 60 + - name: NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS + value: 60 + - name: ob_outputDirectory + value: '$(Build.SourcesDirectory)\out' + - name: ob_artifactBaseName + value: 'drop_wsl' + - name: ob_artifactSuffix + value: '${{ parameters.artifactSuffix }}' + - name: packageStagingDir + value: '$(Build.SourcesDirectory)\packageStagingDir' + - name: ob_sdl_codeSignValidation_excludes + value: -|**\*.ps1;-|**\testbin\** + - ${{ if parameters.includeCodeQL }}: + - name: Codeql.PublishDatabaseLog + value: true + - name: Codeql.SourceRoot + value: src + - ${{ if eq(parameters.isRelease, true) }}: + - name: packageInputDirArg + value: '-DPACKAGE_INPUT_DIR=$(packageStagingDir)' + - ${{ else }}: + - name: packageInputDirArg + value: '' steps: @@ -124,7 +136,7 @@ jobs: displayName: "CMake ${{ parameters.platform }}" inputs: workingDirectory: "." - cmakeArgs: . --fresh -A ${{ parameters.platform }} -DCMAKE_BUILD_TYPE=Release -DCMAKE_SYSTEM_VERSION=10.0.26100.0 -DPACKAGE_VERSION=$(version.WSL_PACKAGE_VERSION) -DWSL_NUGET_PACKAGE_VERSION=$(version.WSL_NUGET_PACKAGE_VERSION) -DSKIP_PACKAGE_SIGNING=${{ parameters.isRelease }} -DOFFICIAL_BUILD=${{ parameters.isRelease }} -DINCLUDE_PACKAGE_STAGE=${{ or(parameters.isRelease, parameters.isNightly) }} -DPIPELINE_BUILD_ID=$(Build.BuildId) -DVSO_ORG=${{ parameters.vsoOrg }} -DVSO_PROJECT=${{ parameters.vsoProject }} -DWSL_BUILD_WSL_SETTINGS=true -DWSL_INCLUDE_SDK_CSHARP=true $(packageInputDirArg)\${{ parameters.platform }} + cmakeArgs: . --fresh -A ${{ parameters.platform }} -DCMAKE_BUILD_TYPE=Release -DCMAKE_SYSTEM_VERSION=$(cmakeSystemVersion) -DPACKAGE_VERSION=$(version.WSL_PACKAGE_VERSION) -DWSL_NUGET_PACKAGE_VERSION=$(version.WSL_NUGET_PACKAGE_VERSION) -DSKIP_PACKAGE_SIGNING=${{ parameters.isRelease }} -DOFFICIAL_BUILD=${{ parameters.isRelease }} -DINCLUDE_PACKAGE_STAGE=${{ or(parameters.isRelease, parameters.isNightly) }} -DPIPELINE_BUILD_ID=$(Build.BuildId) -DVSO_ORG=${{ parameters.vsoOrg }} -DVSO_PROJECT=${{ parameters.vsoProject }} -DWSL_BUILD_WSL_SETTINGS=true -DWSL_INCLUDE_SDK_CSHARP=true $(packageInputDirArg)\${{ parameters.platform }} # Workaround for WSL Settings NuGet restore authentication issue - script: _deps\nuget.exe restore -NonInteractive diff --git a/.pipelines/fuzzing-stage.yml b/.pipelines/fuzzing-stage.yml index eb37a97986..7c76791ed8 100644 --- a/.pipelines/fuzzing-stage.yml +++ b/.pipelines/fuzzing-stage.yml @@ -18,17 +18,22 @@ stages: pool: type: windows + # OneFuzz job notification (a team alias) and the bug assignee (a person) must be defined as secret pipeline variables. variables: - ob_outputDirectory: '$(Build.SourcesDirectory)\out' - fuzzingBinDir: '$(Build.SourcesDirectory)\bin\x64\Release' - fuzzingStagingDir: '$(ob_outputDirectory)\fuzzing' + - template: variables.yml + - name: ob_outputDirectory + value: '$(Build.SourcesDirectory)\out' + - name: fuzzingBinDir + value: '$(Build.SourcesDirectory)\bin\x64\Release' + - name: fuzzingStagingDir + value: '$(ob_outputDirectory)\fuzzing' steps: - task: CMake@1 displayName: "CMake configure (fuzzing)" inputs: workingDirectory: "." - cmakeArgs: . --fresh -A x64 -DCMAKE_BUILD_TYPE=Release -DCMAKE_SYSTEM_VERSION=10.0.26100.0 -DWSL_BUILD_FUZZING=true + cmakeArgs: . --fresh -A x64 -DCMAKE_BUILD_TYPE=Release -DCMAKE_SYSTEM_VERSION=$(cmakeSystemVersion) -DWSL_BUILD_FUZZING=true -DONEFUZZ_NOTIFICATION_EMAIL=$(oneFuzzNotificationEmail) -DONEFUZZ_ASSIGNED_TO=$(oneFuzzAssignedTo) # Build the fuzzing targets, and the MSI for setting up the VMs - task: CMake@1 @@ -60,10 +65,9 @@ stages: # (e.g. wslcsdk.dll/pdb) under the fuzzing output dir. Copy-Item "$binDir\fuzzing\*" $dropDir -Force - # Shared runtime deps. The ASAN runtime path was exported by the CMake configure - # step as the asanRuntimeDll job variable. + # Shared runtime deps. CMake copies the ASAN runtime next to the build output. Copy-Item "$binDir\wsl.msi" $dropDir - Copy-Item "$(asanRuntimeDll)" $dropDir -ErrorAction Stop + Copy-Item "$binDir\clang_rt.asan_dynamic*.dll" $dropDir Get-ChildItem $dropDir -Recurse | Format-Table Name, Length diff --git a/.pipelines/variables.yml b/.pipelines/variables.yml new file mode 100644 index 0000000000..afc95bd108 --- /dev/null +++ b/.pipelines/variables.yml @@ -0,0 +1,3 @@ +variables: + - name: cmakeSystemVersion + value: '10.0.26100.0' diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a6b1db852..2d2515e848 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -326,7 +326,7 @@ if (WSL_BUILD_FUZZING) # All instrumented binaries import the dynamic ASAN runtime. # We resolve its path so that we can include it in the MSI; we can find it next to the compiler. - # We also export its path as a pipeline variable so we can find it there. + # We also copy it next to the build output so that the pipeline can find it easily. if (${TARGET_PLATFORM} STREQUAL "arm64") set(ASAN_ARCH "aarch64") else() @@ -334,7 +334,7 @@ if (WSL_BUILD_FUZZING) endif() get_filename_component(MSVC_BIN_DIR ${CMAKE_CXX_COMPILER} DIRECTORY) set(ASAN_RUNTIME_DLL ${MSVC_BIN_DIR}/clang_rt.asan_dynamic-${ASAN_ARCH}.dll) - execute_process(COMMAND ${CMAKE_COMMAND} -E echo "##vso[task.setvariable variable=asanRuntimeDll]${ASAN_RUNTIME_DLL}") + file(COPY ${ASAN_RUNTIME_DLL} DESTINATION ${BIN}) endif () if (WSL_ENABLE_FUZZING_REPLAY) diff --git a/src/windows/fuzzing/CMakeLists.txt b/src/windows/fuzzing/CMakeLists.txt index 3e81a78c3c..2a01880538 100644 --- a/src/windows/fuzzing/CMakeLists.txt +++ b/src/windows/fuzzing/CMakeLists.txt @@ -1,5 +1,15 @@ set(FUZZING_OUT_DIR ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${CMAKE_BUILD_TYPE}/fuzzing) +# OneFuzz job notification (a team alias) and bug assignee (a person), substituted into +# OneFuzzConfigEntry.json.in. The pipeline passes these via -D from secret variables; local +# builds leave them empty since they never submit to OneFuzz. +if (NOT DEFINED ONEFUZZ_NOTIFICATION_EMAIL) + set(ONEFUZZ_NOTIFICATION_EMAIL "") +endif() +if (NOT DEFINED ONEFUZZ_ASSIGNED_TO) + set(ONEFUZZ_ASSIGNED_TO "") +endif() + function(add_fuzzing_harness TARGET) cmake_parse_arguments(ARG "" "TARGET_NAME" "LINK_LIBRARIES;EXTRA_DEPENDENCIES;INCLUDE_DIRECTORIES" ${ARGN}) diff --git a/src/windows/fuzzing/OneFuzzConfigEntry.json.in b/src/windows/fuzzing/OneFuzzConfigEntry.json.in index a6dde8e68e..f9628cc9a7 100644 --- a/src/windows/fuzzing/OneFuzzConfigEntry.json.in +++ b/src/windows/fuzzing/OneFuzzConfigEntry.json.in @@ -1,5 +1,5 @@ { - "JobNotificationEmail": "florch@microsoft.com", + "JobNotificationEmail": "${ONEFUZZ_NOTIFICATION_EMAIL}", "Skip": false, "RebootAfterSetup": true, "Fuzzer": { @@ -23,7 +23,7 @@ "AdoTemplate": { "Org": "microsoft", "Project": "OS", - "AssignedTo": "florch@microsoft.com", + "AssignedTo": "${ONEFUZZ_ASSIGNED_TO}", "AreaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\InstaDev", "IterationPath": "OS" } diff --git a/src/windows/fuzzing/WslcSdkFuzzing.cpp b/src/windows/fuzzing/WslcSdkFuzzing.cpp index 65afc325ef..a8a1f463f1 100644 --- a/src/windows/fuzzing/WslcSdkFuzzing.cpp +++ b/src/windows/fuzzing/WslcSdkFuzzing.cpp @@ -9,57 +9,12 @@ #include "wslcsdk.h" +#include "FuzzingHarness.h" + #include -#include #include #include -// Helper to read a null-terminated narrow string from the fuzz buffer. -// Advances offset past the string (including null) or to end of buffer. -static std::string ReadString(const uint8_t* data, size_t size, size_t& offset, size_t maxLen = 64) -{ - std::string result; - while (offset < size && result.size() < maxLen) - { - char ch = static_cast(data[offset++]); - if (ch == '\0') - { - break; - } - result.push_back(ch); - } - return result; -} - -// Helper to read a null-terminated wide string from the fuzz buffer. -static std::wstring ReadWideString(const uint8_t* data, size_t size, size_t& offset, size_t maxLen = 64) -{ - std::wstring result; - while (offset + 1 < size && result.size() < maxLen) - { - wchar_t ch = static_cast(data[offset] | (data[offset + 1] << 8)); - offset += 2; - if (ch == L'\0') - { - break; - } - result.push_back(ch); - } - return result; -} - -template -static T ReadValue(const uint8_t* data, size_t size, size_t& offset) -{ - T value{}; - if (offset + sizeof(T) <= size) - { - std::memcpy(&value, data + offset, sizeof(T)); - offset += sizeof(T); - } - return value; -} - extern "C" int LLVMFuzzerInitialize(int*, char***) { return 0; @@ -72,11 +27,11 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) return -1; } - size_t offset = 0; + FuzzInput input{data, size}; // Read session parameters from corpus - std::wstring sessionName = ReadWideString(data, size, offset); - std::wstring storagePath = ReadWideString(data, size, offset); + std::wstring sessionName = input.ReadWideString(); + std::wstring storagePath = input.ReadWideString(); // Initialize session settings WslcSessionSettings sessionSettings{}; @@ -92,22 +47,22 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) WslcSetSessionSettingsTimeout(&sessionSettings, 5000); // Read container parameters - std::string imageName = ReadString(data, size, offset); - std::string containerName = ReadString(data, size, offset); - std::string hostName = ReadString(data, size, offset); + std::string imageName = input.ReadString(); + std::string containerName = input.ReadString(); + std::string hostName = input.ReadString(); // Read port mappings - uint8_t portCount = (offset < size) ? data[offset++] : 0; + uint8_t portCount = input.Read(); std::vector ports(portCount); for (uint8_t i = 0; i < portCount; ++i) { - ports[i].windowsPort = ReadValue(data, size, offset); - ports[i].containerPort = ReadValue(data, size, offset); - ports[i].protocol = static_cast(ReadValue(data, size, offset) % 2); + ports[i].windowsPort = input.Read(); + ports[i].containerPort = input.Read(); + ports[i].protocol = static_cast(input.Read() % 2); } // Read flags - auto containerFlags = static_cast(ReadValue(data, size, offset)); + auto containerFlags = static_cast(input.Read()); // Attempt to create session — may fail if service isn't available WslcSession session = nullptr; @@ -154,5 +109,3 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) return 0; } - -#include "FuzzingHarness.h"