Add a basic fuzzing project (#903)

* Add basic fuzzing project

* Mark the project as non-packable
This commit is contained in:
Miha Zupan
2025-10-17 08:09:28 +02:00
committed by GitHub
parent 5c78932f55
commit 03bdf60086
5 changed files with 183 additions and 0 deletions

4
src/Markdig.Fuzzing/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
corpus
libfuzzer-dotnet-windows.exe
crash-*
timeout-*

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SharpFuzz" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Markdig\Markdig.csproj" />
</ItemGroup>
</Project>

View File

@@ -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<byte> 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();
}
}

View File

@@ -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

View File

@@ -12,6 +12,9 @@
<Project Path="Markdig.Benchmarks/Markdig.Benchmarks.csproj">
<BuildDependency Project="Markdig/Markdig.csproj" />
</Project>
<Project Path="Markdig.Fuzzing/Markdig.Fuzzing.csproj">
<BuildDependency Project="Markdig/Markdig.csproj" />
</Project>
<Project Path="Markdig.Tests/Markdig.Tests.csproj">
<BuildDependency Project="Markdig/Markdig.csproj" />
</Project>