diff --git a/src/Markdig.Fuzzing/.gitignore b/src/Markdig.Fuzzing/.gitignore new file mode 100644 index 00000000..60aca1b0 --- /dev/null +++ b/src/Markdig.Fuzzing/.gitignore @@ -0,0 +1,4 @@ +corpus +libfuzzer-dotnet-windows.exe +crash-* +timeout-* \ No newline at end of file diff --git a/src/Markdig.Fuzzing/Markdig.Fuzzing.csproj b/src/Markdig.Fuzzing/Markdig.Fuzzing.csproj new file mode 100644 index 00000000..b9774246 --- /dev/null +++ b/src/Markdig.Fuzzing/Markdig.Fuzzing.csproj @@ -0,0 +1,19 @@ + + + + Exe + net9.0 + enable + enable + false + + + + + + + + + + + diff --git a/src/Markdig.Fuzzing/Program.cs b/src/Markdig.Fuzzing/Program.cs new file mode 100644 index 00000000..9f63b611 --- /dev/null +++ b/src/Markdig.Fuzzing/Program.cs @@ -0,0 +1,71 @@ +using Markdig; +using Markdig.Renderers.Roundtrip; +using Markdig.Syntax; +using SharpFuzz; +using System.Diagnostics; +using System.Text; + +ReadOnlySpanAction fuzzTarget = ParseRenderFuzzer.FuzzTarget; + +if (args.Length > 0) +{ + // Run the target on existing inputs + string[] files = Directory.Exists(args[0]) + ? Directory.GetFiles(args[0]) + : [args[0]]; + + Debugger.Launch(); + + foreach (string inputFile in files) + { + fuzzTarget(File.ReadAllBytes(inputFile)); + } +} +else +{ + Fuzzer.LibFuzzer.Run(fuzzTarget); +} + +sealed class ParseRenderFuzzer +{ + private static readonly MarkdownPipeline s_advancedPipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .Build(); + + private static readonly ResettableRoundtripRenderer _roundtripRenderer = new(); + + public static void FuzzTarget(ReadOnlySpan bytes) + { + string text = Encoding.UTF8.GetString(bytes); + + try + { + MarkdownDocument document = Markdown.Parse(text); + _ = document.ToHtml(); + + document = Markdown.Parse(text, s_advancedPipeline); + _ = document.ToHtml(s_advancedPipeline); + + document = Markdown.Parse(text, trackTrivia: true); + _ = document.ToHtml(); + _roundtripRenderer.Reset(); + _roundtripRenderer.Render(document); + + _ = Markdown.Normalize(text); + _ = Markdown.ToPlainText(text); + } + catch (Exception ex) when (IsIgnorableException(ex)) { } + } + + private static bool IsIgnorableException(Exception exception) + { + return exception.Message.Contains("Markdown elements in the input are too deeply nested", StringComparison.Ordinal); + } + + private sealed class ResettableRoundtripRenderer : RoundtripRenderer + { + public ResettableRoundtripRenderer() : base(new StringWriter(new StringBuilder(1024 * 1024))) { } + + public new void Reset() => base.Reset(); + } +} \ No newline at end of file diff --git a/src/Markdig.Fuzzing/run-fuzzer.ps1 b/src/Markdig.Fuzzing/run-fuzzer.ps1 new file mode 100644 index 00000000..133acdf8 --- /dev/null +++ b/src/Markdig.Fuzzing/run-fuzzer.ps1 @@ -0,0 +1,86 @@ +param ( + [string]$configuration = $null +) + +Set-StrictMode -Version Latest + +$libFuzzer = "libfuzzer-dotnet-windows.exe" +$outputDir = "bin" + +function Get-LibFuzzer { + param ( + [string]$Path + ) + + $libFuzzerUrl = "https://github.com/Metalnem/libfuzzer-dotnet/releases/download/v2025.05.02.0904/libfuzzer-dotnet-windows.exe" + $expectedHash = "17af5b3f6ff4d2c57b44b9a35c13051b570eb66f0557d00015df3832709050bf" + + Write-Output "Downloading libFuzzer from $libFuzzerUrl..." + + try { + $tempFile = "$Path.tmp" + Invoke-WebRequest -Uri $libFuzzerUrl -OutFile $tempFile -UseBasicParsing + + $downloadedHash = (Get-FileHash -Path $tempFile -Algorithm SHA256).Hash + + if ($downloadedHash -eq $ExpectedHash) { + Move-Item -Path $tempFile -Destination $Path -Force + Write-Output "libFuzzer downloaded successfully to $Path" + } + else { + Write-Error "Hash validation failed." + Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue + exit 1 + } + } + catch { + Write-Error "Failed to download libFuzzer: $($_.Exception.Message)" + Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue + exit 1 + } +} + +# Check if libFuzzer exists, download if not +if (-not (Test-Path $libFuzzer)) { + Get-LibFuzzer -Path $libFuzzer +} + +$toolListOutput = dotnet tool list --global sharpFuzz.CommandLine 2>$null +if (-not ($toolListOutput -match "sharpfuzz")) { + Write-Output "Installing sharpfuzz CLI" + dotnet tool install --global sharpFuzz.CommandLine +} + +if (Test-Path $outputDir) { + Remove-Item -Recurse -Force $outputDir +} + +if ($configuration -eq $null) { + $configuration = "Debug" +} + +dotnet publish -c $configuration -o $outputDir + +$project = Join-Path $outputDir "Markdig.Fuzzing.dll" + +$fuzzingTarget = Join-Path $outputDir "Markdig.dll" + +Write-Output "Instrumenting $fuzzingTarget" +& sharpfuzz $fuzzingTarget + +if ($LastExitCode -ne 0) { + Write-Error "An error occurred while instrumenting $fuzzingTarget" + exit 1 +} + +New-Item -ItemType Directory -Force -Path corpus | Out-Null + +$libFuzzerArgs = @("--target_path=dotnet", "--target_arg=$project", "-timeout=10", "corpus") + +# Add any additional arguments passed to the script +if ($args) { + $libFuzzerArgs += $args +} + +Write-Output "Starting libFuzzer with arguments: $libFuzzerArgs" +& ./$libFuzzer @libFuzzerArgs \ No newline at end of file diff --git a/src/markdig.slnx b/src/markdig.slnx index 512d4a2c..a6f41692 100644 --- a/src/markdig.slnx +++ b/src/markdig.slnx @@ -12,6 +12,9 @@ + + +