Skip to content
Open
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
8 changes: 8 additions & 0 deletions eng/pipelines/libraries/fuzzing/deploy-to-onefuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ extends:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
displayName: Send Base64UrlFuzzer to OneFuzz

- task: onefuzz-task@0
inputs:
onefuzzOSes: 'Windows'
env:
onefuzzDropDirectory: $(fuzzerProject)/deployment/Deflate64Fuzzer
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
displayName: Send Deflate64Fuzzer to OneFuzz

- task: onefuzz-task@0
inputs:
onefuzzOSes: 'Windows'
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
18 changes: 4 additions & 14 deletions src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,7 @@

<ItemGroup>
<Compile Include="Assert.cs" />
<Compile Include="Fuzzers\AssemblyNameInfoFuzzer.cs" />
<Compile Include="Fuzzers\Base64Fuzzer.cs" />
<Compile Include="Fuzzers\Base64UrlFuzzer.cs" />
<Compile Include="Fuzzers\HttpHeadersFuzzer.cs" />
<Compile Include="Fuzzers\IPAddressFuzzer.cs" />
<Compile Include="Fuzzers\JsonDocumentFuzzer.cs" />
<Compile Include="Fuzzers\NrbfDecoderFuzzer.cs" />
<Compile Include="Fuzzers\SearchValuesByteCharFuzzer.cs" />
<Compile Include="Fuzzers\SearchValuesStringFuzzer.cs" />
<Compile Include="Fuzzers\TextEncodingFuzzer.cs" />
<Compile Include="Fuzzers\TypeNameFuzzer.cs" />
<Compile Include="Fuzzers\UTF8Fuzzer.cs" />
<Compile Include="Fuzzers\Utf8JsonWriterFuzzer.cs" />
<Compile Include="Fuzzers\ZipArchiveFuzzer.cs" />
<Compile Include="Fuzzers\*.cs" />
<Compile Include="IFuzzer.cs" />
<Compile Include="PooledBoundedMemory.cs" />
<Compile Include="Program.cs" />
Expand All @@ -43,6 +30,9 @@
<None Include="Dictionaries\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="Corpora\*\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
Expand Down
87 changes: 87 additions & 0 deletions src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/Deflate64Fuzzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Linq;
using System.IO.Compression;
using System.Reflection;
using System.Threading.Tasks;

namespace DotnetFuzzing.Fuzzers;

internal sealed class Deflate64Fuzzer : IFuzzer
{
public string[] TargetAssemblies { get; } = ["System.IO.Compression"];
public string[] TargetCoreLibPrefixes => [];
public string Corpus => "deflate64";

public void FuzzTarget(ReadOnlySpan<byte> bytes)
{
if (bytes.IsEmpty)
{
return;
}

TestArchive(CopyToRentedArray(bytes), bytes.Length, async: false).GetAwaiter().GetResult();
TestArchive(CopyToRentedArray(bytes), bytes.Length, async: true).GetAwaiter().GetResult();
Comment on lines +25 to +26
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fuzzer rents two separate arrays for synchronous and asynchronous test paths. Consider reusing a single rented array for both paths to reduce allocation overhead during fuzzing.

Copilot uses AI. Check for mistakes.
}

#pragma warning disable IL2026 // RequiresUnreferencedCode
private static readonly object _deflate64Value = Enum.ToObject(
typeof(ZipArchive).Assembly.GetType("System.IO.Compression.ZipArchiveEntry+CompressionMethodValues", throwOnError: true)!
, 0x9); // Deflate64

private static readonly Type _deflateStreamType = typeof(ZipArchive).Assembly.GetType("System.IO.Compression.DeflateManagedStream", throwOnError: true)!;
#pragma warning restore IL2026

private static Stream CreateStream(byte[] bytes, int length)
{
#pragma warning disable IL2077 // dynamic access to non-public ctors
return (Stream)Activator.CreateInstance(
_deflateStreamType,
bindingAttr: BindingFlags.NonPublic | BindingFlags.Instance,
binder: null,
args: new object[] { new MemoryStream(bytes, 0, length), _deflate64Value, -1L },
culture: null)!;
#pragma warning restore IL2077
}

private byte[] CopyToRentedArray(ReadOnlySpan<byte> bytes)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bytes.Length);
try
{
bytes.CopyTo(buffer);
return buffer;
}
catch
{
ArrayPool<byte>.Shared.Return(buffer);
throw;
}
}

private async Task TestArchive(byte[] buffer, int length, bool async)
{
try
{
using var stream = CreateStream(buffer, length);
if (async)
{
await stream.CopyToAsync(Stream.Null);
}
else
{
stream.CopyTo(Stream.Null);
}
}
catch (InvalidDataException)
{
// ignore, this exception is expected to be thrown for invalid/corrupted archives.
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ internal sealed class ZipArchiveFuzzer : IFuzzer
public string[] TargetAssemblies { get; } = ["System.IO.Compression"];
public string[] TargetCoreLibPrefixes => [];
public string Dictionary => "ziparchive.dict";
public string Corpus => "ziparchive";

public void FuzzTarget(ReadOnlySpan<byte> bytes)
{
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/Fuzzing/DotnetFuzzing/IFuzzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ internal interface IFuzzer
/// <summary>Optional name of the dictionary to use to better guide the fuzzer.</summary>
string? Dictionary => null;

/// <summary>Optional name of the directory to use as an initial corpus for the fuzzer.</summary>
string? Corpus => null;

/// <summary>Entry point for the fuzzer. Should exercise code paths in <see cref="TargetAssemblies"/> and/or <see cref="TargetCoreLibPrefixes"/>.</summary>
void FuzzTarget(ReadOnlySpan<byte> bytes);
}
32 changes: 31 additions & 1 deletion src/libraries/Fuzzing/DotnetFuzzing/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ private static async Task PrepareOneFuzzDeploymentAsync(IFuzzer[] fuzzers, strin
throw new Exception($"Dictionary '{unusedDictionary}' is not referenced by any fuzzer.");
}

string[] corpora = Directory.GetDirectories(Path.Combine(publishDirectory, "Corpora"))
.Select(Path.GetFileName)
.ToArray()!;

if (corpora.FirstOrDefault(corpus => !fuzzers.Any(f => f.Corpus == corpus)) is { } unusedCorpus)
{
throw new Exception($"Corpus '{unusedCorpus}' is not referenced by any fuzzer.");
}

Directory.CreateDirectory(outputDirectory);

await DownloadArtifactAsync(
Expand Down Expand Up @@ -154,6 +163,20 @@ void PrepareFuzzer(IFuzzer fuzzer)
File.Copy(Path.Combine(publishDirectory, "Dictionaries", dict), Path.Combine(fuzzerDirectory, "dictionary"), overwrite: true);
}

if (fuzzer.Corpus is string corpus)
{
if (!corpora.Contains(corpus, StringComparer.Ordinal))
{
throw new Exception($"Fuzzer '{fuzzer.Name}' is referencing a corpus '{fuzzer.Corpus}' that does not exist in the publish directory.");
}

Directory.CreateDirectory(Path.Combine(fuzzerDirectory, "corpus"));
foreach (string file in Directory.EnumerateFiles(Path.Combine(publishDirectory, "Corpora", corpus), "*", SearchOption.TopDirectoryOnly))
{
File.Copy(file, Path.Combine(fuzzerDirectory, "corpus", Path.GetFileName(file)), overwrite: true);
}
}

InstrumentAssemblies(fuzzer, fuzzerDirectory);

File.WriteAllText(Path.Combine(fuzzerDirectory, "OneFuzzConfig.json"), GenerateOneFuzzConfigJson(fuzzer));
Expand Down Expand Up @@ -361,7 +384,14 @@ private static string GenerateLocalRunHelperScript(IFuzzer fuzzer)
{
string script = $"%~dp0/libfuzzer-dotnet.exe --target_path=%~dp0/DotnetFuzzing.exe --target_arg={fuzzer.Name}";

if (fuzzer.Dictionary is not null)
// We don't support dictionaries and corpora at the same time yet, and some fuzzers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Any reason why not?
The corpus will be ignored by OneFuzz, but I don't see why we should block it locally.

Copy link
Member Author

@rzikm rzikm Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes only the local run script, OneFuzz runs are unaffected.

Since we don't have corpora setup for OneFuzz (yet), some fuzzers place example inputs as dictionary entries instead, but that does not work as well as having them in the corpus. I didn't want to remove the dictionaries entirely because that might slow down OneFuzz runs, so as a temporary compromise, corpus takes precedence over dictionary when running locally (as it is more efficient to omit the suboptimal dictionary in this case).

Of course, having both at the same time (corpus of example whole inputs, dictionary with the right "alphabet" from which to compose inputs) would be best and is something we should aim for the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If those dictionaries aren't adding any value beyond seeding the initial corpus for OneFuzz, I think they should be fine to delete now - OneFuzz will reuse the current (already seeded) corpus for new runs for us.

// put corpus in dictionary to work around lack of corpus setup in OneFuzz. Locally
// use corpus as corpus if available as it is more effective that way.
if (fuzzer.Corpus is not null)
{
script += " %~dp0/corpus";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this after the "additional arguments" so that it becomes the last option.

Going by the text in https://llvm.org/docs/LibFuzzer.html#options

To run the fuzzer, pass zero or more corpus directories as command line arguments. The fuzzer will read test inputs from each of these corpus directories, and any new test inputs that are generated will be written back to the first corpus directory

This way if you use a custom folder when fuzzing locally, you'll see the inputs being written there instead of in the deployment folder.

}
else if (fuzzer.Dictionary is not null)
{
script += " -dict=%~dp0dictionary";
}
Expand Down
Loading