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