Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

snap-bootstrap: load partition information from a manifest file for cloudimg-rootfs mode #14713

Open
wants to merge 22 commits into
base: master
Choose a base branch
from

Conversation

sespiros
Copy link
Contributor

@sespiros sespiros commented Nov 12, 2024

Rebased on top of #14178 which is approved but not merged. New commits start at commit 2035d4d.

Rebased on top of #14789 which is approved but not merged. New commits start at commit c55f509.

This refactors cloudimg-rootfs mode and adds support for externally providing a dm-verity root hash for the rootfs that is mounted. This root hash is part of a new manifest file that resides on the ESP.

Subsequent PRs will add support for measuring the manifest to the TPM for remote attestation purposes.

Jira: https://warthogs.atlassian.net/browse/FR-9480

@alfonsosanchezbeato
Copy link
Member

@sespiros probably it would be good to rebase this now that the prerequisite has been merged

@alfonsosanchezbeato alfonsosanchezbeato added the Run nested The PR also runs tests inluded in nested suite label Nov 20, 2024
@sespiros sespiros force-pushed the snap-bootstrap-manifest branch from 5c312d1 to 45d1ecd Compare November 21, 2024 17:28
Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sespiros I think we discussed this at the sprint, my strong preference would be to first move the existing CVM support to a _cvm.go and _cmv_test.go file, and then do this change

Copy link

codecov bot commented Dec 3, 2024

Codecov Report

Attention: Patch coverage is 78.52349% with 32 lines in your changes missing coverage. Please review.

Project coverage is 78.22%. Comparing base (24a0034) to head (a43c494).
Report is 89 commits behind head on master.

Files with missing lines Patch % Lines
cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go 78.52% 21 Missing and 11 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #14713      +/-   ##
==========================================
+ Coverage   78.20%   78.22%   +0.01%     
==========================================
  Files        1151     1156       +5     
  Lines      151396   152997    +1601     
==========================================
+ Hits       118402   119684    +1282     
- Misses      25662    25919     +257     
- Partials     7332     7394      +62     
Flag Coverage Δ
unittests 78.22% <78.52%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@sespiros sespiros force-pushed the snap-bootstrap-manifest branch from b4d37ea to 6b717d2 Compare December 4, 2024 11:53
This is a preparatory commit which modifies the existing CVM mode
to support mounting multiple overlaid partitions for the rootfs.
In order to provide integrity protection for the root fs, the root fs will
be mounted read-only and will be accompanied by dm-verity data from a new
auto-discovered partition. To support writable state for the ephemeral CVM
scenario, an overlayfs mount is created with an upper layer residing on tmpfs.
A writable layer residing on disk can be also supported but it is currently
disabled.

The filesystem layout in the initramfs will look like:

. /run/mnt
├── ...
├── data                     (where the final merged overlay will be mounted)
├── cloudimg-rootfs          (read-only mount of the root fs, folder name is the fs label)
├── ..                       (future WIP, other partition mounts as separate mounts)
└── cloudimg-rootfs-writable (tmpfs mount for the writable upper layer)
    ├── work
    └── upper

An example final overlay mount command will look like:

mount -t overlay overlay -olowerdir=/run/mnt/cloudimg-rootfs,\
upperdir=/run/mnt/cloudimg-rootfs-writable/upper,\
workdir=/run/mnt/cloudimg-rootfs-writable/work \
/run/mnt/data
v1 CVM images use the filesystem label to auto-discover an encrypted
rootfs partition based on whether it has the '-enc' suffix. The new
images use the GPT label to auto-discover the verity partition. In
order to keep backwards compatibility, the customization tool will use
set both labels to the same one.

This commit clarifies that the field in the new structs actually contain
GPT label information.
creating the upper and work folders used for setting up the overlayfs
mount, was initially handled by snap-bootstrap whether it set up a tmpfs
or detected a writable partition on disk but in order to support the
writable partition case, it was later discovered that these folders might
need to be created during the customization step and might already exist.

snap-bootstrap will now create those folders only if it has detected
no writable partition in the manifest and has auto-created a tmpfs-based
writable partition.
trying to boot an image on a system without a TPM present was failing
due to not-finding a TPM device. The source of the error was not the
provision step as secbotProvisionForCVM already ignores errors (returns
nil) if a TPM device is not found or a template file for provision the
TPM is not found.
CVM mode used to mount the rootfs partition with the Ephemeral flag as
cloud images contain a fixed fstab entry.
This is a legacy check that is not actually needed now as
secbootUnlockVolumeUsingSealedKeyIfEncrypted will only find partitions
residing on the disk that the ESP is from.
@sespiros sespiros force-pushed the snap-bootstrap-manifest branch from 6b717d2 to c4ab893 Compare December 5, 2024 10:48
Copy link
Member

@alfonsosanchezbeato alfonsosanchezbeato left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I have some comments

Opts *systemdMountOptions
}

type ImageManifestPartition struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this and other new types like ImageManifest or ManifestError do not need to be exported.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made them private.

@@ -94,6 +94,14 @@ func MockOsutilIsMounted(f func(string) (bool, error)) (restore func()) {
}
}

func MockOsReadFile(f func(string) ([]byte, error)) (restore func()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In tests we tend to create the files instead of mocking the read, note that dirs.GlobalRootDir is set to a temporary folder so this is in principle easy to do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented the suggestion, thanks.

Comment on lines 59 to 62
GptLabel string `json:"label"`
RootHash string `json:"root_hash"`
Overlay string `json:"overlay"`
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A description on what is each of these fields would be helpful, or maybe a link to a description of the format if available.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added descriptions for all the proposed fields.

}

type ImageManifest struct {
Partitions []ImageManifestPartition `json:"partitions"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same: re describing the field

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a description although I feel it is a bit self-describing.

return im, nil
}

// generateMountsFromManifest is used to parse a manifest file which contains information about which
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strictly speaking, the manifest passed around parsed already, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, I modified my comment here.

if p.Overlay == "lowerdir" {
// XXX: currently only one lower layer is permitted. The rest, if found, are ignored.
if foundReadOnlyPartition != "" {
continue
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not this as a minimum be logged? Or should it be considered an error in the manifest?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should at least log something here, yes. Initially I thought to support potential future cases where I might have an older snap-bootstrap (which only supports a single lowerdir for instance) be supplied with a manifest that might be richer in functionality (multiple lowerdirs) and still allow it to boot.

This might be too complicated of a use case so I decided to throw an error instead.

} else {
// Only one writable partition is permitted.
if foundWritablePartition != "" {
continue
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same regarding log/error out

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See previous comment. This will now be stricter and throw an error.

GptLabel: "cloudimg-rootfs",
Opts: &systemdMountOptions{
Overlayfs: true,
LowerDirs: []string{filepath.Join(boot.InitramfsRunMntDir, partitionMounts[0].GptLabel)},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is using partitionMounts[0] assuming an order in the definitions of the partitions in the file? should this always be the read-only partition?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed this to use the foundReadOnlyPartition variable. I also reworked some bits a little bit:

  • I no longer require any order (dropped some sanity checks from parseImageManifest)
  • I renamed the Overlay field which was a bit confusing to ReadOnly. I described the change in the generateMountsFromManifest comment.

Comment on lines 187 to 188
pm.Opts.UpperDir = filepath.Join(boot.InitramfsRunMntDir, foundWritablePartition, "upper")
pm.Opts.WorkDir = filepath.Join(boot.InitramfsRunMntDir, foundWritablePartition, "work")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be done when pm is defined, splitting the initialization of Opts does not seem necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

restore = main.MockOsReadFile(func(path string) ([]byte, error) {
// check attempt to read manifest from specific path
c.Assert(path, Equals, filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/ubuntu", "manifest.json"))
return []byte(fmt.Sprintf(`{"partitions":[{"label":"cloudimg-rootfs","root_hash":%q,"overlay":"lowerdir"}]}`, expectedRootHash)), nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a test case for when more than one partition is defined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I modified the existing one to accommodate the changes to the manifest and also added extra test cases for the private generateMountsFromManifest to test multiple unsupported partitions scenarios.

Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did a pass

if err != nil {
return err
}
if err := os.Mkdir(path, fi.Mode()); err != nil && !os.IsExist(err) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't we always get an IsExist error, otherwise how can the previous os.Stat work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reworked that bit.

return partitionMounts, nil
}

var createOverlayDirs = func(path string) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not tested

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added several test cases.

// then we need to specify that the data mountpoint is
// expected to be a decrypted device
diskOpts.IsDecryptedDevice = true
if imageManifestFile != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be an "else" of the previous "if"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is the same logically (could os.ReadFile return both an error and non nil imageManifestFile?). I changed it and put it in the else block.

@sespiros sespiros requested a review from pedronis December 17, 2024 18:16
Copy link
Member

@alfonsosanchezbeato alfonsosanchezbeato left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes, I have some minor comments

} else {
// Only one writable partition is permitted.
if foundWritablePartition != "" {
return nil, errors.New("manifest contains multiple non read-only partitions")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: maybe "manifest contains multiple writable partitions"

@@ -241,3 +247,11 @@ func MockBuildInstallObserver(f func(model *asserts.Model, gadgetDir string, use
installBuildInstallObserver = old
}
}

func MockCreateOverlayDirs(f func(path string) error) (restore func()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My comment about mocking also applies here, you do not need to mock, you could use osutil.IsDirectory() to check if the folder has been created.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I replaced that with a call to osutil.IsDirectory().

// then we need to specify that the data mountpoint is
// expected to be a decrypted device
diskOpts.IsDecryptedDevice = true
// XXX: if a manifest file is not found fall-back to CVM v1 behaviour

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: why the XXX in the comment? There is nothing to fix now or in the future here, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tagged with XXX the comments where either I made assumptions about which behavior would be actually most desirable (this case here) or where I foresee things will be changed. For this particular one, I believe this behavior is most likely what we will stick with so I removed the XXX.

pm.Where = filepath.Join(boot.InitramfsRunMntDir, p.GptLabel)

if p.ReadOnly {
// XXX: currently only a single read-only partition/overlay fs lowerdir is permitted.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something expected to change in the future? Maybe the comment should clarify this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have discussed about having support for multiple lowerdirs in the future to support varying degrees of customization. I clarified this in my comment.

Comment on lines 288 to 290
// Create overlayfs' upperdir and workdir in the writable tmpfs layer.
if pm.Opts.Tmpfs {
if err := createOverlayDirs(pm.Where); err != nil {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these directories then always expected to exist in a non-temporary writable partition? Maybe the comment could clarify this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I clarified the comment a bit for now. Currently we mostly care about the ephemeral case that doesn't have a persistent writable layer. If a persistent writable layer exists, I expect these directories to be created by the image creation tool and not by snap-bootstrap.

This was developed in conjunction with canonical/encrypt-cloud-image#26 (the image creation tool) which hasn't been reviewed yet. I might change this to make it actually a responsibility of snap-bootstrap once this gets some reviews.

// comes from the same disk as ESP
return fmt.Errorf("cannot validate boot: cloudimg-rootfs mountpoint is expected to be from disk %s but is not", disk.Dev())

// Mount partitions

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes that the overlayfs mount is last, right? Maybe it could be good to add that to the comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, generateMountsFromManifest should create the partitionMounts array in a specific order (ro, rw, overlayfs mount). I clarified the comment.

Copy link
Member

@alfonsosanchezbeato alfonsosanchezbeato left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@alfonsosanchezbeato
Copy link
Member

@sespiros there are some static checks failing atm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Run nested The PR also runs tests inluded in nested suite
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants