Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Markdig.Fuzzing/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
corpus
libfuzzer-dotnet-windows.exe
crash-*
timeout-*
19 changes: 19 additions & 0 deletions src/Markdig.Fuzzing/Markdig.Fuzzing.csproj
Original file line number Diff line number Diff line change
@@ -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>
71 changes: 71 additions & 0 deletions src/Markdig.Fuzzing/Program.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
86 changes: 86 additions & 0 deletions src/Markdig.Fuzzing/run-fuzzer.ps1
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions src/markdig.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down