From 2e3b8a1e9168f30e7df6a620e993995ff446b25c Mon Sep 17 00:00:00 2001 From: Charalampos Mainas Date: Wed, 29 Jul 2026 15:40:56 +0200 Subject: [PATCH 1/2] refactor: move all monitor resources in InitialSetup Gather everything related to the guest rootfs (along with the preStart) command in once during InitialSetup and store them in monitorResources. Exec simply loads the struct and uses them. No rootfs decision is made in Exec. Signed-off-by: Charalampos Mainas --- pkg/unikontainers/block.go | 2 +- pkg/unikontainers/initrd_rootfs.go | 2 +- pkg/unikontainers/rootfs.go | 8 +- pkg/unikontainers/shared_fs.go | 17 +- pkg/unikontainers/types/types.go | 11 ++ pkg/unikontainers/unikontainers.go | 262 +++++++++++++---------------- pkg/unikontainers/utils.go | 26 ++- 7 files changed, 153 insertions(+), 175 deletions(-) diff --git a/pkg/unikontainers/block.go b/pkg/unikontainers/block.go index 69db4629a..aaac2446b 100644 --- a/pkg/unikontainers/block.go +++ b/pkg/unikontainers/block.go @@ -426,7 +426,7 @@ func (b blockRootfs) getSharedDirs() (types.SharedfsParams, error) { return types.SharedfsParams{}, nil } -func (b blockRootfs) preStart() error { +func (b blockRootfs) preStartCmd() []string { return nil } diff --git a/pkg/unikontainers/initrd_rootfs.go b/pkg/unikontainers/initrd_rootfs.go index ee70d9da2..136b77365 100644 --- a/pkg/unikontainers/initrd_rootfs.go +++ b/pkg/unikontainers/initrd_rootfs.go @@ -57,6 +57,6 @@ func (i initrdRootfs) getSharedDirs() (types.SharedfsParams, error) { return types.SharedfsParams{}, nil } -func (i initrdRootfs) preStart() error { +func (i initrdRootfs) preStartCmd() []string { return nil } diff --git a/pkg/unikontainers/rootfs.go b/pkg/unikontainers/rootfs.go index bd1c800f0..40dd5b963 100644 --- a/pkg/unikontainers/rootfs.go +++ b/pkg/unikontainers/rootfs.go @@ -30,17 +30,13 @@ import ( // TODO: Find and set the correct size for the tmpfs in the host const tmpfsSizeForNoRootfs = "65536k" -// annotRootfsParams holds JSON RootfsParams after shim chooseGuestRootfs. -// When present in bundle config.json, Exec reuses it; otherwise Exec runs ChooseRootfs. -const annotRootfsParams = "com.urunc.internal.rootfs.params" - type rootfsBuilder interface { preSetup() error postSetup() error getMounts() ([]specs.Mount, error) getBlockDevs() ([]types.BlockDevParams, error) getSharedDirs() (types.SharedfsParams, error) - preStart() error + preStartCmd() []string } // tmpfsMount creates a mount for a tmpfs in the form of "/tmp" at target @@ -162,7 +158,7 @@ func (n noRootfs) getSharedDirs() (types.SharedfsParams, error) { return types.SharedfsParams{}, nil } -func (n noRootfs) preStart() error { +func (n noRootfs) preStartCmd() []string { return nil } diff --git a/pkg/unikontainers/shared_fs.go b/pkg/unikontainers/shared_fs.go index 9441f39a7..875d9dc6f 100644 --- a/pkg/unikontainers/shared_fs.go +++ b/pkg/unikontainers/shared_fs.go @@ -15,7 +15,6 @@ package unikontainers import ( - "fmt" "path/filepath" "strings" @@ -73,26 +72,24 @@ func (s sharedfsRootfs) getSharedDirs() (types.SharedfsParams, error) { }, nil } -func (s sharedfsRootfs) preStart() error { +func (s sharedfsRootfs) preStartCmd() []string { if s.sfsType == "9pfs" { return nil } - // Start the virtiofsd process - args := []string{ + // The virtiofsd argv, with the binary itself as the first element so it can be + // both stored in the monitor spec and spawned later by spawnProcess. + argv := []string{ + s.vfsdConfig.Path, "--socket-path=/tmp/vhostqemu", "--shared-dir", s.sharedPath, } if s.vfsdConfig.Options != "" { - args = append(args, strings.Fields(s.vfsdConfig.Options)...) + argv = append(argv, strings.Fields(s.vfsdConfig.Options)...) } - err := spawnProcess(s.vfsdConfig.Path, args) - if err != nil { - err = fmt.Errorf("failed to start virtiofsd: %w", err) - } - return err + return argv } func chooseTmpfsSize(sfsType string, mem uint64) string { diff --git a/pkg/unikontainers/types/types.go b/pkg/unikontainers/types/types.go index f5d668b0d..8950d9388 100644 --- a/pkg/unikontainers/types/types.go +++ b/pkg/unikontainers/types/types.go @@ -142,3 +142,14 @@ type MonitorConfig struct { DataPath string `toml:"data_path,omitempty"` // Optional path to the hypervisor data files (e.g. qemu bios stuff) Vhost bool `toml:"vhost,omitempty"` // Optional: enable vhost for network performance optimization } + +// MonitorSpec is everything the post-pivot urunc process needs in order to +// finalize the monitor's process execution environment and exec the monitor. +type MonitorSpec struct { + ContainerID string `json:"containerID"` + UnikernelType string `json:"unikernelType"` + MonitorType string `json:"monitorType"` + MonitorCfg MonitorConfig `json:"monitorCfg"` + ExecArgs ExecArgs `json:"execArgs"` + GuestParams UnikernelParams `json:"guestParams"` +} diff --git a/pkg/unikontainers/unikontainers.go b/pkg/unikontainers/unikontainers.go index ad0bf2897..dcdd570a8 100644 --- a/pkg/unikontainers/unikontainers.go +++ b/pkg/unikontainers/unikontainers.go @@ -169,26 +169,10 @@ func (u *Unikontainer) InitialSetup() error { // if the respective annotation is set then, depending on the guest // (supports block or 9pfs), it will use the supported option. In case // both ae supported, then the block option will be used by default. - var rootfsParams types.RootfsParams - - // Read the rootfs choice written by the shim. - if rootfsParamsJSON := u.Spec.Annotations[annotRootfsParams]; rootfsParamsJSON != "" { - if err := json.Unmarshal([]byte(rootfsParamsJSON), &rootfsParams); err != nil { - return fmt.Errorf("could not decode guest rootfs params: %w", err) - } - } - - if rootfsParams.MonRootfs == "" { - rootfsParams, err = ChooseRootfs(bundleDir, rootfsDir, u.State.Annotations, u.UruncCfg) - if err != nil { - uniklog.Errorf("could not choose guest rootfs: %v", err) - return err - } - encoded, err := json.Marshal(rootfsParams) - if err != nil { - return err - } - u.State.Annotations[annotRootfsParams] = string(encoded) + rootfsParams, err := ChooseRootfs(bundleDir, rootfsDir, u.State.Annotations, u.UruncCfg) + if err != nil { + uniklog.Errorf("could not choose guest rootfs: %v", err) + return err } uniklog.WithFields(logrus.Fields{ "rootfs_type": rootfsParams.Type, @@ -216,6 +200,12 @@ func (u *Unikontainer) InitialSetup() error { if err != nil { return err } + monRes.Rootfs = rootfsParams + + err = rfsBuilder.postSetup() + if err != nil { + return fmt.Errorf("post setup step for rootfs failed: %w", err) + } u.State.Status = specs.StateCreating // FIXME: should we really create this base dir @@ -388,6 +378,13 @@ func getMonitorResources(rfs rootfsBuilder, rootfsParams types.RootfsParams, vmm } res.Devices = append(res.Devices, blockDevs...) + res.Sharedfs, err = rfs.getSharedDirs() + if err != nil { + return res, fmt.Errorf("failed to get directories to share with sandbox: %w", err) + } + + res.PreStartCmd = rfs.preStartCmd() + return res, nil } @@ -446,45 +443,19 @@ func monitorMemoryBytes(defaultMem uint, resources *specs.LinuxResources) uint64 return mem } -// nolint:gocyclo -func (u *Unikontainer) Exec(metrics m.Writer) error { - metrics.Capture(m.TS15) - - // container Paths - // Make sure paths are clean - bundleDir := filepath.Clean(u.State.Bundle) - rootfsDir := filepath.Clean(u.Spec.Root.Path) - rootfsDir, err := resolveAgainstBase(bundleDir, rootfsDir) - if err != nil { - uniklog.Errorf("could not resolve rootfs directory %s: %v", rootfsDir, err) - return err - } +// buildMonitorSpec assembles the base MonitorSpec: everything the monitor needs +// that can be derived from the OCI spec, the container's annotations and the +// monitor resources gathered during InitialSetup. +func (u *Unikontainer) buildMonitorSpec(rootfsParams types.RootfsParams, monRes monitorResources) types.MonitorSpec { + var mSpec types.MonitorSpec - // unikernel unikernelType := u.State.Annotations[annotType] - unikernel, err := unikernels.New(unikernelType) - if err != nil { - return err - } - - // Vmm vmmType := u.State.Annotations[annotHypervisor] - vmm, err := hypervisors.NewVMM(hypervisors.VmmType(vmmType), u.UruncCfg.Monitors) - if err != nil { - return err - } - - // unikernelParams unikernelVersion := u.State.Annotations[annotVersion] - - // ExecArgs unikernelPath := u.State.Annotations[annotBinary] initrdPath := u.State.Annotations[annotInitrd] - // debug uniklog.WithFields(logrus.Fields{ - "bundle directory": bundleDir, - "rootfs directory": rootfsDir, "vmm type": vmmType, "unikernel type": unikernelType, "unikernel version": unikernelVersion, @@ -492,14 +463,12 @@ func (u *Unikontainer) Exec(metrics m.Writer) error { "initrd Path": initrdPath, }).Debug("Initialization values") - // ExecArgs defaultVCPUs := u.UruncCfg.Monitors[vmmType].DefaultVCPUs if defaultVCPUs < 1 { defaultVCPUs = 1 } defaultMemSizeMB := u.UruncCfg.Monitors[vmmType].DefaultMemoryMB - // ExecArgs vmmArgs := types.ExecArgs{ ContainerID: u.State.ID, UnikernelPath: unikernelPath, @@ -510,127 +479,139 @@ func (u *Unikontainer) Exec(metrics m.Writer) error { Environment: os.Environ(), } - // ExecArgs // Check if container is set to unconfined -- disable seccomp if u.Spec.Linux.Seccomp == nil { uniklog.Warn("Seccomp is disabled") vmmArgs.Seccomp = false } - procAttrs := types.ProcessConfig{ - UID: u.Spec.Process.User.UID, - GID: u.Spec.Process.User.GID, - WorkDir: u.Spec.Process.Cwd, - } - // UnikernelParams - // populate unikernel params - unikernelParams := types.UnikernelParams{ - CmdLine: u.Spec.Process.Args, - EnvVars: u.Spec.Process.Env, - Monitor: vmmType, - Version: unikernelVersion, - ProcConf: procAttrs, + guest := types.UnikernelParams{ + CmdLine: u.Spec.Process.Args, + EnvVars: u.Spec.Process.Env, + Monitor: vmmType, + Version: unikernelVersion, + ProcConf: types.ProcessConfig{ + UID: u.Spec.Process.User.UID, + GID: u.Spec.Process.User.GID, + WorkDir: u.Spec.Process.Cwd, + }, NetDevName: u.State.Annotations[annotNetDev], BlkDevName: u.State.Annotations[annotBlkDev], + Rootfs: rootfsParams, + Block: monRes.BlockArgs, } - if len(unikernelParams.CmdLine) == 0 { - unikernelParams.CmdLine = strings.Fields(u.State.Annotations[annotCmdLine]) + if len(guest.CmdLine) == 0 { + guest.CmdLine = strings.Fields(u.State.Annotations[annotCmdLine]) } - // handle network - netArgs, err := u.SetupNet() - if err != nil { - uniklog.Errorf("failed to setup network: %v", err) - return err + if rootfsParams.Type == "virtiofs" || rootfsParams.Type == "9pfs" { + // Update the paths of the files we need to pass in the monitor process. + vmmArgs.UnikernelPath = adjustPathsForSharedfs(vmmArgs.UnikernelPath) + vmmArgs.InitrdPath = adjustPathsForSharedfs(vmmArgs.InitrdPath) } - metrics.Capture(m.TS16) - withTUNTAP := netArgs.IP != "" + vmmArgs.Sharedfs = monRes.Sharedfs - // UnikernelParams - unikernelParams.Net = netArgs - - // ExecArgs - vmmArgs.Net = netArgs + mSpec.ContainerID = u.State.ID + mSpec.UnikernelType = unikernelType + mSpec.MonitorType = vmmType + mSpec.MonitorCfg = u.UruncCfg.Monitors[vmmType] + mSpec.ExecArgs = vmmArgs + mSpec.GuestParams = guest - // guest rootfs - // block - // handle guest's rootfs. - // There are three options: - // 1. No rootfs for guest - // 2. Use the devmapper snapshot as a block device for the guest's rootfs - // 3. Use 9pfs to share the container's rootfs as the guest's rootfs - // By default, urunc will not set any rootfs for the guest. However, - // if the respective annotation is set then, depending on the guest - // (supports block or 9pfs), it will use the supported option. In case - // both ae supported, then the block option will be used by default. - var rootfsParams types.RootfsParams + return mSpec +} - // Read the rootfs choice written by the shim. - if rootfsParamsJSON := u.State.Annotations[annotRootfsParams]; rootfsParamsJSON != "" { - if err := json.Unmarshal([]byte(rootfsParamsJSON), &rootfsParams); err != nil { - return fmt.Errorf("could not decode guest rootfs params: %w", err) - } +// setupMonitorRootfs prepares the monitor rootfs: it makes sure the directory +// exists and is mounted with a propagation flag that allows a later pivot, then +// replicates the gathered mounts and devices inside it and gives the monitor a +// console. +func (u *Unikontainer) setupMonitorRootfs(monRootfs string, monRes monitorResources, withTUNTAP bool) error { + err := os.MkdirAll(monRootfs, 0o755) + if err != nil { + return fmt.Errorf("failed to create monitor rootfs directory %s: %w", monRootfs, err) } - if rootfsParams.MonRootfs == "" { - uniklog.Errorf("missing annotations from selected rootfs") - return fmt.Errorf("missing metadata for rootfs preparation") + // Make sure that rootfs is mounted with the correct propagation + // flags so we can later pivot if needed. + err = prepareRoot(monRootfs, u.Spec.Linux.RootfsPropagation) + if err != nil { + return err } - uniklog.WithFields(logrus.Fields{ - "rootfs_type": rootfsParams.Type, - "rootfs_path": rootfsParams.Path, - "mon_rootfs": rootfsParams.MonRootfs, - }).Debug("guest rootfs params") - rfsBuilder := u.newRootfsBuilder(rootfsParams, unikernel, unikernelPath, initrdPath, vmmArgs.MemSizeB) - if rootfsParams.Type == "virtiofs" || rootfsParams.Type == "9pfs" { - // Update the paths of the files we need to pass in the monitor process. - vmmArgs.UnikernelPath = adjustPathsForSharedfs(vmmArgs.UnikernelPath) - vmmArgs.InitrdPath = adjustPathsForSharedfs(vmmArgs.InitrdPath) + err = applyMounts(monRootfs, monRes.Mounts) + if err != nil { + return fmt.Errorf("failed to apply rootfs mounts: %w", err) } - if err = os.MkdirAll(rootfsParams.MonRootfs, 0o755); err != nil { - return fmt.Errorf("failed to create monitor rootfs directory %s: %w", rootfsParams.MonRootfs, err) + // setupDevices decides whether to create the TUN/TAP device based on the + // container's network configuration. + err = setupDevices(monRootfs, monRes.Devices, withTUNTAP) + if err != nil { + return fmt.Errorf("failed to create devices in monitor rootfs: %w", err) } - // Prepare Monitor rootfs - // Make sure that rootfs is mounted with the correct propagation - // flags so we can later pivot if needed. - err = prepareRoot(rootfsParams.MonRootfs, u.Spec.Linux.RootfsPropagation) + err = setupConsole(monRootfs) if err != nil { - return err + return fmt.Errorf("failed to setup console: %w", err) } - // The monitor mounts and devices were gathered and stored in a file during - // InitialSetup; here we just apply them. postSetup applies the container's own - // bind mounts on top of the shared rootfs, and setupDevices decides whether to - // create the TUN/TAP device based on the container's network configuration. + return nil +} + +// nolint:gocyclo +func (u *Unikontainer) Exec(metrics m.Writer) error { + metrics.Capture(m.TS15) + + // The chosen guest rootfs params, together with the monitor mounts, devices + // and block args, were gathered and stored in monitor.json during + // InitialSetup. Load them back here. monRes, err := loadMonitorResources(u.BaseDir) if err != nil { return fmt.Errorf("failed to load monitor resources: %w", err) } + rootfsParams := monRes.Rootfs + if rootfsParams.MonRootfs == "" { + uniklog.Errorf("missing metadata for selected rootfs") + return fmt.Errorf("missing metadata for rootfs preparation") + } + uniklog.WithFields(logrus.Fields{ + "rootfs_type": rootfsParams.Type, + "rootfs_path": rootfsParams.Path, + "mon_rootfs": rootfsParams.MonRootfs, + }).Debug("guest rootfs params") + + ms := u.buildMonitorSpec(rootfsParams, monRes) + vmmArgs := ms.ExecArgs + unikernelParams := ms.GuestParams - err = applyMounts(rootfsParams.MonRootfs, monRes.Mounts) + // The spec carries the monitor and unikernel by type; rebuild the behaviour + // objects it refers to, exactly as the monitor process does. + unikernel, err := unikernels.New(ms.UnikernelType) if err != nil { - return fmt.Errorf("failed to apply rootfs mounts: %w", err) + return err } - - err = rfsBuilder.postSetup() + vmm, err := hypervisors.NewVMM(hypervisors.VmmType(ms.MonitorType), u.UruncCfg.Monitors) if err != nil { - return fmt.Errorf("post setup step for rootfs failed: %w", err) + return err } - err = setupDevices(rootfsParams.MonRootfs, monRes.Devices, withTUNTAP) + // handle network + netArgs, err := u.SetupNet() if err != nil { - return fmt.Errorf("failed to create devices in monitor rootfs: %w", err) + uniklog.Errorf("failed to setup network: %v", err) + return err } + metrics.Capture(m.TS16) + withTUNTAP := netArgs.IP != "" + unikernelParams.Net = netArgs + vmmArgs.Net = netArgs - err = setupConsole(rootfsParams.MonRootfs) + err = u.setupMonitorRootfs(rootfsParams.MonRootfs, monRes, withTUNTAP) if err != nil { return err } - metrics.Capture(m.TS17) + // vAccel setup vAccelType, vsockSocketPath, rpcAddress, err := resolveVAccelConfig(u.State.Annotations[annotHypervisor], u.Spec.Annotations) if err != nil { @@ -664,20 +645,6 @@ func (u *Unikontainer) Exec(metrics m.Writer) error { vmmArgs.VSockDevID = idToGuestCID(u.State.ID) } - unikernelParams.Rootfs = rootfsParams - - // unikernelParams - // The block parameters were gathered in InitialSetup and stored in the - // monitor resources file. - unikernelParams.Block = monRes.BlockArgs - - // ExecArgs - sharedfsArgs, err := rfsBuilder.getSharedDirs() - if err != nil { - return fmt.Errorf("failed to get directories to share with sandbox: %w", err) - } - vmmArgs.Sharedfs = sharedfsArgs - // unikernel err = unikernel.Init(unikernelParams) if errors.Is(err, unikernels.ErrUndefinedVersion) || @@ -687,14 +654,11 @@ func (u *Unikontainer) Exec(metrics m.Writer) error { return err } - // unikernel // build the unikernel command unikernelCmd, err := unikernel.CommandString() if err != nil { return err } - - // ExecArgs vmmArgs.Command = unikernelCmd // pivot @@ -728,7 +692,7 @@ func (u *Unikontainer) Exec(metrics m.Writer) error { return err } - err = rfsBuilder.preStart() + err = spawnProcess(monRes.PreStartCmd) if err != nil { return err } diff --git a/pkg/unikontainers/utils.go b/pkg/unikontainers/utils.go index 90b9500cf..68977dd0d 100644 --- a/pkg/unikontainers/utils.go +++ b/pkg/unikontainers/utils.go @@ -44,12 +44,16 @@ const ( rootfsDirName = "rootfs" ) -// monitorResources holds the mounts and devices that must be replicated inside -// the monitor's rootfs, along with the block parameters the guest needs. +// monitorResources holds the chosen guest rootfs params, the mounts and devices +// that must be replicated inside the monitor's rootfs, and the block parameters +// the guest needs. type monitorResources struct { - Mounts []specs.Mount `json:"mounts"` - Devices []specs.LinuxDevice `json:"devices"` - BlockArgs []types.BlockDevParams `json:"blockArgs"` + Rootfs types.RootfsParams `json:"rootfs"` + Mounts []specs.Mount `json:"mounts"` + Devices []specs.LinuxDevice `json:"devices"` + BlockArgs []types.BlockDevParams `json:"blockArgs"` + Sharedfs types.SharedfsParams `json:"sharedfs"` + PreStartCmd []string `json:"preStartCmd,omitempty"` } // saveMonitorResources stores the monitorResources passed as an argument in a JSON @@ -243,14 +247,20 @@ func convertUint32ToIntSlice(valSlice []uint32, size int) []int { // return data.Bytes(), nil // } -func spawnProcess(binaryPath string, args []string) error { - cmd := exec.Command(binaryPath, args...) +// spawnProcess starts the process described by argv, whose first element is the +// binary. An empty argv is a no-op. +func spawnProcess(argv []string) error { + if len(argv) == 0 { + return nil + } + // argv is built by urunc, based on its config, not untrusted input + cmd := exec.Command(argv[0], argv[1:]...) //nolint:gosec cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { - return err + return fmt.Errorf("failed to start %s: %w", argv[0], err) } return nil From e397503d9fbda0f5e13d5a3b140a23f77a720257 Mon Sep 17 00:00:00 2001 From: Charalampos Mainas Date: Wed, 29 Jul 2026 16:41:10 +0200 Subject: [PATCH 2/2] refactor: Break Exec and create in smaller reusable functions Break the large Exec function and the create function into smaller functions that cna be reused: - split createUnikontainer into newUnikontainer (bundle parse and InitialSetup) and the reexec handshake - make SetupNet a standalone function - pull buildUnikernelCommand and execMonitor out of Exec The rationale is to let the later port of libcontainer to use some of this functionality directly instead of duplicating logic. Signed-off-by: Charalampos Mainas --- cmd/urunc/create.go | 43 ++++++++++++------- pkg/unikontainers/unikontainers.go | 68 +++++++++++++++++------------- 2 files changed, 65 insertions(+), 46 deletions(-) diff --git a/cmd/urunc/create.go b/cmd/urunc/create.go index 5a9e2f94d..75118af98 100644 --- a/cmd/urunc/create.go +++ b/cmd/urunc/create.go @@ -82,16 +82,15 @@ var createCommand = &cli.Command{ }, } -// createUnikontainer creates a Unikernel struct from bundle data, -// initializes it's base dir and state.json, -// setups terminal if required and spawns reexec process, -// waits for reexec process to notify, executes CreateRuntime hooks, -// sends ACK to reexec process -func createUnikontainer(cmd *cli.Command, uruncCfg *unikontainers.UruncConfig) (err error) { - err = nil +// newUnikontainer parses the bundle and performs the host-side preparation for +// the monitor execution environment (Unikontainer, base directory, state and +// monitor resources). It never returns for a container that is not a urunc +// container: those are handed over to the real runc with an execve. +func newUnikontainer(cmd *cli.Command, uruncCfg *unikontainers.UruncConfig) (*unikontainers.Unikontainer, error) { containerID := cmd.Args().First() - if err = validateID(containerID); err != nil { - return err + err := validateID(containerID) + if err != nil { + return nil, err } metrics.SetLoggerContainerID(containerID) metrics.Capture(m.TS00) @@ -105,7 +104,7 @@ func createUnikontainer(cmd *cli.Command, uruncCfg *unikontainers.UruncConfig) ( if bundlePath == "" { bundlePath, err = os.Getwd() if err != nil { - return err + return nil, err } } @@ -114,21 +113,33 @@ func createUnikontainer(cmd *cli.Command, uruncCfg *unikontainers.UruncConfig) ( if err != nil { if errors.Is(err, unikontainers.ErrQueueProxy) || errors.Is(err, unikontainers.ErrNotUnikernel) { - // Exec runc to handle non unikernel containers - err = runcExec() - return err + // Exec runc to handle non urunc containers. + // It should never return. + return nil, runcExec() } - return err + return nil, err } metrics.Capture(m.TS01) err = unikontainer.InitialSetup() if err != nil { - return err + return nil, err } - metrics.Capture(m.TS02) + return unikontainer, nil +} + +// createUnikontainer creates a Unikernel struct from bundle data, initializes +// it's base dir and state.json, setups terminal if required and spawns reexec +// process, waits for reexec process to notify, executes CreateRuntime hooks, +// sends ACK to reexec process +func createUnikontainer(cmd *cli.Command, uruncCfg *unikontainers.UruncConfig) (err error) { + unikontainer, err := newUnikontainer(cmd, uruncCfg) + if err != nil { + return err + } + // Create socket for nsenter initSockParent, initSockChild, err := newSockPair("init") if err != nil { diff --git a/pkg/unikontainers/unikontainers.go b/pkg/unikontainers/unikontainers.go index dcdd570a8..5b3db8f24 100644 --- a/pkg/unikontainers/unikontainers.go +++ b/pkg/unikontainers/unikontainers.go @@ -245,8 +245,9 @@ func (u *Unikontainer) SetRunningState() error { return u.saveContainerState() } -func (u *Unikontainer) SetupNet() (types.NetDevParams, error) { - networkType := u.getNetworkType() +// SetupNet creates the sandbox's network device (tap) in the current network +// namespace and returns its parameters; uid and gid own the tap device. +func SetupNet(networkType string, uid, gid uint32) (types.NetDevParams, error) { uniklog.WithField("network type", networkType).Debug("Retrieved network type") netArgs := types.NetDevParams{} netManager, err := network.NewNetworkManager(networkType) @@ -254,7 +255,7 @@ func (u *Unikontainer) SetupNet() (types.NetDevParams, error) { return netArgs, fmt.Errorf("failed to create network manager for %s type: %v", networkType, err) } - networkInfo, err := netManager.NetworkSetup(u.Spec.Process.User.UID, u.Spec.Process.User.GID) + networkInfo, err := netManager.NetworkSetup(uid, gid) if err != nil { // TODO: Handle this case better. We do not need to show an error // since there was no network in the container. Therefore, we @@ -443,9 +444,6 @@ func monitorMemoryBytes(defaultMem uint, resources *specs.LinuxResources) uint64 return mem } -// buildMonitorSpec assembles the base MonitorSpec: everything the monitor needs -// that can be derived from the OCI spec, the container's annotations and the -// monitor resources gathered during InitialSetup. func (u *Unikontainer) buildMonitorSpec(rootfsParams types.RootfsParams, monRes monitorResources) types.MonitorSpec { var mSpec types.MonitorSpec @@ -596,7 +594,7 @@ func (u *Unikontainer) Exec(metrics m.Writer) error { } // handle network - netArgs, err := u.SetupNet() + netArgs, err := SetupNet(u.getNetworkType(), u.Spec.Process.User.UID, u.Spec.Process.User.GID) if err != nil { uniklog.Errorf("failed to setup network: %v", err) return err @@ -646,20 +644,11 @@ func (u *Unikontainer) Exec(metrics m.Writer) error { } // unikernel - err = unikernel.Init(unikernelParams) - if errors.Is(err, unikernels.ErrUndefinedVersion) || - errors.Is(err, unikernels.ErrVersionParsing) { - uniklog.WithError(err).Error("an error occurred while initializing the unikernel") - } else if err != nil { - return err - } - // build the unikernel command - unikernelCmd, err := unikernel.CommandString() + vmmArgs.Command, err = buildUnikernelCommand(unikernel, unikernelParams) if err != nil { return err } - vmmArgs.Command = unikernelCmd // pivot _, err = findNS(u.Spec.Linux.Namespaces, specs.MountNamespace) @@ -697,30 +686,49 @@ func (u *Unikontainer) Exec(metrics m.Writer) error { return err } - uniklog.Debug("calling vmm execve") - metrics.Capture(m.TS18) - - // Build the VMM command once and verify it can be constructed successfully. - // This ensures we don't report the container as started if command building fails. + // Build the VMM command once and verify it can be constructed successfully, so + // we do not report the container as started if command building fails. execCmd, err := vmm.BuildExecCmd(vmmArgs, unikernel) if err != nil { uniklog.WithError(err).Error("failed to build VMM command") return err } - // Notify urunc start that the monitor is ready to execute. - // We send this after BuildExecCmd succeeds to avoid reporting a container - // as started when the VMM command cannot be built. - // TODO: The container can still be reported as running if the PreExec step - // (e.g., BPF/seccomp filter setup) fails after this point. We should find - // a way to handle that case as well. + // Notify urunc start that the monitor is ready to execute, only after the + // command builds so a container is never reported started when it cannot be. err = u.SendMessage(StartSuccess) if err != nil { return err } + return execMonitor(metrics, vmm, vmmArgs, execCmd) +} + +// buildUnikernelCommand initializes the unikernel with the collected parameters +// and returns its command line. +func buildUnikernelCommand(unikernel types.Unikernel, params types.UnikernelParams) (string, error) { + err := unikernel.Init(params) + if errors.Is(err, unikernels.ErrUndefinedVersion) || + errors.Is(err, unikernels.ErrVersionParsing) { + uniklog.WithError(err).Error("an error occurred while initializing the unikernel") + } else if err != nil { + return "", err + } + + return unikernel.CommandString() +} + +// execMonitor runs the monitor's pre-exec setup and finally execve's the monitor. +// It does not return on success: +// +// TODO: The container can still be reported as running if the PreExec step +// (e.g., BPF/seccomp filter setup) fails after the caller reported success. We +// should find a way to handle that case as well. +func execMonitor(metrics m.Writer, vmm types.VMM, execArgs types.ExecArgs, execCmd []string) error { + uniklog.Debug("calling vmm execve") + metrics.Capture(m.TS18) // Perform any monitor-specific pre-exec setup (e.g., seccomp filters for HVT). - err = vmm.PreExec(vmmArgs) + err := vmm.PreExec(execArgs) if err != nil { uniklog.WithError(err).Error("failed to perform pre-exec setup") return err @@ -728,7 +736,7 @@ func (u *Unikontainer) Exec(metrics m.Writer) error { // Execute the VMM using the command we built earlier. uniklog.WithField("command", execCmd).Debug("Ready to execve VMM") - return syscall.Exec(vmm.Path(), execCmd, vmmArgs.Environment) //nolint: gosec + return syscall.Exec(vmm.Path(), execCmd, execArgs.Environment) //nolint: gosec } func setupUser(user specs.User) error {