github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/oci/layer/layer_fuzzer.go (about) 1 //go:build gofuzz 2 // +build gofuzz 3 4 /* 5 * umoci: Umoci Modifies Open Containers' Images 6 * Copyright (C) 2021 SUSE LLC 7 * 8 * Licensed under the Apache License, Version 2.0 (the "License"); 9 * you may not use this file except in compliance with the License. 10 * You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, software 15 * distributed under the License is distributed on an "AS IS" BASIS, 16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 * See the License for the specific language governing permissions and 18 * limitations under the License. 19 */ 20 21 package layer 22 23 import ( 24 "bytes" 25 "encoding/base64" 26 "io" 27 28 "github.com/opencontainers/go-digest" 29 "github.com/opencontainers/umoci/oci/cas/dir" 30 31 "context" 32 33 "github.com/apex/log" 34 "github.com/opencontainers/image-spec/specs-go" 35 ispec "github.com/opencontainers/image-spec/specs-go/v1" 36 rspec "github.com/opencontainers/runtime-spec/specs-go" 37 "github.com/opencontainers/umoci/oci/casext" 38 39 "archive/tar" 40 41 "io/ioutil" 42 "os" 43 "path/filepath" 44 "unicode" 45 46 fuzz "github.com/AdaLogics/go-fuzz-headers" 47 fuzzheaders "github.com/AdaLogics/go-fuzz-headers" 48 "github.com/vbatts/go-mtree" 49 ) 50 51 // createRandomFile is a helper function 52 func createRandomFile(dirpath string, filename []byte, filecontents []byte) error { 53 fileP := filepath.Join(dirpath, string(filename)) 54 if err := ioutil.WriteFile(fileP, filecontents, 0644); err != nil { 55 return err 56 } 57 defer os.Remove(fileP) 58 return nil 59 } 60 61 // createRandomDir is a helper function 62 func createRandomDir(basedir string, dirname []byte, dirArray []string) ([]string, error) { 63 dirPath := filepath.Join(basedir, string(dirname)) 64 if err := os.MkdirAll(dirPath, 0755); err != nil { 65 return dirArray, err 66 } 67 defer os.RemoveAll(dirPath) 68 dirArray = append(dirArray, string(dirname)) 69 return dirArray, nil 70 } 71 72 // isLetter is a helper function 73 func isLetter(input []byte) bool { 74 s := string(input) 75 for _, r := range s { 76 if !unicode.IsLetter(r) { 77 return false 78 } 79 } 80 return true 81 } 82 83 // FuzzGenerateLayer implements a fuzzer 84 // that targets layer.GenerateLayer(). 85 func FuzzGenerateLayer(data []byte) int { 86 if len(data) < 5 { 87 return -1 88 } 89 if !fuzzheaders.IsDivisibleBy(len(data), 2) { 90 return -1 91 } 92 half := len(data) / 2 93 firstHalf := data[:half] 94 f1 := fuzzheaders.NewConsumer(firstHalf) 95 err := f1.Split(3, 30) 96 if err != nil { 97 return -1 98 } 99 100 secondHalf := data[half:] 101 f2 := fuzzheaders.NewConsumer(secondHalf) 102 err = f2.Split(3, 30) 103 if err != nil { 104 return -1 105 } 106 baseDir := "fuzz-base-dir" 107 if err := os.MkdirAll(baseDir, 0755); err != nil { 108 return -1 109 } 110 defer os.RemoveAll(baseDir) 111 112 var dirArray []string 113 iteration := 0 114 chunkSize := len(f1.RestOfArray) / f1.NumberOfCalls 115 for i := 0; i < len(f1.RestOfArray); i = i + chunkSize { 116 from := i //lower 117 to := i + chunkSize //upper 118 inputData := firstHalf[from:to] 119 if len(inputData) > 6 && isLetter(inputData[:5]) { 120 dirArray, err = createRandomDir(baseDir, inputData[:5], dirArray) 121 if err != nil { 122 continue 123 } 124 } else { 125 if len(dirArray) == 0 { 126 continue 127 } 128 dirp := int(inputData[0]) % len(dirArray) 129 fp := filepath.Join(baseDir, dirArray[dirp]) 130 if len(inputData) > 10 { 131 filename := inputData[5:8] 132 err = createRandomFile(fp, filename, inputData[8:]) 133 if err != nil { 134 continue 135 } 136 } 137 } 138 iteration++ 139 } 140 141 // Get initial. 142 initDh, err := mtree.Walk(baseDir, nil, append(mtree.DefaultKeywords, "sha256digest"), nil) 143 if err != nil { 144 return 0 145 } 146 iteration = 0 147 chunkSize = len(f2.RestOfArray) / f2.NumberOfCalls 148 for i := 0; i < len(f2.RestOfArray); i = i + chunkSize { 149 from := i //lower 150 to := i + chunkSize //upper 151 inputData := secondHalf[from:to] 152 if len(inputData) > 6 && isLetter(inputData[:5]) { 153 dirArray, err = createRandomDir(baseDir, inputData[:5], dirArray) 154 if err != nil { 155 continue 156 } 157 } else { 158 if len(dirArray) == 0 { 159 continue 160 } 161 dirp := int(inputData[0]) % len(dirArray) 162 fp := filepath.Join(baseDir, dirArray[dirp]) 163 if len(inputData) > 10 { 164 filename := inputData[5:8] 165 err = createRandomFile(fp, filename, inputData[8:]) 166 if err != nil { 167 continue 168 } 169 } 170 } 171 iteration++ 172 } 173 174 // Get post. 175 postDh, err := mtree.Walk(baseDir, nil, initDh.UsedKeywords(), nil) 176 if err != nil { 177 return 0 178 } 179 180 diffs, err := mtree.Compare(initDh, postDh, initDh.UsedKeywords()) 181 if err != nil { 182 return -1 183 } 184 reader, err := GenerateLayer(baseDir, diffs, &RepackOptions{}) 185 if err != nil { 186 return -1 187 } 188 defer reader.Close() 189 190 tr := tar.NewReader(reader) 191 for { 192 _, err = tr.Next() 193 if err != nil { 194 break 195 } 196 } 197 return 1 198 } 199 200 // mustDecodeString is a helper function 201 func mustDecodeString(s string) []byte { 202 b, _ := base64.StdEncoding.DecodeString(s) 203 return b 204 } 205 206 // makeImage is a helper function 207 func makeImage(base641, base642 string) (string, ispec.Manifest, casext.Engine, error) { 208 209 ctx := context.Background() 210 211 var layers = []struct { 212 base64 string 213 digest digest.Digest 214 }{ 215 { 216 base64: base641, 217 digest: digest.NewDigestFromHex(digest.SHA256.String(), "e489a16a8ca0d682394867ad8a8183f0a47cbad80b3134a83412a6796ad9242a"), 218 }, 219 { 220 base64: base642, 221 digest: digest.NewDigestFromHex(digest.SHA256.String(), "39f100ed000b187ba74b3132cc207c63ad1765adaeb783aa7f242f1f7b6f5ea2"), 222 }, 223 } 224 225 root, err := ioutil.TempDir("", "umoci-TestUnpackManifestCustomLayer") 226 if err != nil { 227 return "nil", ispec.Manifest{}, casext.Engine{}, err 228 } 229 230 // Create our image. 231 image := filepath.Join(root, "image") 232 if err := dir.Create(image); err != nil { 233 return "nil", ispec.Manifest{}, casext.Engine{}, err 234 } 235 engine, err := dir.Open(image) 236 if err != nil { 237 return "nil", ispec.Manifest{}, casext.Engine{}, err 238 } 239 engineExt := casext.NewEngine(engine) 240 241 // Set up the CAS and an image from the above layers. 242 var layerDigests []digest.Digest 243 var layerDescriptors []ispec.Descriptor 244 for _, layer := range layers { 245 var layerReader io.Reader 246 247 layerReader = bytes.NewBuffer(mustDecodeString(layer.base64)) 248 layerDigest, layerSize, err := engineExt.PutBlob(ctx, layerReader) 249 if err != nil { 250 return "nil", ispec.Manifest{}, casext.Engine{}, err 251 } 252 253 layerDigests = append(layerDigests, layer.digest) 254 layerDescriptors = append(layerDescriptors, ispec.Descriptor{ 255 MediaType: ispec.MediaTypeImageLayerGzip, 256 Digest: layerDigest, 257 Size: layerSize, 258 }) 259 } 260 261 // Create the config. 262 config := ispec.Image{ 263 OS: "linux", 264 RootFS: ispec.RootFS{ 265 Type: "layers", 266 DiffIDs: layerDigests, 267 }, 268 } 269 configDigest, configSize, err := engineExt.PutBlobJSON(ctx, config) 270 if err != nil { 271 return "nil", ispec.Manifest{}, casext.Engine{}, err 272 } 273 configDescriptor := ispec.Descriptor{ 274 MediaType: ispec.MediaTypeImageConfig, 275 Digest: configDigest, 276 Size: configSize, 277 } 278 279 // Create the manifest. 280 manifest := ispec.Manifest{ 281 Versioned: specs.Versioned{ 282 SchemaVersion: 2, 283 }, 284 MediaType: ispec.MediaTypeImageManifest, 285 Config: configDescriptor, 286 Layers: layerDescriptors, 287 } 288 289 return root, manifest, engineExt, nil 290 } 291 292 // FuzzUnpack implements a fuzzer 293 // that targets UnpackManifest(). 294 func FuzzUnpack(data []byte) int { 295 // We would like as little log output as possible: 296 level, err := log.ParseLevel("fatal") 297 if err != nil { 298 return -1 299 } 300 log.SetLevel(level) 301 ctx := context.Background() 302 c := fuzz.NewConsumer(data) 303 base641, err := c.GetString() 304 if err != nil { 305 return -1 306 } 307 308 base642, err := c.GetString() 309 if err != nil { 310 return -1 311 } 312 root, manifest, engineExt, err := makeImage(base641, base642) 313 if err != nil { 314 return 0 315 } 316 defer os.RemoveAll(root) 317 318 bundle, err := ioutil.TempDir("", "umoci-TestUnpackManifestCustomLayer_bundle") 319 if err != nil { 320 return 0 321 } 322 defer os.RemoveAll(bundle) 323 324 unpackOptions := &UnpackOptions{MapOptions: MapOptions{ 325 UIDMappings: []rspec.LinuxIDMapping{ 326 {HostID: uint32(os.Geteuid()), ContainerID: 0, Size: 1}, 327 {HostID: uint32(os.Geteuid()), ContainerID: 1000, Size: 1}, 328 }, 329 GIDMappings: []rspec.LinuxIDMapping{ 330 {HostID: uint32(os.Getegid()), ContainerID: 0, Size: 1}, 331 {HostID: uint32(os.Getegid()), ContainerID: 100, Size: 1}, 332 }, 333 Rootless: os.Geteuid() != 0, 334 }} 335 336 called := false 337 unpackOptions.AfterLayerUnpack = func(m ispec.Manifest, d ispec.Descriptor) error { 338 called = true 339 return nil 340 } 341 342 _ = UnpackManifest(ctx, engineExt, bundle, manifest, unpackOptions) 343 344 return 1 345 }