Skip to content

Commit 15b996e

Browse files
authored
Merge pull request #6005 from oasisprotocol/kostko/feature/tdx-persistent-image
go/runtime/host/tdx: Add support for persistent image overlay
2 parents 2425552 + 95c0127 commit 15b996e

File tree

10 files changed

+154
-2
lines changed

10 files changed

+154
-2
lines changed

.changelog/6005.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
go/runtime/host/tdx: Add support for persistent image overlay

go/oasis-test-runner/oasis/runtime.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ type DeploymentCfg struct {
9898
// ComponentCfg is a runtime component configuration.
9999
type ComponentCfg struct {
100100
Kind component.Kind `json:"kind"`
101+
Name string `json:"name,omitempty"`
101102
Version version.Version `json:"version"`
102103
Binaries map[node.TEEHardware]string `json:"binaries"`
103104
}
@@ -281,6 +282,7 @@ func (rt *Runtime) toRuntimeBundle(deploymentIndex int) (*bundle.Bundle, error)
281282

282283
comp := &bundle.Component{
283284
Kind: compCfg.Kind,
285+
Name: compCfg.Name,
284286
Version: compCfg.Version,
285287
ELF: &bundle.ELFMetadata{
286288
Executable: elfBin,

go/oasis-test-runner/scenario/e2e/runtime/rofl.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func (sc *roflImpl) Fixture() (*oasis.NetworkFixture, error) {
4141
// Add ROFL component.
4242
f.Runtimes[1].Deployments[0].Components = append(f.Runtimes[1].Deployments[0].Components, oasis.ComponentCfg{
4343
Kind: component.ROFL,
44+
Name: "test-rofl",
4445
Binaries: sc.ResolveRuntimeBinaries(ROFLComponentBinary),
4546
})
4647

go/runtime/bundle/bundle_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ func TestDetachedBundle(t *testing.T) {
140140
// No RONL component in the manifest.
141141
{
142142
Kind: component.ROFL,
143+
Name: "my-rofl-comp",
143144
ELF: &ELFMetadata{
144145
Executable: "runtime.bin",
145146
},

go/runtime/bundle/component.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,24 @@ package bundle
33
import (
44
"fmt"
55
"path/filepath"
6+
"regexp"
67

78
"github.com/oasisprotocol/oasis-core/go/common"
89
"github.com/oasisprotocol/oasis-core/go/common/sgx"
910
"github.com/oasisprotocol/oasis-core/go/common/version"
1011
"github.com/oasisprotocol/oasis-core/go/runtime/bundle/component"
1112
)
1213

14+
// componentNameRegexp is the regular expression for valid component names.
15+
var componentNameRegexp = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
16+
17+
const (
18+
// minComponentNameLen is the minimum length of a valid component name.
19+
minComponentNameLen = 3
20+
// maxComponentNameLen is the maximum length of a valid component name.
21+
maxComponentNameLen = 128
22+
)
23+
1324
// ExplodedComponent is an exploded runtime component ready for execution.
1425
type ExplodedComponent struct {
1526
*Component
@@ -118,6 +129,15 @@ func (c *Component) Validate() error {
118129
return fmt.Errorf("RONL component cannot be disabled")
119130
}
120131
case component.ROFL:
132+
if len(c.Name) < minComponentNameLen {
133+
return fmt.Errorf("ROFL component name must be at least %d characters long", minComponentNameLen)
134+
}
135+
if len(c.Name) > maxComponentNameLen {
136+
return fmt.Errorf("ROFL component name must be at most %d characters long", maxComponentNameLen)
137+
}
138+
if !componentNameRegexp.MatchString(c.Name) {
139+
return fmt.Errorf("ROFL component name is invalid (must satisfy: %s)", componentNameRegexp)
140+
}
121141
default:
122142
return fmt.Errorf("unknown component kind: '%s'", c.Kind)
123143
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package bundle
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/oasisprotocol/oasis-core/go/runtime/bundle/component"
10+
)
11+
12+
func TestComponentValidation(t *testing.T) {
13+
require := require.New(t)
14+
15+
// Test component names.
16+
var comp Component
17+
err := comp.Validate()
18+
require.ErrorContains(err, "unknown component kind")
19+
comp.Kind = component.ROFL
20+
21+
for _, tc := range []struct {
22+
name string
23+
err string
24+
}{
25+
{"", "ROFL component name must be at least 3 characters long"},
26+
{strings.Repeat("a", 129), "ROFL component name must be at most 128 characters long"},
27+
{"my invalid component name", "ROFL component name is invalid"},
28+
{"my.invalid.component.name", "ROFL component name is invalid"},
29+
{"my:invalid:component:name", "ROFL component name is invalid"},
30+
{"my-valid-component-name", ""},
31+
} {
32+
comp.Name = tc.name
33+
err = comp.Validate()
34+
if tc.err == "" {
35+
require.NoError(err)
36+
} else {
37+
require.ErrorContains(err, tc.err)
38+
}
39+
}
40+
}

go/runtime/bundle/registry_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func createSyntheticBundle(runtimeID common.Namespace, version version.Version,
3131
case component.ROFL:
3232
manifest.Components = append(manifest.Components, &Component{
3333
Kind: component.ROFL,
34+
Name: "my-rofl-comp",
3435
Version: version,
3536
})
3637
default:

go/runtime/host/tdx/qemu.go

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,27 @@ package tdx
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"net"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
711
"strings"
812
"sync"
913
"time"
1014

1115
"github.com/mdlayher/vsock"
1216

17+
"github.com/oasisprotocol/oasis-core/go/common"
1318
"github.com/oasisprotocol/oasis-core/go/common/identity"
1419
"github.com/oasisprotocol/oasis-core/go/common/logging"
1520
"github.com/oasisprotocol/oasis-core/go/common/node"
1621
"github.com/oasisprotocol/oasis-core/go/common/persistent"
1722
"github.com/oasisprotocol/oasis-core/go/common/sgx/pcs"
1823
sgxQuote "github.com/oasisprotocol/oasis-core/go/common/sgx/quote"
1924
consensus "github.com/oasisprotocol/oasis-core/go/consensus/api"
25+
"github.com/oasisprotocol/oasis-core/go/runtime/bundle"
2026
"github.com/oasisprotocol/oasis-core/go/runtime/bundle/component"
2127
"github.com/oasisprotocol/oasis-core/go/runtime/host"
2228
"github.com/oasisprotocol/oasis-core/go/runtime/host/protocol"
@@ -28,10 +34,15 @@ import (
2834
const (
2935
// defaultQemuSystemPath is the default QEMU system binary path.
3036
defaultQemuSystemPath = "/usr/bin/qemu-system-x86_64"
37+
// defaultQemuImgPath is the default qemu-bin binary path.
38+
defaultQemuImgPath = "/usr/bin/qemu-img"
3139
// defaultStartCid is the default start CID.
3240
defaultStartCid = 0xA5150000
3341
// defaultRuntimeAttestInterval is the default runtime (re-)attestation interval.
3442
defaultRuntimeAttestInterval = 2 * time.Hour
43+
// persistentImageDir is the name of the directory within the runtime data directory
44+
// where persistent overlay images can be stored.
45+
persistentImageDir = "images"
3546

3647
// vsockPortRHP is the VSOCK port used for the Runtime-Host Protocol.
3748
vsockPortRHP = 1
@@ -41,6 +52,8 @@ const (
4152

4253
// QemuConfig is the configuration of the QEMU-based TDX runtime provisioner.
4354
type QemuConfig struct {
55+
// DataDir is the runtime data directory.
56+
DataDir string
4457
// HostInfo provides information about the host environment.
4558
HostInfo *protocol.HostInfo
4659

@@ -166,9 +179,20 @@ func (q *qemuProvisioner) getSandboxConfig(rtCfg host.Config, _ sandbox.Connecto
166179
return process.Config{}, fmt.Errorf("format '%s' is not supported", stage2Format)
167180
}
168181

182+
// Set up a persistent overlay image when configured to do so.
183+
snapshotMode := "on" // Default to ephemeral images.
184+
if tdxCfg.Stage2Persist {
185+
stage2Image, err = q.createPersistentOverlayImage(rtCfg, comp, stage2Image, stage2Format)
186+
if err != nil {
187+
return process.Config{}, err
188+
}
189+
stage2Format = "qcow2"
190+
snapshotMode = "off"
191+
}
192+
169193
cfg.Args = append(cfg.Args,
170194
// Stage 2 drive.
171-
"-drive", fmt.Sprintf("format=%s,file=%s,if=none,id=drive0,snapshot=on", stage2Format, stage2Image),
195+
"-drive", fmt.Sprintf("format=%s,file=%s,if=none,id=drive0,snapshot=%s", stage2Format, stage2Image, snapshotMode),
172196
"-device", "virtio-blk-pci,drive=drive0",
173197
)
174198
}
@@ -211,6 +235,65 @@ func (q *qemuProvisioner) getSandboxConfig(rtCfg host.Config, _ sandbox.Connecto
211235
return cfg, nil
212236
}
213237

238+
// createPersistentOverlayImage creates a persistent overlay image for the given backing image and
239+
// returns the full path to the overlay image. In case the image already exists, it is reused.
240+
//
241+
// The format of the resulting image is always qcow2.
242+
func (q *qemuProvisioner) createPersistentOverlayImage(
243+
rtCfg host.Config,
244+
comp *bundle.ExplodedComponent,
245+
image string,
246+
format string,
247+
) (string, error) {
248+
compID, _ := comp.ID().MarshalText()
249+
imageDir := filepath.Join(q.cfg.DataDir, persistentImageDir, rtCfg.ID.String(), string(compID))
250+
imageFn := filepath.Join(imageDir, fmt.Sprintf("%s.overlay", filepath.Base(image)))
251+
switch _, err := os.Stat(imageFn); {
252+
case err == nil:
253+
// Image already exists, perform a rebase operation to account for the backing file location
254+
// changing (e.g. due to an upgrade).
255+
cmd := exec.Command(
256+
defaultQemuImgPath,
257+
"rebase",
258+
"-u",
259+
"-f", "qcow2",
260+
"-b", image,
261+
"-F", format,
262+
imageFn,
263+
)
264+
var out strings.Builder
265+
cmd.Stderr = &out
266+
cmd.Stdout = &out
267+
if err := cmd.Run(); err != nil {
268+
return "", fmt.Errorf("failed to rebase persistent overlay image: %s\n%w", out.String(), err)
269+
}
270+
case errors.Is(err, os.ErrNotExist):
271+
// Create image directory if it doesn't yet exist.
272+
if err := common.Mkdir(imageDir); err != nil {
273+
return "", fmt.Errorf("failed to create persistent overlay image directory: %w", err)
274+
}
275+
276+
// Create the persistent overlay image.
277+
cmd := exec.Command(
278+
defaultQemuImgPath,
279+
"create",
280+
"-f", "qcow2",
281+
"-b", image,
282+
"-F", format,
283+
imageFn,
284+
)
285+
var out strings.Builder
286+
cmd.Stderr = &out
287+
cmd.Stdout = &out
288+
if err := cmd.Run(); err != nil {
289+
return "", fmt.Errorf("failed to create persistent overlay image: %s\n%w", out.String(), err)
290+
}
291+
default:
292+
return "", fmt.Errorf("failed to stat persistent overlay image: %w", err)
293+
}
294+
return imageFn, nil
295+
}
296+
214297
func (q *qemuProvisioner) updateCapabilityTEE(ctx context.Context, hp *sandbox.HostInitializerParams) (cap *node.CapabilityTEE, aerr error) {
215298
defer func() {
216299
sgxCommon.UpdateAttestationMetrics(hp.Runtime.ID(), component.TEEKindTDX, aerr)

go/runtime/registry/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"maps"
77
"os"
8+
"path/filepath"
89
"slices"
910
"strings"
1011
"time"
@@ -114,6 +115,7 @@ func createHostInfo(consensus consensus.Backend) (*hostProtocol.HostInfo, error)
114115
}
115116

116117
func createProvisioner(
118+
dataDir string,
117119
commonStore *persistent.CommonStore,
118120
identity *identity.Identity,
119121
consensus consensus.Backend,
@@ -205,6 +207,7 @@ func createProvisioner(
205207
// Configure TDX provisioner.
206208
// TODO: Allow provisioner selection in the future, currently we only have QEMU.
207209
provisioners[component.TEEKindTDX], err = hostTdx.NewQemu(hostTdx.QemuConfig{
210+
DataDir: filepath.Join(dataDir, RuntimesDir),
208211
HostInfo: hostInfo,
209212
CommonStore: commonStore,
210213
PCS: qs,

go/runtime/registry/registry.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,7 @@ func New(
725725
}
726726

727727
// Create runtime provisioner.
728-
provisioner, err := createProvisioner(commonStore, identity, consensus, hostInfo, ias, qs)
728+
provisioner, err := createProvisioner(dataDir, commonStore, identity, consensus, hostInfo, ias, qs)
729729
if err != nil {
730730
return nil, err
731731
}

0 commit comments

Comments
 (0)