Skip to content

Commit 03bdf60

Browse files
authored
Add a basic fuzzing project (#903)
* Add basic fuzzing project * Mark the project as non-packable
1 parent 5c78932 commit 03bdf60

File tree

5 files changed

+183
-0
lines changed

5 files changed

+183
-0
lines changed

src/Markdig.Fuzzing/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
corpus
2+
libfuzzer-dotnet-windows.exe
3+
crash-*
4+
timeout-*
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net9.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<IsPackable>false</IsPackable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="SharpFuzz" Version="2.2.0" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<ProjectReference Include="..\Markdig\Markdig.csproj" />
17+
</ItemGroup>
18+
19+
</Project>

src/Markdig.Fuzzing/Program.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using Markdig;
2+
using Markdig.Renderers.Roundtrip;
3+
using Markdig.Syntax;
4+
using SharpFuzz;
5+
using System.Diagnostics;
6+
using System.Text;
7+
8+
ReadOnlySpanAction fuzzTarget = ParseRenderFuzzer.FuzzTarget;
9+
10+
if (args.Length > 0)
11+
{
12+
// Run the target on existing inputs
13+
string[] files = Directory.Exists(args[0])
14+
? Directory.GetFiles(args[0])
15+
: [args[0]];
16+
17+
Debugger.Launch();
18+
19+
foreach (string inputFile in files)
20+
{
21+
fuzzTarget(File.ReadAllBytes(inputFile));
22+
}
23+
}
24+
else
25+
{
26+
Fuzzer.LibFuzzer.Run(fuzzTarget);
27+
}
28+
29+
sealed class ParseRenderFuzzer
30+
{
31+
private static readonly MarkdownPipeline s_advancedPipeline = new MarkdownPipelineBuilder()
32+
.UseAdvancedExtensions()
33+
.Build();
34+
35+
private static readonly ResettableRoundtripRenderer _roundtripRenderer = new();
36+
37+
public static void FuzzTarget(ReadOnlySpan<byte> bytes)
38+
{
39+
string text = Encoding.UTF8.GetString(bytes);
40+
41+
try
42+
{
43+
MarkdownDocument document = Markdown.Parse(text);
44+
_ = document.ToHtml();
45+
46+
document = Markdown.Parse(text, s_advancedPipeline);
47+
_ = document.ToHtml(s_advancedPipeline);
48+
49+
document = Markdown.Parse(text, trackTrivia: true);
50+
_ = document.ToHtml();
51+
_roundtripRenderer.Reset();
52+
_roundtripRenderer.Render(document);
53+
54+
_ = Markdown.Normalize(text);
55+
_ = Markdown.ToPlainText(text);
56+
}
57+
catch (Exception ex) when (IsIgnorableException(ex)) { }
58+
}
59+
60+
private static bool IsIgnorableException(Exception exception)
61+
{
62+
return exception.Message.Contains("Markdown elements in the input are too deeply nested", StringComparison.Ordinal);
63+
}
64+
65+
private sealed class ResettableRoundtripRenderer : RoundtripRenderer
66+
{
67+
public ResettableRoundtripRenderer() : base(new StringWriter(new StringBuilder(1024 * 1024))) { }
68+
69+
public new void Reset() => base.Reset();
70+
}
71+
}

src/Markdig.Fuzzing/run-fuzzer.ps1

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
param (
2+
[string]$configuration = $null
3+
)
4+
5+
Set-StrictMode -Version Latest
6+
7+
$libFuzzer = "libfuzzer-dotnet-windows.exe"
8+
$outputDir = "bin"
9+
10+
function Get-LibFuzzer {
11+
param (
12+
[string]$Path
13+
)
14+
15+
$libFuzzerUrl = "https://github.com/Metalnem/libfuzzer-dotnet/releases/download/v2025.05.02.0904/libfuzzer-dotnet-windows.exe"
16+
$expectedHash = "17af5b3f6ff4d2c57b44b9a35c13051b570eb66f0557d00015df3832709050bf"
17+
18+
Write-Output "Downloading libFuzzer from $libFuzzerUrl..."
19+
20+
try {
21+
$tempFile = "$Path.tmp"
22+
Invoke-WebRequest -Uri $libFuzzerUrl -OutFile $tempFile -UseBasicParsing
23+
24+
$downloadedHash = (Get-FileHash -Path $tempFile -Algorithm SHA256).Hash
25+
26+
if ($downloadedHash -eq $ExpectedHash) {
27+
Move-Item -Path $tempFile -Destination $Path -Force
28+
Write-Output "libFuzzer downloaded successfully to $Path"
29+
}
30+
else {
31+
Write-Error "Hash validation failed."
32+
Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue
33+
exit 1
34+
}
35+
}
36+
catch {
37+
Write-Error "Failed to download libFuzzer: $($_.Exception.Message)"
38+
Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue
39+
exit 1
40+
}
41+
}
42+
43+
# Check if libFuzzer exists, download if not
44+
if (-not (Test-Path $libFuzzer)) {
45+
Get-LibFuzzer -Path $libFuzzer
46+
}
47+
48+
$toolListOutput = dotnet tool list --global sharpFuzz.CommandLine 2>$null
49+
if (-not ($toolListOutput -match "sharpfuzz")) {
50+
Write-Output "Installing sharpfuzz CLI"
51+
dotnet tool install --global sharpFuzz.CommandLine
52+
}
53+
54+
if (Test-Path $outputDir) {
55+
Remove-Item -Recurse -Force $outputDir
56+
}
57+
58+
if ($configuration -eq $null) {
59+
$configuration = "Debug"
60+
}
61+
62+
dotnet publish -c $configuration -o $outputDir
63+
64+
$project = Join-Path $outputDir "Markdig.Fuzzing.dll"
65+
66+
$fuzzingTarget = Join-Path $outputDir "Markdig.dll"
67+
68+
Write-Output "Instrumenting $fuzzingTarget"
69+
& sharpfuzz $fuzzingTarget
70+
71+
if ($LastExitCode -ne 0) {
72+
Write-Error "An error occurred while instrumenting $fuzzingTarget"
73+
exit 1
74+
}
75+
76+
New-Item -ItemType Directory -Force -Path corpus | Out-Null
77+
78+
$libFuzzerArgs = @("--target_path=dotnet", "--target_arg=$project", "-timeout=10", "corpus")
79+
80+
# Add any additional arguments passed to the script
81+
if ($args) {
82+
$libFuzzerArgs += $args
83+
}
84+
85+
Write-Output "Starting libFuzzer with arguments: $libFuzzerArgs"
86+
& ./$libFuzzer @libFuzzerArgs

src/markdig.slnx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
<Project Path="Markdig.Benchmarks/Markdig.Benchmarks.csproj">
1313
<BuildDependency Project="Markdig/Markdig.csproj" />
1414
</Project>
15+
<Project Path="Markdig.Fuzzing/Markdig.Fuzzing.csproj">
16+
<BuildDependency Project="Markdig/Markdig.csproj" />
17+
</Project>
1518
<Project Path="Markdig.Tests/Markdig.Tests.csproj">
1619
<BuildDependency Project="Markdig/Markdig.csproj" />
1720
</Project>

0 commit comments

Comments
 (0)