github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/gadget/raw.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package gadget 21 22 import ( 23 "bytes" 24 "crypto" 25 _ "crypto/sha1" 26 "fmt" 27 "io" 28 "os" 29 "path/filepath" 30 31 "github.com/snapcore/snapd/osutil" 32 ) 33 34 // TODO: RawStructureWriter should not be exported 35 36 // RawStructureWriter implements support for writing raw (bare) structures. 37 type RawStructureWriter struct { 38 contentDir string 39 ps *LaidOutStructure 40 } 41 42 // NewRawStructureWriter returns a writer for the given structure, that will load 43 // the structure content data from the provided gadget content directory. 44 func NewRawStructureWriter(contentDir string, ps *LaidOutStructure) (*RawStructureWriter, error) { 45 if ps == nil { 46 return nil, fmt.Errorf("internal error: *LaidOutStructure is nil") 47 } 48 if ps.HasFilesystem() { 49 return nil, fmt.Errorf("internal error: structure %s has a filesystem", ps) 50 } 51 if contentDir == "" { 52 return nil, fmt.Errorf("internal error: gadget content directory cannot be unset") 53 } 54 rw := &RawStructureWriter{ 55 contentDir: contentDir, 56 ps: ps, 57 } 58 return rw, nil 59 } 60 61 // writeRawStream writes the input stream in that corresponds to provided 62 // laid out content. The number of bytes read from input stream must match 63 // exactly the declared size of the content entry. 64 func writeRawStream(out io.WriteSeeker, pc *LaidOutContent, in io.Reader) error { 65 if _, err := out.Seek(int64(pc.StartOffset), io.SeekStart); err != nil { 66 return fmt.Errorf("cannot seek to content start offset 0x%x: %v", pc.StartOffset, err) 67 } 68 69 _, err := io.CopyN(out, in, int64(pc.Size)) 70 if err != nil { 71 return fmt.Errorf("cannot write image: %v", err) 72 } 73 return nil 74 } 75 76 // writeRawImage writes a single image described by a laid out content entry. 77 func (r *RawStructureWriter) writeRawImage(out io.WriteSeeker, pc *LaidOutContent) error { 78 if pc.Image == "" { 79 return fmt.Errorf("internal error: no image defined") 80 } 81 img, err := os.Open(filepath.Join(r.contentDir, pc.Image)) 82 if err != nil { 83 return fmt.Errorf("cannot open image file: %v", err) 84 } 85 defer img.Close() 86 87 return writeRawStream(out, pc, img) 88 } 89 90 // Write will write whole contents of a structure into the output stream. 91 func (r *RawStructureWriter) Write(out io.WriteSeeker) error { 92 for _, pc := range r.ps.LaidOutContent { 93 if err := r.writeRawImage(out, &pc); err != nil { 94 return fmt.Errorf("failed to write image %v: %v", pc, err) 95 } 96 } 97 return nil 98 } 99 100 // rawStructureUpdater implements support for updating raw (bare) structures. 101 type rawStructureUpdater struct { 102 *RawStructureWriter 103 backupDir string 104 deviceLookup deviceLookupFunc 105 } 106 107 type deviceLookupFunc func(ps *LaidOutStructure) (device string, offs Size, err error) 108 109 // newRawStructureUpdater returns an updater for the given raw (bare) structure. 110 // Update data will be loaded from the provided gadget content directory. 111 // Backups of replaced structures are temporarily kept in the rollback 112 // directory. 113 func newRawStructureUpdater(contentDir string, ps *LaidOutStructure, backupDir string, deviceLookup deviceLookupFunc) (*rawStructureUpdater, error) { 114 if deviceLookup == nil { 115 return nil, fmt.Errorf("internal error: device lookup helper must be provided") 116 } 117 if backupDir == "" { 118 return nil, fmt.Errorf("internal error: backup directory cannot be unset") 119 } 120 121 rw, err := NewRawStructureWriter(contentDir, ps) 122 if err != nil { 123 return nil, err 124 } 125 ru := &rawStructureUpdater{ 126 RawStructureWriter: rw, 127 backupDir: backupDir, 128 deviceLookup: deviceLookup, 129 } 130 return ru, nil 131 } 132 133 func rawContentBackupPath(backupDir string, ps *LaidOutStructure, pc *LaidOutContent) string { 134 return filepath.Join(backupDir, fmt.Sprintf("struct-%v-%v", ps.Index, pc.Index)) 135 } 136 137 func (r *rawStructureUpdater) backupOrCheckpointContent(disk io.ReadSeeker, pc *LaidOutContent) error { 138 backupPath := rawContentBackupPath(r.backupDir, r.ps, pc) 139 backupName := backupPath + ".backup" 140 sameName := backupPath + ".same" 141 142 if osutil.FileExists(backupName) || osutil.FileExists(sameName) { 143 // already have a backup or the image was found to be identical 144 // before 145 return nil 146 } 147 148 if _, err := disk.Seek(int64(pc.StartOffset), io.SeekStart); err != nil { 149 return fmt.Errorf("cannot seek to structure's start offset: %v", err) 150 } 151 152 // copy out at most the size of updated content 153 lr := io.LimitReader(disk, int64(pc.Size)) 154 155 // backup the original content 156 backup, err := osutil.NewAtomicFile(backupName, 0644, 0, osutil.NoChown, osutil.NoChown) 157 if err != nil { 158 return fmt.Errorf("cannot create backup file: %v", err) 159 } 160 // becomes a noop if canceled 161 defer backup.Commit() 162 163 // checksum the original data while it's being copied 164 origHash := crypto.SHA1.New() 165 htr := io.TeeReader(lr, origHash) 166 167 _, err = io.CopyN(backup, htr, int64(pc.Size)) 168 if err != nil { 169 defer backup.Cancel() 170 return fmt.Errorf("cannot backup original image: %v", err) 171 } 172 173 // digest of the update 174 updateDigest, _, err := osutil.FileDigest(filepath.Join(r.contentDir, pc.Image), crypto.SHA1) 175 if err != nil { 176 defer backup.Cancel() 177 return fmt.Errorf("cannot checksum update image: %v", err) 178 } 179 // digest of the currently present data 180 origDigest := origHash.Sum(nil) 181 182 if bytes.Equal(origDigest, updateDigest) { 183 // files are identical, no update needed 184 if err := osutil.AtomicWriteFile(sameName, nil, 0644, 0); err != nil { 185 return fmt.Errorf("cannot create a checkpoint file: %v", err) 186 } 187 188 // makes the previous commit a noop 189 backup.Cancel() 190 } 191 192 return nil 193 } 194 195 // matchDevice identifies the device matching the configured structure, returns 196 // device path and a shifted structure should any offset adjustments be needed 197 func (r *rawStructureUpdater) matchDevice() (device string, shifted *LaidOutStructure, err error) { 198 device, offs, err := r.deviceLookup(r.ps) 199 if err != nil { 200 return "", nil, fmt.Errorf("cannot find device matching structure %v: %v", r.ps, err) 201 } 202 203 if offs == r.ps.StartOffset { 204 return device, r.ps, nil 205 } 206 207 // Structure starts at different offset, make the necessary adjustment. 208 structForDevice := ShiftStructureTo(*r.ps, offs) 209 return device, &structForDevice, nil 210 } 211 212 // Backup attempts to analyze and prepare a backup copy of data that will be 213 // replaced during subsequent update. Backups are kept in the backup directory 214 // passed to newRawStructureUpdater(). Each region replaced by new content is 215 // copied out to a separate file. Only differing regions are backed up. Analysis 216 // and backup of each region is checkpointed. Regions that have been backed up 217 // or determined to be identical will not be analyzed on subsequent calls. 218 func (r *rawStructureUpdater) Backup() error { 219 device, structForDevice, err := r.matchDevice() 220 if err != nil { 221 return err 222 } 223 224 disk, err := os.OpenFile(device, os.O_RDONLY, 0) 225 if err != nil { 226 return fmt.Errorf("cannot open device for reading: %v", err) 227 } 228 defer disk.Close() 229 230 for _, pc := range structForDevice.LaidOutContent { 231 if err := r.backupOrCheckpointContent(disk, &pc); err != nil { 232 return fmt.Errorf("cannot backup image %v: %v", pc, err) 233 } 234 } 235 236 return nil 237 } 238 239 func (r *rawStructureUpdater) rollbackDifferent(out io.WriteSeeker, pc *LaidOutContent) error { 240 backupPath := rawContentBackupPath(r.backupDir, r.ps, pc) 241 242 if osutil.FileExists(backupPath + ".same") { 243 // content the same, no update needed 244 return nil 245 } 246 247 backup, err := os.Open(backupPath + ".backup") 248 if err != nil { 249 return fmt.Errorf("cannot open backup image: %v", err) 250 } 251 252 if err := writeRawStream(out, pc, backup); err != nil { 253 return fmt.Errorf("cannot restore backup: %v", err) 254 } 255 256 return nil 257 } 258 259 // Rollback attempts to restore original content from the backup copies prepared during Backup(). 260 func (r *rawStructureUpdater) Rollback() error { 261 device, structForDevice, err := r.matchDevice() 262 if err != nil { 263 return err 264 } 265 266 disk, err := os.OpenFile(device, os.O_WRONLY, 0) 267 if err != nil { 268 return fmt.Errorf("cannot open device for writing: %v", err) 269 } 270 defer disk.Close() 271 272 for _, pc := range structForDevice.LaidOutContent { 273 if err := r.rollbackDifferent(disk, &pc); err != nil { 274 return fmt.Errorf("cannot rollback image %v: %v", pc, err) 275 } 276 } 277 278 return nil 279 } 280 281 func (r *rawStructureUpdater) updateDifferent(disk io.WriteSeeker, pc *LaidOutContent) error { 282 backupPath := rawContentBackupPath(r.backupDir, r.ps, pc) 283 284 if osutil.FileExists(backupPath + ".same") { 285 // content the same, no update needed 286 return ErrNoUpdate 287 } 288 289 if !osutil.FileExists(backupPath + ".backup") { 290 // not the same, but a backup file is missing, error out just in 291 // case 292 return fmt.Errorf("missing backup file") 293 } 294 295 if err := r.writeRawImage(disk, pc); err != nil { 296 return err 297 } 298 299 return nil 300 } 301 302 // Update attempts to update the structure. The structure must have been 303 // analyzed and backed up by a prior Backup() call. 304 func (r *rawStructureUpdater) Update() error { 305 device, structForDevice, err := r.matchDevice() 306 if err != nil { 307 return err 308 } 309 310 disk, err := os.OpenFile(device, os.O_WRONLY, 0) 311 if err != nil { 312 return fmt.Errorf("cannot open device for writing: %v", err) 313 } 314 defer disk.Close() 315 316 skipped := 0 317 for _, pc := range structForDevice.LaidOutContent { 318 if err := r.updateDifferent(disk, &pc); err != nil { 319 if err == ErrNoUpdate { 320 skipped++ 321 continue 322 } 323 return fmt.Errorf("cannot update image %v: %v", pc, err) 324 } 325 } 326 327 if skipped == len(structForDevice.LaidOutContent) { 328 // all content is identical, nothing was updated 329 return ErrNoUpdate 330 } 331 332 return nil 333 }