@@ -10,7 +10,6 @@ import (
10
10
"debug/pe"
11
11
"errors"
12
12
"fmt"
13
- "math/rand"
14
13
"os"
15
14
"path/filepath"
16
15
"runtime"
@@ -19,6 +18,7 @@ import (
19
18
"syscall"
20
19
"testing"
21
20
"time"
21
+ "unicode/utf8"
22
22
"unsafe"
23
23
24
24
"golang.org/x/sys/windows"
@@ -562,78 +562,143 @@ func TestResourceExtraction(t *testing.T) {
562
562
}
563
563
}
564
564
565
- func TestCommandLineRecomposition (t * testing.T ) {
566
- const (
567
- maxCharsPerArg = 35
568
- maxArgsPerTrial = 80
569
- doubleQuoteProb = 4
570
- singleQuoteProb = 1
571
- backSlashProb = 3
572
- spaceProb = 1
573
- trials = 1000
574
- )
575
- randString := func (l int ) []rune {
576
- s := make ([]rune , l )
577
- for i := range s {
578
- s [i ] = rand .Int31 ()
579
- }
580
- return s
581
- }
582
- mungeString := func (s []rune , char rune , timesInTen int ) {
583
- if timesInTen < rand .Intn (10 )+ 1 || len (s ) == 0 {
584
- return
585
- }
586
- s [rand .Intn (len (s ))] = char
587
- }
588
- argStorage := make ([]string , maxArgsPerTrial + 1 )
589
- for i := 0 ; i < trials ; i ++ {
590
- args := argStorage [:rand .Intn (maxArgsPerTrial )+ 2 ]
591
- args [0 ] = "valid-filename-for-arg0"
592
- for j := 1 ; j < len (args ); j ++ {
593
- arg := randString (rand .Intn (maxCharsPerArg + 1 ))
594
- mungeString (arg , '"' , doubleQuoteProb )
595
- mungeString (arg , '\'' , singleQuoteProb )
596
- mungeString (arg , '\\' , backSlashProb )
597
- mungeString (arg , ' ' , spaceProb )
598
- args [j ] = string (arg )
565
+ func FuzzComposeCommandLine (f * testing.F ) {
566
+ f .Add (`C:\foo.exe /bar /baz "-bag qux"` )
567
+ f .Add (`"C:\Program Files\Go\bin\go.exe" env` )
568
+ f .Add (`C:\"Program Files"\Go\bin\go.exe env` )
569
+ f .Add (`C:\"Program Files"\Go\bin\go.exe env` )
570
+ f .Add (`C:\"Pro"gram Files\Go\bin\go.exe env` )
571
+ f .Add (`` )
572
+ f .Add (` ` )
573
+ f .Add (`W\"0` )
574
+ f .Add ("\" \f " )
575
+ f .Add ("\f " )
576
+ f .Add ("\x16 " )
577
+ f .Add (`"" ` + strings .Repeat ("a" , 8193 ))
578
+ f .Add (strings .Repeat (`"" ` , 8193 ))
579
+
580
+ f .Add ("\x00 abcd" )
581
+ f .Add ("ab\x00 cd" )
582
+ f .Add ("abcd\x00 " )
583
+ f .Add ("\x00 abcd\x00 " )
584
+ f .Add ("\x00 ab\x00 cd\x00 " )
585
+ f .Add ("\x00 \x00 \x00 " )
586
+ f .Add ("\x16 \x00 \x16 " )
587
+ f .Add (`C:\Program Files\Go\bin\go.exe` + "\x00 env" )
588
+ f .Add (`"C:\Program Files\Go\bin\go.exe"` + "\x00 env" )
589
+ f .Add (`C:\"Program Files"\Go\bin\go.exe` + "\x00 env" )
590
+ f .Add (`C:\"Pro"gram Files\Go\bin\go.exe` + "\x00 env" )
591
+ f .Add ("\x00 " + strings .Repeat ("a" , 8192 ))
592
+ f .Add (strings .Repeat ("\x00 " + strings .Repeat ("a" , 8192 ), 4 ))
593
+
594
+ f .Fuzz (func (t * testing.T , s string ) {
595
+ // DecomposeCommandLine is the “control” for our experiment:
596
+ // if it returns a particular list of arguments, then we know
597
+ // it must be possible to create an input string that produces
598
+ // exactly those arguments.
599
+ //
600
+ // However, DecomposeCommandLine returns an error if the string
601
+ // contains a NUL byte. In that case, we will fall back to
602
+ // strings.Split, and be a bit more permissive about the results.
603
+ args , err := windows .DecomposeCommandLine (s )
604
+ argsFromSplit := false
605
+
606
+ if err == nil {
607
+ if testing .Verbose () {
608
+ t .Logf ("DecomposeCommandLine(%#q) = %#q" , s , args )
609
+ }
610
+ } else {
611
+ t .Logf ("DecomposeCommandLine: %v" , err )
612
+ if ! strings .Contains (s , "\x00 " ) {
613
+ // The documentation for CommandLineToArgv takes for granted that
614
+ // the first argument is a valid file path, and doesn't describe any
615
+ // specific behavior for malformed arguments. Empirically it seems to
616
+ // tolerate anything we throw at it, but if we discover cases where it
617
+ // actually returns an error we might need to relax this check.
618
+ t .Fatal ("(error unexpected)" )
619
+ }
620
+
621
+ // Since DecomposeCommandLine can't handle this string,
622
+ // interpret it as the raw arguments to ComposeCommandLine.
623
+ args = strings .Split (s , "\x00 " )
624
+ argsFromSplit = true
625
+ for i , arg := range args {
626
+ if ! utf8 .ValidString (arg ) {
627
+ // We need to encode the arguments as UTF-16 to pass them to
628
+ // CommandLineToArgvW, so skip inputs that are not valid: they might
629
+ // have one or more runes converted to replacement characters.
630
+ t .Skipf ("skipping: input %d is not valid UTF-8" , i )
631
+ }
632
+ if len (arg ) > 8192 {
633
+ // CommandLineToArgvW seems to truncate each argument after 8192
634
+ // UTF-16 code units, although this behavior is not documented. Since
635
+ // it isn't documented, we shouldn't rely on it one way or the other,
636
+ // so skip the input to tell the fuzzer to try a different approach.
637
+ enc , _ := windows .UTF16FromString (arg )
638
+ if len (enc ) > 8192 {
639
+ t .Skipf ("skipping: input %d encodes to more than 8192 UTF-16 code units" , i )
640
+ }
641
+ }
642
+ }
643
+ if testing .Verbose () {
644
+ t .Logf ("using input: %#q" , args )
645
+ }
599
646
}
647
+
648
+ // It's ok if we compose a different command line than what was read.
649
+ // Just check that we are able to compose something that round-trips
650
+ // to the same results as the original.
600
651
commandLine := windows .ComposeCommandLine (args )
601
- decomposedArgs , err := windows .DecomposeCommandLine (commandLine )
652
+ t .Logf ("ComposeCommandLine(_) = %#q" , commandLine )
653
+
654
+ got , err := windows .DecomposeCommandLine (commandLine )
602
655
if err != nil {
603
- t .Errorf ("Unable to decompose %#q made from %v: %v" , commandLine , args , err )
604
- continue
656
+ t .Fatalf ("DecomposeCommandLine: unexpected error: %v" , err )
605
657
}
606
- if len (decomposedArgs ) != len (args ) {
607
- t .Errorf ("Incorrect decomposition length from %v to %#q to %v" , args , commandLine , decomposedArgs )
608
- continue
658
+ if testing .Verbose () {
659
+ t .Logf ("DecomposeCommandLine(_) = %#q" , got )
609
660
}
610
- badMatches := make ([]int , 0 , len (args ))
661
+
662
+ var badMatches []int
611
663
for i := range args {
612
- if args [i ] != decomposedArgs [i ] {
664
+ if i >= len (got ) {
665
+ badMatches = append (badMatches , i )
666
+ continue
667
+ }
668
+ want := args [i ]
669
+ if got [i ] != want {
670
+ if i == 0 && argsFromSplit {
671
+ // It is possible that args[0] cannot be encoded exactly, because
672
+ // CommandLineToArgvW doesn't unescape that argument in the same way
673
+ // as the others: since the first argument is assumed to be the name
674
+ // of the program itself, we only have the option of quoted or not.
675
+ //
676
+ // If args[0] contains a space or control character, we must quote it
677
+ // to avoid it being split into multiple arguments.
678
+ // If args[0] already starts with a quote character, we have no way
679
+ // to indicate that that character is part of the literal argument.
680
+ // In either case, if the string already contains a quote character
681
+ // we must avoid misinterpriting that character as the end of the
682
+ // quoted argument string.
683
+ //
684
+ // Unfortunately, ComposeCommandLine does not return an error, so we
685
+ // can't report existing quote characters as errors.
686
+ // Instead, we strip out the problematic quote characters from the
687
+ // argument, and quote the remainder.
688
+ // For paths like C:\"Program Files"\Go\bin\go.exe that is arguably
689
+ // what the caller intended anyway, and for other strings it seems
690
+ // less harmful than corrupting the subsequent arguments.
691
+ if got [i ] == strings .ReplaceAll (want , `"` , `` ) {
692
+ continue
693
+ }
694
+ }
613
695
badMatches = append (badMatches , i )
614
696
}
615
697
}
616
698
if len (badMatches ) != 0 {
617
- t .Errorf ("Incorrect decomposition at indices %v from %v to %#q to %v" , badMatches , args , commandLine , decomposedArgs )
618
- continue
699
+ t .Errorf ("Incorrect decomposition at indices: %v" , badMatches )
619
700
}
620
- }
621
-
622
- // check that windows.DecomposeCommandLine returns error for strings with NUL
623
- testsWithNUL := []string {
624
- "\x00 abcd" ,
625
- "ab\x00 cd" ,
626
- "abcd\x00 " ,
627
- "\x00 abcd\x00 " ,
628
- "\x00 ab\x00 cd\x00 " ,
629
- "\x00 \x00 \x00 " ,
630
- }
631
- for _ , test := range testsWithNUL {
632
- _ , err := windows .DecomposeCommandLine (test )
633
- if err == nil {
634
- t .Errorf ("Failed to return error while decomposing %#q string with NUL inside" , test )
635
- }
636
- }
701
+ })
637
702
}
638
703
639
704
func TestWinVerifyTrust (t * testing.T ) {
0 commit comments