From 000c116e5164365e14539ae6f5785011ffd7d098 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Wed, 5 Nov 2025 19:10:55 -0500 Subject: [PATCH] Add Fuzz Tests (#26384) Co-authored-by: anamnavi --- .../AssemblyInfo.cs | 1 + .../common/RemoteSessionHyperVSocket.cs | 35 +++++++-- test/fuzzing/FuzzingApp/Program.cs | 45 +++++++++++ test/fuzzing/FuzzingApp/Target.cs | 19 +++++ .../FuzzingApp/powershell-fuzz-tests.csproj | 29 +++++++ test/fuzzing/inputs/maxinput | 1 + test/fuzzing/inputs/mininput | 1 + test/fuzzing/runFuzzer.ps1 | 75 +++++++++++++++++++ 8 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 test/fuzzing/FuzzingApp/Program.cs create mode 100644 test/fuzzing/FuzzingApp/Target.cs create mode 100644 test/fuzzing/FuzzingApp/powershell-fuzz-tests.csproj create mode 100644 test/fuzzing/inputs/maxinput create mode 100644 test/fuzzing/inputs/mininput create mode 100644 test/fuzzing/runFuzzer.ps1 diff --git a/src/System.Management.Automation/AssemblyInfo.cs b/src/System.Management.Automation/AssemblyInfo.cs index 559d809fe7..e265bb453c 100644 --- a/src/System.Management.Automation/AssemblyInfo.cs +++ b/src/System.Management.Automation/AssemblyInfo.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("powershell-tests,PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: InternalsVisibleTo("powershell-perf,PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] +[assembly: InternalsVisibleTo("powershell-fuzz-tests,PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: InternalsVisibleTo(@"Microsoft.PowerShell.Commands.Utility" + @",PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: InternalsVisibleTo(@"Microsoft.PowerShell.Commands.Management" + @",PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] diff --git a/src/System.Management.Automation/engine/remoting/common/RemoteSessionHyperVSocket.cs b/src/System.Management.Automation/engine/remoting/common/RemoteSessionHyperVSocket.cs index cd18dc511b..8d53adf0ca 100644 --- a/src/System.Management.Automation/engine/remoting/common/RemoteSessionHyperVSocket.cs +++ b/src/System.Management.Automation/engine/remoting/common/RemoteSessionHyperVSocket.cs @@ -434,7 +434,10 @@ namespace System.Management.Automation.Remoting // Final check if we got the token before the timeout cancellationToken.ThrowIfCancellationRequested(); - if (string.IsNullOrEmpty(responseString) || !responseString.StartsWith("TOKEN ", StringComparison.Ordinal)) + ReadOnlySpan responseBytes = Encoding.UTF8.GetBytes(responseString); + string responseToken = RemoteSessionHyperVSocketClient.ExtractToken(responseBytes); + + if (responseToken == null) { socket.Send("FAIL"u8); // If the response is not in the expected format, we throw an exception. @@ -444,9 +447,6 @@ namespace System.Management.Automation.Remoting PSRemotingErrorInvariants.FormatResourceString(RemotingErrorIdStrings.HyperVInvalidResponse, "Client", "Token Response")); } - // Extract the token from the response. - string responseToken = responseString.Substring(6).Trim(); - if (!string.Equals(responseToken, token, StringComparison.Ordinal)) { socket.Send("FAIL"u8); @@ -1059,14 +1059,18 @@ namespace System.Management.Automation.Remoting // allowing a significant larger size, allows the broker to make almost arbitrary changes, // without breaking the client. string token = ReceiveResponse(HyperVSocket, 1024); // either "PASS" or "FAIL" - if (token == null || !token.StartsWith("TOKEN ", StringComparison.Ordinal)) + + ReadOnlySpan tokenResponseBytes = Encoding.UTF8.GetBytes(token); + string extractedToken = ExtractToken(tokenResponseBytes); + + if (extractedToken == null) { s_tracer.WriteLine("ExchangeCredentialsAndConfiguration: Server did not respond with a valid token. Response: {0}", token); throw new PSDirectException( PSRemotingErrorInvariants.FormatResourceString(RemotingErrorIdStrings.HyperVInvalidResponse, "Broker", "Token " + token)); } - token = token.Substring(6); // remove "TOKEN " prefix + token = extractedToken; HyperVSocket.Send("PASS"u8); // acknowledge the token return (true, token); @@ -1122,6 +1126,25 @@ namespace System.Management.Automation.Remoting } } + internal static string ExtractToken(ReadOnlySpan tokenResponse) + { + string token = Encoding.UTF8.GetString(tokenResponse); + + if (token == null || !token.StartsWith("TOKEN ", StringComparison.Ordinal)) + { + return null; // caller method will write trace (and determine when to expose token info as appropriate) + } + + token = token.Substring(6).Trim(); // remove "TOKEN " prefix + + if (token.Length == 0) + { + return null; + } + + return token; + } + /// /// Sends user data (domain, username, etc.) over the HyperVSocket using Unicode encoding. /// diff --git a/test/fuzzing/FuzzingApp/Program.cs b/test/fuzzing/FuzzingApp/Program.cs new file mode 100644 index 0000000000..77dee97165 --- /dev/null +++ b/test/fuzzing/FuzzingApp/Program.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Text; +using SharpFuzz; +using System.Management.Automation.Remoting; + +namespace FuzzTests +{ + public static class Program + { + public static void Main(string[] args) + { + FuzzTargetMethod(args); + } + + public static void FuzzTargetMethod(string[] args) + { + if (args == null) + { + Console.WriteLine("args was null"); + args = Array.Empty(); + } + + try + { + Fuzzer.LibFuzzer.Run(Target.ExtractToken); + } + catch (System.ArgumentNullException nex) + { + Console.WriteLine($"ArgumentNullException in main: {nex.Message}"); + Console.WriteLine($"Stack Trace: {nex.StackTrace}"); + Environment.Exit(1); + } + catch (Exception ex) + { + Console.WriteLine($"Exception in main: {ex.Message}"); + Console.WriteLine($"Exception type: {ex.GetType()}"); + Console.WriteLine($"Stack Trace: {ex.StackTrace}"); + Environment.Exit(1); + } + } + } +} diff --git a/test/fuzzing/FuzzingApp/Target.cs b/test/fuzzing/FuzzingApp/Target.cs new file mode 100644 index 0000000000..033f651762 --- /dev/null +++ b/test/fuzzing/FuzzingApp/Target.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Text; +using SharpFuzz; +using System.Management.Automation.Remoting; + +namespace FuzzTests +{ + public static class Target + { + public static void ExtractToken(ReadOnlySpan tokenResponse) + { + RemoteSessionHyperVSocketClient.ExtractToken(tokenResponse); + } + } +} + diff --git a/test/fuzzing/FuzzingApp/powershell-fuzz-tests.csproj b/test/fuzzing/FuzzingApp/powershell-fuzz-tests.csproj new file mode 100644 index 0000000000..fc6549122f --- /dev/null +++ b/test/fuzzing/FuzzingApp/powershell-fuzz-tests.csproj @@ -0,0 +1,29 @@ + + + + PowerShell Fuzzing + powershell-fuzz-tests + + + + Exe + net10.0 + enable + enable + + + + true + ../../../src/signing/visualstudiopublic.snk + true + + + + + + + + + + + diff --git a/test/fuzzing/inputs/maxinput b/test/fuzzing/inputs/maxinput new file mode 100644 index 0000000000..6773a896b3 --- /dev/null +++ b/test/fuzzing/inputs/maxinput @@ -0,0 +1 @@ +TOKEN abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/= \ No newline at end of file diff --git a/test/fuzzing/inputs/mininput b/test/fuzzing/inputs/mininput new file mode 100644 index 0000000000..c9b91c737c --- /dev/null +++ b/test/fuzzing/inputs/mininput @@ -0,0 +1 @@ +TOKEN TQ \ No newline at end of file diff --git a/test/fuzzing/runFuzzer.ps1 b/test/fuzzing/runFuzzer.ps1 new file mode 100644 index 0000000000..94000b59bb --- /dev/null +++ b/test/fuzzing/runFuzzer.ps1 @@ -0,0 +1,75 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +param ( + [string]$libFuzzer = ".\libfuzzer-dotnet-windows.exe", + [string]$project = ".\FuzzingApp\powershell-fuzz-tests.csproj", + [Parameter(Mandatory=$true)] + [string]$corpus, + [string]$command = "sharpfuzz.exe" +) + +Set-StrictMode -Version Latest + +$outputDir = "out" + +if (Test-Path $outputDir) { + Remove-Item -Recurse -Force $outputDir +} + +Write-Host "dotnet publish $project -c release -o $outputDir" +dotnet publish $project -c release -o $outputDir +Write-Host "build completed" + +$projectName = (Get-Item $project).BaseName +$projectDll = "$projectName.dll" +$project = Join-Path $outputDir $projectDll + +$exclusions = @( + "dnlib.dll", + "SharpFuzz.dll", + "SharpFuzz.Common.dll" +) + +$exclusions += $projectDll + +Write-Host "instrumenting: $project" +& $command $project Target +Write-Host "done instrumenting $project" + +$fuzzingTargets = Get-ChildItem $outputDir -Filter *.dll ` +| Where-Object { $_.Name -notin $exclusions } ` +| Where-Object { $_.Name -notlike "System.*.dll" } +| Where-Object { $_.Name -notlike "Newtonsoft.*.dll" } +| Where-Object { $_.Name -notlike "Microsoft.*.dll" } + +foreach ($fuzzingTarget in $fuzzingTargets) { + Write-Output "Instrumenting $fuzzingTarget" + & $command $fuzzingTarget.FullName + + if ($LastExitCode -ne 0) { + Write-Error "An error occurred while instrumenting $fuzzingTarget" + exit 1 + } +} + +$smaDllPath = Join-Path $outputDir "System.Management.Automation.dll" + +Write-Host "instrumenting: $smaDllPath" +& $command $smaDllPath Remoting +Write-Host "done instrumenting: $smaDllPath" + +$fuzzingTargets += $projectDll +$fuzzingTargets += $smaDllPath + +if (($fuzzingTargets | Measure-Object).Count -eq 0) { + Write-Error "No fuzzing targets found" + exit 1 +} + +$outputPath = Join-Path $outputDir "output.txt" + +Write-Host "launching fuzzer on $project" +Write-Host "$libFuzzer --target_path=dotnet --target_arg=$project $corpus" +& $libFuzzer --target_path=dotnet --target_arg=$project $corpus -max_len=1024 2>&1 ` +| Tee-Object -FilePath $outputPath