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  }