github.com/rawahars/moby@v24.0.4+incompatible/pkg/archive/utils_test.go (about) 1 package archive // import "github.com/docker/docker/pkg/archive" 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "time" 11 ) 12 13 var testUntarFns = map[string]func(string, io.Reader) error{ 14 "untar": func(dest string, r io.Reader) error { 15 return Untar(r, dest, nil) 16 }, 17 "applylayer": func(dest string, r io.Reader) error { 18 _, err := ApplyLayer(dest, r) 19 return err 20 }, 21 } 22 23 // testBreakout is a helper function that, within the provided `tmpdir` directory, 24 // creates a `victim` folder with a generated `hello` file in it. 25 // `untar` extracts to a directory named `dest`, the tar file created from `headers`. 26 // 27 // Here are the tested scenarios: 28 // - removed `victim` folder (write) 29 // - removed files from `victim` folder (write) 30 // - new files in `victim` folder (write) 31 // - modified files in `victim` folder (write) 32 // - file in `dest` with same content as `victim/hello` (read) 33 // 34 // When using testBreakout make sure you cover one of the scenarios listed above. 35 func testBreakout(untarFn string, tmpdir string, headers []*tar.Header) error { 36 tmpdir, err := os.MkdirTemp("", tmpdir) 37 if err != nil { 38 return err 39 } 40 defer os.RemoveAll(tmpdir) 41 42 dest := filepath.Join(tmpdir, "dest") 43 if err := os.Mkdir(dest, 0755); err != nil { 44 return err 45 } 46 47 victim := filepath.Join(tmpdir, "victim") 48 if err := os.Mkdir(victim, 0755); err != nil { 49 return err 50 } 51 hello := filepath.Join(victim, "hello") 52 helloData, err := time.Now().MarshalText() 53 if err != nil { 54 return err 55 } 56 if err := os.WriteFile(hello, helloData, 0644); err != nil { 57 return err 58 } 59 helloStat, err := os.Stat(hello) 60 if err != nil { 61 return err 62 } 63 64 reader, writer := io.Pipe() 65 go func() { 66 t := tar.NewWriter(writer) 67 for _, hdr := range headers { 68 t.WriteHeader(hdr) 69 } 70 t.Close() 71 }() 72 73 untar := testUntarFns[untarFn] 74 if untar == nil { 75 return fmt.Errorf("could not find untar function %q in testUntarFns", untarFn) 76 } 77 if err := untar(dest, reader); err != nil { 78 if _, ok := err.(breakoutError); !ok { 79 // If untar returns an error unrelated to an archive breakout, 80 // then consider this an unexpected error and abort. 81 return err 82 } 83 // Here, untar detected the breakout. 84 // Let's move on verifying that indeed there was no breakout. 85 fmt.Printf("breakoutError: %v\n", err) 86 } 87 88 // Check victim folder 89 f, err := os.Open(victim) 90 if err != nil { 91 // codepath taken if victim folder was removed 92 return fmt.Errorf("archive breakout: error reading %q: %v", victim, err) 93 } 94 defer f.Close() 95 96 // Check contents of victim folder 97 // 98 // We are only interested in getting 2 files from the victim folder, because if all is well 99 // we expect only one result, the `hello` file. If there is a second result, it cannot 100 // hold the same name `hello` and we assume that a new file got created in the victim folder. 101 // That is enough to detect an archive breakout. 102 names, err := f.Readdirnames(2) 103 if err != nil { 104 // codepath taken if victim is not a folder 105 return fmt.Errorf("archive breakout: error reading directory content of %q: %v", victim, err) 106 } 107 for _, name := range names { 108 if name != "hello" { 109 // codepath taken if new file was created in victim folder 110 return fmt.Errorf("archive breakout: new file %q", name) 111 } 112 } 113 114 // Check victim/hello 115 f, err = os.Open(hello) 116 if err != nil { 117 // codepath taken if read permissions were removed 118 return fmt.Errorf("archive breakout: could not lstat %q: %v", hello, err) 119 } 120 defer f.Close() 121 b, err := io.ReadAll(f) 122 if err != nil { 123 return err 124 } 125 fi, err := f.Stat() 126 if err != nil { 127 return err 128 } 129 if helloStat.IsDir() != fi.IsDir() || 130 // TODO: cannot check for fi.ModTime() change 131 helloStat.Mode() != fi.Mode() || 132 helloStat.Size() != fi.Size() || 133 !bytes.Equal(helloData, b) { 134 // codepath taken if hello has been modified 135 return fmt.Errorf("archive breakout: file %q has been modified. Contents: expected=%q, got=%q. FileInfo: expected=%#v, got=%#v", hello, helloData, b, helloStat, fi) 136 } 137 138 // Check that nothing in dest/ has the same content as victim/hello. 139 // Since victim/hello was generated with time.Now(), it is safe to assume 140 // that any file whose content matches exactly victim/hello, managed somehow 141 // to access victim/hello. 142 return filepath.WalkDir(dest, func(path string, info os.DirEntry, err error) error { 143 if info.IsDir() { 144 if err != nil { 145 // skip directory if error 146 return filepath.SkipDir 147 } 148 // enter directory 149 return nil 150 } 151 if err != nil { 152 // skip file if error 153 return nil 154 } 155 b, err := os.ReadFile(path) 156 if err != nil { 157 // Houston, we have a problem. Aborting (space)walk. 158 return err 159 } 160 if bytes.Equal(helloData, b) { 161 return fmt.Errorf("archive breakout: file %q has been accessed via %q", hello, path) 162 } 163 return nil 164 }) 165 }