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