< Summary

Information
Class: DotnetFuzzing.Fuzzers.ZipEncryptionStreamFuzzer
Assembly: DotnetFuzzing
File(s): D:\runner\runtime\src\libraries\Fuzzing\DotnetFuzzing\Fuzzers\ZipEncryptionStreamFuzzer.cs
Line coverage
93%
Covered lines: 93
Uncovered lines: 6
Coverable lines: 99
Total lines: 182
Line coverage: 93.9%
Branch coverage
100%
Covered branches: 28
Total branches: 28
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.cctor()100%11100%
FuzzTarget(...)100%1010100%
RoundTrip()100%181892.75%

File(s)

D:\runner\runtime\src\libraries\Fuzzing\DotnetFuzzing\Fuzzers\ZipEncryptionStreamFuzzer.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Buffers;
 5using System.IO.Compression;
 6using System.Runtime.InteropServices;
 7using System.Runtime.Versioning;
 8using System.Threading.Tasks;
 9
 10namespace DotnetFuzzing.Fuzzers;
 11
 12// Exercises every ZIP entry-encryption method through a single round-trip: legacy ZipCrypto
 13// (ZipCryptoStream) and WinZip AES-128/192/256 (WinZipAesStream). The two ciphers share the same
 14// ZipArchiveEntry entry points and stream shape, so one fuzzer covers both encryption paths.
 15[UnsupportedOSPlatform("browser")]
 16internal sealed class ZipEncryptionStreamFuzzer : IFuzzer
 17{
 118    public string[] TargetAssemblies { get; } = ["System.IO.Compression"];
 019    public string[] TargetCoreLibPrefixes => [];
 20
 21    // The entry name is not part of the encryption-stream surface this fuzzer targets: it only feeds
 22    // the generic ZIP name-encoding logic (ASCII-vs-UTF8 detection and the UTF-8 general-purpose-bit
 23    // flag), which is independent of the AES/ZipCrypto encryption path. Fuzzing it here would add
 24    // coverage outside the target, so it is hardcoded; entry-name encoding belongs in a ZIP fuzzer.
 25    private const string EntryName = "entry";
 26
 27    // Used only when the fuzzed password slice is empty; a non-empty password is required to encrypt.
 28    private const string FallbackPassword = "fuzz-password";
 29
 130    private static readonly ZipEncryptionMethod[] s_methods =
 131    [
 132        ZipEncryptionMethod.ZipCrypto,
 133        ZipEncryptionMethod.Aes128,
 134        ZipEncryptionMethod.Aes192,
 135        ZipEncryptionMethod.Aes256,
 136    ];
 37
 38    public void FuzzTarget(ReadOnlySpan<byte> bytes)
 9639    {
 40        // The first byte is consumed below as the encryption-method selector, so an empty
 41        // input has no byte to interpret and there is nothing to fuzz.
 9642        if (bytes.Length < 2)
 143        {
 144            return;
 45        }
 46
 47        // Use the first byte to select the encryption method so all variants get exercised.
 9548        ZipEncryptionMethod method = s_methods[bytes[0] % s_methods.Length];
 9549        ReadOnlySpan<byte> payload = bytes.Slice(2); // Slice 2 to keep MemoryMarshal.Cast<byte, char> below aligned
 50
 51        // Split the rest of the input into a password and the entry content: everything up to the
 52        // first newline is the password, the remainder is the content. Deriving the password from the
 53        // input drives the key derivation and the UTF-8 password-encoding path instead of always using
 54        // a fixed key. The content — the remainder — is what drives the cipher chunking (and, for AES,
 55        // the trailing HMAC), so it stays the bulk of the input.
 9556        int newlineIndex = payload.IndexOf((byte)'\n');
 9557        ReadOnlySpan<byte> passwordBytes = newlineIndex < 0 ? ReadOnlySpan<byte>.Empty : payload.Slice(0, newlineIndex);
 9558        ReadOnlySpan<byte> contentBytes = newlineIndex < 0 ? payload : payload.Slice(newlineIndex + 1);
 59
 60        // An empty entry is stored unencrypted (no ciphertext is produced and IsEncrypted is false on
 61        // read-back), so it does not exercise the encryption stream this fuzzer targets. Skip it.
 9562        if (contentBytes.IsEmpty)
 463        {
 464            return;
 65        }
 66
 67        // Interpret the password bytes as UTF-16 so arbitrary char values (including lone surrogates)
 68        // exercise the UTF-8 encoding of the password during key derivation.
 9169        string password = new string(MemoryMarshal.Cast<byte, char>(passwordBytes));
 70
 71        // A non-empty password is required to create an encrypted entry (CreateEntry throws otherwise).
 72        // The fuzzed slice may yield an empty string (no newline, or a single byte that casts to zero
 73        // chars), so fall back to a fixed non-empty password to keep exercising the key-derivation path.
 9174        if (password.Length == 0)
 7175        {
 7176            password = FallbackPassword;
 7177        }
 78
 79        // Poison the page immediately after the content so any over-read while encrypting or
 80        // decrypting faults immediately instead of silently reading adjacent memory.
 9181        using PooledBoundedMemory<byte> buffer = PooledBoundedMemory<byte>.Rent(contentBytes, PoisonPagePlacement.After)
 9182        RoundTrip(buffer.Memory, password, method, async: false).GetAwaiter().GetResult();
 9183        RoundTrip(buffer.Memory, password, method, async: true).GetAwaiter().GetResult();
 18784    }
 85
 86    private static async Task RoundTrip(ReadOnlyMemory<byte> content, string password, ZipEncryptionMethod method, bool 
 18287    {
 18288        using var archiveStream = new MemoryStream();
 89
 90        // Encrypt the fuzz input, exercising the encryption stream (and HMAC path for AES).
 18291        ZipArchive writeArchive = async
 18292            ? await ZipArchive.CreateAsync(archiveStream, ZipArchiveMode.Create, leaveOpen: true, entryNameEncoding: nul
 18293            : new ZipArchive(archiveStream, ZipArchiveMode.Create, leaveOpen: true, entryNameEncoding: null);
 94
 18295        ZipArchiveEntry writeEntry = writeArchive.CreateEntry(EntryName, password.AsSpan(), method);
 96
 18297        Stream entryWriteStream = async ? await writeEntry.OpenAsync() : writeEntry.Open();
 18298        if (async)
 9199        {
 91100            await entryWriteStream.WriteAsync(content);
 91101            await entryWriteStream.DisposeAsync();
 91102            await writeArchive.DisposeAsync();
 91103        }
 104        else
 91105        {
 91106            entryWriteStream.Write(content.Span);
 91107            entryWriteStream.Dispose();
 91108            writeArchive.Dispose();
 91109        }
 110
 182111        archiveStream.Position = 0;
 112
 113        // Decrypt with the correct password and verify the round-trip is lossless.
 182114        ZipArchive readArchive = async
 182115            ? await ZipArchive.CreateAsync(archiveStream, ZipArchiveMode.Read, leaveOpen: true, entryNameEncoding: null)
 182116            : new ZipArchive(archiveStream, ZipArchiveMode.Read, leaveOpen: true, entryNameEncoding: null);
 117
 182118        ZipArchiveEntry readEntry = readArchive.GetEntry(EntryName)!;
 182119        Assert.True(readEntry.IsEncrypted);
 182120        Assert.Equal(method, readEntry.EncryptionMethod);
 121
 182122        using (var decrypted = new MemoryStream())
 182123        {
 182124            Stream entryReadStream = async ? await readEntry.OpenAsync(password.AsSpan()) : readEntry.Open(password.AsSp
 182125            if (async)
 91126            {
 91127                await entryReadStream.CopyToAsync(decrypted);
 91128                await entryReadStream.DisposeAsync();
 91129            }
 130            else
 91131            {
 91132                entryReadStream.CopyTo(decrypted);
 91133                entryReadStream.Dispose();
 91134            }
 135
 182136            Assert.SequenceEqual(content.Span, decrypted.GetBuffer().AsSpan(0, (int)decrypted.Length));
 182137        }
 138
 139        // The AES variant of ZIP encryption uses a form of HMAC that makes accidentally accepting the wrong key
 140        // statistically unlikely, so a wrong password must fail with InvalidDataException.
 141        // ZipCrypto only has a weak 1-byte verifier over a stream cipher, so a wrong key is rejected
 142        // with roughly 255/256 probability and may occasionally decrypt to garbage without throwing;
 143        // both outcomes are acceptable and cannot be distinguished reliably on fuzzed input. Any
 144        // other exception type would indicate a real bug and is intentionally left to propagate.
 145        // Append a byte so the wrong password is guaranteed to differ from the fuzzed one above.
 182146        bool authenticated = method != ZipEncryptionMethod.ZipCrypto;
 182147        string wrongPassword = password + "!";
 182148        bool wrongPasswordRejected = false;
 149        try
 182150        {
 182151            if (async)
 91152            {
 91153                await using Stream wrongStream = await readEntry.OpenAsync(wrongPassword.AsSpan());
 0154                await wrongStream.CopyToAsync(Stream.Null);
 0155            }
 156            else
 91157            {
 91158                using Stream wrongStream = readEntry.Open(wrongPassword.AsSpan());
 0159                wrongStream.CopyTo(Stream.Null);
 0160            }
 0161        }
 182162        catch (InvalidDataException)
 182163        {
 164            // Expected: the AES password verifier / HMAC (or the ZipCrypto check byte) rejects the wrong key.
 182165            wrongPasswordRejected = true;
 182166        }
 167
 182168        if (authenticated)
 48169        {
 48170            Assert.True(wrongPasswordRejected);
 48171        }
 172
 182173        if (async)
 91174        {
 91175            await readArchive.DisposeAsync();
 91176        }
 177        else
 91178        {
 91179            readArchive.Dispose();
 91180        }
 182181    }
 182}