Skip to content

Commit

Permalink
Base layer manipulation (microsoft#1637)
Browse files Browse the repository at this point in the history
* Simple baseLayerReader to export parentless layers

This is the inverse of the baseLayerWriter: It walks Files/ and
UtilityVM/Files/ (if present) and ignores the rest of the layer data,
as it will be recreated when the layer is imported.

Signed-off-by: Paul "TBBle" Hampson <[email protected]>

* Introduce hcsshim.ConvertToBaseLayer

This API allows turning any collection of files into a WCOW base layer.

It will create the necessary files in Files/ for
hcsshim.ProcessBaseLayer to function, validate the necessary files for
hcsshim.ProcessUtilityVMImage if UtilityVM/ exists, and then call those
two APIs to complete the process.

Calling this on a directory containing an untarred base layer OCI
tarball, gives a very similar outcome to passing the tar stream through
ociwclayer.ImportLayer.

The new API is used in `TestSCSIAddRemoveWCOW` to create nearly-empty
base layers for the scratch layers attached and removed from the utility
VM.

A wclayer command is also introduced: `makebaselayer` for testing and
validation purposes.

Signed-off-by: Paul "TBBle" Hampson <[email protected]>

* Include hard-linked files as hard-links in the tarstream

Signed-off-by: Paul "TBBle" Hampson <[email protected]>

* Use offline registry library to generate min hive

This change adds functions to generate valid, empty hives.

Signed-off-by: Gabriel Adrian Samfira <[email protected]>

* Rename ofreg.go and close key

Signed-off-by: Gabriel Adrian Samfira <[email protected]>

* Fix temp dir creation

Signed-off-by: Gabriel Adrian Samfira <[email protected]>

* Cleanup tests

Signed-off-by: Gabriel Adrian Samfira <[email protected]>

* Fix ORCloseHive definition

Signed-off-by: Gabriel Adrian Samfira <[email protected]>

* Remove unused ctx from baseLayerReader

Signed-off-by: Gabriel Adrian Samfira <[email protected]>

* Use string in sys definition and check for err

  * We can use string instead of *uint16 in the //sys definition and allow
mksyscall to generate the proper boilerplate.
  * do not shadow err if it's not nil

Signed-off-by: Gabriel Adrian Samfira <[email protected]>

* Close the r.proceed channel

Signed-off-by: Gabriel Adrian Samfira <[email protected]>

* Return if backup reader is nil

Signed-off-by: Gabriel Adrian Samfira <[email protected]>

---------

Signed-off-by: Paul "TBBle" Hampson <[email protected]>
Signed-off-by: Gabriel Adrian Samfira <[email protected]>
Co-authored-by: Paul "TBBle" Hampson <[email protected]>
  • Loading branch information
gabriel-samfira and TBBle authored Feb 28, 2023
1 parent a2a2dd3 commit 83d2ad0
Show file tree
Hide file tree
Showing 7 changed files with 435 additions and 0 deletions.
216 changes: 216 additions & 0 deletions wclayer/baselayerreader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package wclayer

import (
"errors"
"io"
"os"
"path/filepath"
"strings"
"syscall"

"github.com/Microsoft/go-winio"
"github.com/Microsoft/hcsshim/internal/longpath"
"github.com/Microsoft/hcsshim/internal/oc"
"go.opencensus.io/trace"
)

type baseLayerReader struct {
s *trace.Span
root string
result chan *fileEntry
proceed chan bool
currentFile *os.File
backupReader *winio.BackupFileReader
}

func newBaseLayerReader(root string, s *trace.Span) (r *baseLayerReader) {
r = &baseLayerReader{
s: s,
root: root,
result: make(chan *fileEntry),
proceed: make(chan bool),
}
go r.walk()
return r
}

func (r *baseLayerReader) walkUntilCancelled() error {
root, err := longpath.LongAbs(r.root)
if err != nil {
return err
}

r.root = root

err = filepath.Walk(filepath.Join(r.root, filesPath), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

// Indirect fix for https://github.com/moby/moby/issues/32838#issuecomment-343610048.
// Handle failure from what may be a golang bug in the conversion of
// UTF16 to UTF8 in files which are left in the recycle bin. Os.Lstat
// which is called by filepath.Walk will fail when a filename contains
// unicode characters. Skip the recycle bin regardless which is goodness.
if strings.EqualFold(path, filepath.Join(r.root, `Files\$Recycle.Bin`)) && info.IsDir() {
return filepath.SkipDir
}

r.result <- &fileEntry{path, info, nil}
if !<-r.proceed {
return errorIterationCanceled
}

return nil
})

if err == errorIterationCanceled {
return nil
}

if err != nil {
return err
}

utilityVMAbsPath := filepath.Join(r.root, utilityVMPath)
utilityVMFilesAbsPath := filepath.Join(r.root, utilityVMFilesPath)

// Ignore a UtilityVM without Files, that's not _really_ a UtiltyVM
if _, err = os.Lstat(utilityVMFilesAbsPath); err != nil {
if os.IsNotExist(err) {
return io.EOF
}
return err
}

err = filepath.Walk(utilityVMAbsPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

if path != utilityVMAbsPath && path != utilityVMFilesAbsPath && !hasPathPrefix(path, utilityVMFilesAbsPath) {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}

r.result <- &fileEntry{path, info, nil}
if !<-r.proceed {
return errorIterationCanceled
}

return nil
})

if err == errorIterationCanceled {
return nil
}

if err != nil {
return err
}

return io.EOF
}

func (r *baseLayerReader) walk() {
defer close(r.result)
if !<-r.proceed {
return
}

err := r.walkUntilCancelled()
if err != nil {
for {
r.result <- &fileEntry{err: err}
if !<-r.proceed {
return
}
}
}
}

func (r *baseLayerReader) reset() {
if r.backupReader != nil {
r.backupReader.Close()
r.backupReader = nil
}
if r.currentFile != nil {
r.currentFile.Close()
r.currentFile = nil
}
}

func (r *baseLayerReader) Next() (path string, size int64, fileInfo *winio.FileBasicInfo, err error) {
r.reset()
r.proceed <- true
fe := <-r.result
if fe == nil {
err = errors.New("BaseLayerReader closed")
return
}
if fe.err != nil {
err = fe.err
return
}

path, err = filepath.Rel(r.root, fe.path)
if err != nil {
return
}

f, err := openFileOrDir(fe.path, syscall.GENERIC_READ, syscall.OPEN_EXISTING)
if err != nil {
return
}
defer func() {
if f != nil {
f.Close()
}
}()

fileInfo, err = winio.GetFileBasicInfo(f)
if err != nil {
return
}

size = fe.fi.Size()
r.backupReader = winio.NewBackupFileReader(f, true)

r.currentFile = f
f = nil
return
}

func (r *baseLayerReader) LinkInfo() (uint32, *winio.FileIDInfo, error) {
fileStandardInfo, err := winio.GetFileStandardInfo(r.currentFile)
if err != nil {
return 0, nil, err
}
fileIDInfo, err := winio.GetFileID(r.currentFile)
if err != nil {
return 0, nil, err
}
return fileStandardInfo.NumberOfLinks, fileIDInfo, nil
}

func (r *baseLayerReader) Read(b []byte) (int, error) {
if r.backupReader == nil {
return 0, io.EOF
}
return r.backupReader.Read(b)
}

func (r *baseLayerReader) Close() (err error) {
defer r.s.End()
defer func() {
oc.SetSpanStatus(r.s, err)
close(r.proceed)
}()
r.proceed <- false
// The r.result channel will be closed once walk() returns
<-r.result
r.reset()
return nil
}
File renamed without changes.
158 changes: 158 additions & 0 deletions wclayer/converttobaselayer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package wclayer

import (
"context"
"fmt"
"os"
"path/filepath"
"syscall"

"github.com/Microsoft/hcsshim/internal/hcserror"
"github.com/Microsoft/hcsshim/internal/longpath"
"github.com/Microsoft/hcsshim/internal/oc"
"github.com/Microsoft/hcsshim/internal/safefile"
"github.com/Microsoft/hcsshim/internal/winapi"
"github.com/pkg/errors"
"go.opencensus.io/trace"
"golang.org/x/sys/windows"
)

var hiveNames = []string{"DEFAULT", "SAM", "SECURITY", "SOFTWARE", "SYSTEM"}

// Ensure the given file exists as an ordinary file, and create a minimal hive file if not.
func ensureHive(path string, root *os.File) (err error) {
_, err = safefile.LstatRelative(path, root)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("accessing %s: %w", path, err)
}

version := windows.RtlGetVersion()
if version == nil {
return fmt.Errorf("failed to get OS version")
}

var fullPath string
fullPath, err = longpath.LongAbs(filepath.Join(root.Name(), path))
if err != nil {
return fmt.Errorf("getting path: %w", err)
}

var key syscall.Handle
err = winapi.ORCreateHive(&key)
if err != nil {
return fmt.Errorf("creating hive: %w", err)
}

defer func() {
closeErr := winapi.ORCloseHive(key)
if closeErr != nil && err == nil {
err = fmt.Errorf("closing hive key: %w", closeErr)
}
}()

err = winapi.ORSaveHive(key, fullPath, version.MajorVersion, version.MinorVersion)
if err != nil {
return fmt.Errorf("saving hive: %w", err)
}

return nil
}

func ensureBaseLayer(root *os.File) (hasUtilityVM bool, err error) {
// The base layer registry hives will be copied from here
const hiveSourcePath = "Files\\Windows\\System32\\config"
if err = safefile.MkdirAllRelative(hiveSourcePath, root); err != nil {
return
}

for _, hiveName := range hiveNames {
hivePath := filepath.Join(hiveSourcePath, hiveName)
if err = ensureHive(hivePath, root); err != nil {
return
}
}

stat, err := safefile.LstatRelative(utilityVMFilesPath, root)

if os.IsNotExist(err) {
return false, nil
}

if err != nil {
return
}

if !stat.Mode().IsDir() {
fullPath := filepath.Join(root.Name(), utilityVMFilesPath)
return false, errors.Errorf("%s has unexpected file mode %s", fullPath, stat.Mode().String())
}

const bcdRelativePath = "EFI\\Microsoft\\Boot\\BCD"

// Just check that this exists as a regular file. If it exists but is not a valid registry hive,
// ProcessUtilityVMImage will complain:
// "The registry could not read in, or write out, or flush, one of the files that contain the system's image of the registry."
bcdPath := filepath.Join(utilityVMFilesPath, bcdRelativePath)

stat, err = safefile.LstatRelative(bcdPath, root)
if err != nil {
return false, errors.Wrapf(err, "UtilityVM must contain '%s'", bcdRelativePath)
}

if !stat.Mode().IsRegular() {
fullPath := filepath.Join(root.Name(), bcdPath)
return false, errors.Errorf("%s has unexpected file mode %s", fullPath, stat.Mode().String())
}

return true, nil
}

func convertToBaseLayer(ctx context.Context, root *os.File) error {
hasUtilityVM, err := ensureBaseLayer(root)

if err != nil {
return err
}

if err := ProcessBaseLayer(ctx, root.Name()); err != nil {
return err
}

if !hasUtilityVM {
return nil
}

err = safefile.EnsureNotReparsePointRelative(utilityVMPath, root)
if err != nil {
return err
}

utilityVMPath := filepath.Join(root.Name(), utilityVMPath)
return ProcessUtilityVMImage(ctx, utilityVMPath)
}

// ConvertToBaseLayer processes a candidate base layer, i.e. a directory
// containing the desired file content under Files/, and optionally the
// desired file content for a UtilityVM under UtilityVM/Files/
func ConvertToBaseLayer(ctx context.Context, path string) (err error) {
title := "hcsshim::ConvertToBaseLayer"
ctx, span := trace.StartSpan(ctx, title)
defer span.End()
defer func() { oc.SetSpanStatus(span, err) }()
span.AddAttributes(trace.StringAttribute("path", path))

root, err := safefile.OpenRoot(path)
if err != nil {
return hcserror.New(err, title+" - failed", "")
}
defer func() {
if err2 := root.Close(); err == nil && err2 != nil {
err = hcserror.New(err2, title+" - failed", "")
}
}()

if err = convertToBaseLayer(ctx, root); err != nil {
return hcserror.New(err, title+" - failed", "")
}
return nil
}
7 changes: 7 additions & 0 deletions wclayer/exportlayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ func ExportLayer(ctx context.Context, path string, exportFolderPath string, pare
type LayerReader interface {
// Next advances to the next file and returns the name, size, and file info
Next() (string, int64, *winio.FileBasicInfo, error)
// LinkInfo returns the number of links and the file identifier for the current file.
LinkInfo() (uint32, *winio.FileIDInfo, error)
// Read reads data from the current file, in the format of a Win32 backup stream, and
// returns the number of bytes read.
Read(b []byte) (int, error)
Expand All @@ -67,6 +69,11 @@ func NewLayerReader(ctx context.Context, path string, parentLayerPaths []string)
trace.StringAttribute("path", path),
trace.StringAttribute("parentLayerPaths", strings.Join(parentLayerPaths, ", ")))

if len(parentLayerPaths) == 0 {
// This is a base layer. It gets exported differently.
return newBaseLayerReader(path, span), nil
}

exportPath, err := os.MkdirTemp("", "hcs")
if err != nil {
return nil, err
Expand Down
Loading

0 comments on commit 83d2ad0

Please sign in to comment.