github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/image/compression_optimized.go (about) 1 // Copyright 2022 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 //go:build !windows && !386 && !arm 5 6 package image 7 8 import ( 9 "bytes" 10 "compress/zlib" 11 "fmt" 12 "io" 13 "sync" 14 "syscall" 15 "unsafe" 16 ) 17 18 // Temporary scratch data used by the decompression procedure. 19 type decompressScratch struct { 20 r bytes.Reader 21 zr io.Reader 22 buf []byte 23 } 24 25 // This is just for memory consumption estimation, does not need to be precise. 26 const pageSize = 4 << 10 27 28 var decompressPool = sync.Pool{New: func() interface{} { 29 return &decompressScratch{ 30 buf: make([]byte, pageSize), 31 } 32 }} 33 34 func mustDecompress(compressed []byte) (data []byte, dtor func()) { 35 // Optimized decompression procedure that is ~2x faster than a naive version 36 // and consumes significantly less memory and generates less garbage. 37 // Images tend to contain lots of 0s, especially the larger images. 38 // The main idea is that we mmap a buffer and then don't write 0s into it 39 // (since it already contains all 0s). As the result if a page is all 0s 40 // then we don't page it in and don't consume memory for it. 41 // Executor uses the same optimization during decompression. 42 scratch := decompressPool.Get().(*decompressScratch) 43 defer decompressPool.Put(scratch) 44 scratch.r.Reset(compressed) 45 if scratch.zr == nil { 46 zr, err := zlib.NewReader(&scratch.r) 47 if err != nil { 48 panic(err) 49 } 50 scratch.zr = zr 51 } else { 52 if err := scratch.zr.(zlib.Resetter).Reset(&scratch.r, nil); err != nil { 53 panic(err) 54 } 55 } 56 // We don't know the size of the uncompressed image. 57 // We could uncompress it into ioutil.Discard first, then allocate memory and uncompress second time 58 // (and it's still faster than the naive uncompress into bytes.Buffer!). 59 // But we know maximum size of images, so just mmap the max size. 60 // It's fast and unused part does not consume memory. 61 // Note: executor/common_zlib.h also knows this const. 62 const maxImageSize = 132 << 20 63 var err error 64 data, err = syscall.Mmap(-1, 0, maxImageSize, syscall.PROT_READ|syscall.PROT_WRITE, 65 syscall.MAP_ANON|syscall.MAP_PRIVATE) 66 if err != nil { 67 panic(err) 68 } 69 pages := 0 70 dtor = func() { 71 StatImages.Add(-1) 72 StatMemory.Add(int64(-pages * pageSize)) 73 if err := syscall.Munmap(data[:maxImageSize]); err != nil { 74 panic(err) 75 } 76 } 77 pagedIn := 0 78 offset := 0 79 for { 80 n, err := scratch.zr.Read(scratch.buf) 81 if err != nil && err != io.EOF { 82 panic(err) 83 } 84 if n == 0 { 85 break 86 } 87 if offset+n > len(data) { 88 panic(fmt.Sprintf("bad image size: offset=%v n=%v data=%v", offset, n, len(data))) 89 } 90 // Copy word-at-a-time and avoid bounds checks in the loop, 91 // this is considerably faster than a naive byte loop. 92 // We already checked bounds above. 93 type word uint64 94 const wordSize = unsafe.Sizeof(word(0)) 95 // Don't copy the last word b/c otherwise we calculate pointer outside of scratch.buf object 96 // on the last iteration. We don't use it, but unsafe rules prohibit even calculating 97 // such pointers. Alternatively we could add 8 unused bytes to scratch.buf, but it will 98 // play badly with memory allocator size classes (it will consume whole additional page, 99 // or whatever is the alignment for such large objects). We could also break from the middle 100 // of the loop before updating src/dst pointers, but it hurts codegen a lot (compilers like 101 // canonical loop forms). 102 hasData := false 103 words := uintptr(n-1) / wordSize 104 src := (*word)(unsafe.Pointer(&scratch.buf[0])) 105 dst := (*word)(unsafe.Pointer(&data[offset])) 106 for i := uintptr(0); i < words; i++ { 107 if *src != 0 { 108 *dst = *src 109 } 110 src = (*word)(unsafe.Pointer(uintptr(unsafe.Pointer(src)) + wordSize)) 111 dst = (*word)(unsafe.Pointer(uintptr(unsafe.Pointer(dst)) + wordSize)) 112 hasData = true 113 } 114 // Copy any remaining trailing bytes. 115 for i := words * wordSize; i < uintptr(n); i++ { 116 v := scratch.buf[i] 117 if v != 0 { 118 data[uintptr(offset)+i] = v 119 hasData = true 120 } 121 } 122 if hasData && offset >= pagedIn { 123 pagedIn = (offset + n + pageSize - 1) & ^(pageSize - 1) 124 pages++ 125 } 126 offset += n 127 } 128 data = data[:offset] 129 StatImages.Add(1) 130 StatMemory.Add(int64(pages * pageSize)) 131 return 132 }