-
Couldn't load subscription status.
- Fork 5.2k
Add corpus support to local DotnetFuzzing runs and fuzz Deflate64 #121019
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(); | ||
| } | ||
|
|
||
| #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( | ||
MihaZupan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| _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 |
|---|---|---|
|
|
@@ -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()!; | ||
MihaZupan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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( | ||
|
|
@@ -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)); | ||
|
|
@@ -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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Any reason why not? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
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"; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
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.