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  }