Skip to content

refactor(shim): precompute guest rootfs after task create#702

Merged
cmainas merged 1 commit into
urunc-dev:main-pr702from
sidneychang:feat/shim-rootfs-precompute
May 22, 2026
Merged

refactor(shim): precompute guest rootfs after task create#702
cmainas merged 1 commit into
urunc-dev:main-pr702from
sidneychang:feat/shim-rootfs-precompute

Conversation

@sidneychang
Copy link
Copy Markdown
Contributor

@sidneychang sidneychang commented May 18, 2026

Description

This PR precomputes guest rootfs in the urunc containerd task shim after inner TaskService.Create mounts bundle/rootfs, so block-backed snapshots (e.g. devmapper) can be selected without relying on getMountInfo on an unmounted path.

Problem

Previously, guest rootfs selection was performed only in the runtime Exec() path. In the containerd shim path, this made block-backed container rootfs detection fragile, because the runtime had to rediscover the guest rootfs from the bundle rootfs path later through getMountInfo.

For block-backed snapshots such as devmapper, the rootfs decision should be made only after the wrapped containerd task service has completed Create, when the bundle rootfs has already been prepared by the normal containerd/runc create flow. This allows urunc to reuse the existing rootfs selection logic on the actual bundle rootfs path, instead of introducing a separate shim-only rootfs discovery path.

Solution

This PR moves guest rootfs selection into the urunc containerd task shim after the wrapped TaskService.Create succeeds.

The shim now:

  • lets the inner task service run Create first;
  • reads the bundle config.json;
  • obtains the unikernel annotations and urunc configuration;
  • calls the shared ChooseRootfs implementation with the bundle path and OCI spec.Root.Path;
  • stores the selected RootfsParams in config.json under the internal annotation com.urunc.internal.rootfs.params.

Then, runtime Exec() consumes this precomputed rootfs result when the annotation is present. If the annotation is absent, such as in direct urunc / podman-style flows, Exec() falls back to running ChooseRootfs itself. This keeps the original runtime behavior while making the containerd shim path more reliable for block-backed rootfs selection.

Related issues

How was this tested?

LLM usage

Cursor

Checklist

  • I have read the contribution guide.
  • The linter passes locally (make lint).
  • The e2e tests of at least one tool pass locally (make test_ctr, make test_nerdctl, make test_docker, make test_crictl).
  • If LLMs were used: I have read the llm policy.

@netlify
Copy link
Copy Markdown

netlify Bot commented May 18, 2026

Deploy Preview for urunc canceled.

Name Link
🔨 Latest commit 9e58d34
🔍 Latest deploy log https://app.netlify.com/projects/urunc/deploys/6a1020fafe45d00008bdea8e

@sidneychang
Copy link
Copy Markdown
Contributor Author

Hi @cmainas ,
Could you please take a look at PR #702 when you have time?
Thanks!

Copy link
Copy Markdown
Contributor

@cmainas cmainas left a comment

Choose a reason for hiding this comment

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

Hello @sidneychang ,

I have added several comments in some changes. Moreover:

  • I do not think we need to encode for the new rootfs annotations that the shim creates. Please let me know if I am wrong.
  • Why do not we perform the chooseRootfa functionality after TaskService.Create? THis will simplify a lot the changes.
  • The logic of ChooseRootfs has changed a lot and it has made the code less cleaner. For example, it creates new instances for a unikernel interface multiple times, while just a single instance is enough as the original code.
  • The code for annotations is all around the place. Just place it in one file and let it be ugly. We will just throw it away.

Comment thread pkg/unikontainers/config.go Outdated
if err := conf.validate(); err != nil {
return nil, err
}
if err := conf.decode(); err != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We do not need to decode the annotations passed form the shim. They should be plain test.

Comment thread pkg/unikontainers/config.go Outdated
return jsonConf, nil
}

// GetUnikernelConfigFromSpecAnnotations retrieves urunc configuration only
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since this function is called from the shim, then the annotations should already be in the config and hence we can reuse the previous function which will not fallback to urunc.json if the annotations exist in the config.json

Comment thread pkg/unikontainers/unikontainers.go Outdated
Comment on lines +368 to +370
// Decode from State.Annotations only: the shim writes the internal key to
// Spec (config.json), and saveContainerState copies missing Spec keys into
// state.json during create/InitialSetup before reexec reaches Exec.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

While this is correct and we should follow it across urunc, we do not need an explicit comment just for that..

Comment thread pkg/unikontainers/rootfs.go Outdated

// DecodeRootfsParams reads rootfs parameters from OCI annotations. The bool
// return value reports whether the internal annotation was present.
func DecodeRootfsParams(annotations map[string]string) (types.RootfsParams, bool, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can derive if the shim has done the rootfs selection by checking the MonRootfs value when it gets returned.

Comment thread pkg/unikontainers/unikontainers.go
Comment thread pkg/unikontainers/rootfs.go Outdated
return nil
}

unikernel, err := unikernels.New(unikernelType)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This check should not be performed here, but as before we should try each one of the options in one place.

Comment thread pkg/unikontainers/rootfs.go Outdated
// bundle layout and annotations. specRoot may be relative to bundle. rootfsMounts
// carries CreateTask mounts from the shim; when nil, block backing may be probed
// from the live container rootfs mount.
func ChooseRootfs(bundle, specRoot string, annot map[string]string, cfg *UruncConfig, rootfsMounts []RootfsMount) (types.RootfsParams, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This function changes the whole logic of choosing rootfs a lot. I do not really see why this is necessary and the previous logic should remain the same.

Comment thread pkg/unikontainers/rootfs.go Outdated
Comment on lines +111 to +124
if containerRootfsBlock == nil && shouldMountContainerRootfs(annot) {
unikernelPreview, err := unikernels.New(annot[annotType])
if err != nil {
return types.RootfsParams{}, err
}
if unikernelPreview.SupportsBlock() {
rootFsDevice, err := getMountInfo(rootfsDir)
if err == nil {
containerRootfsBlock = &rootFsDevice
} else if !errors.Is(err, ErrMountpoint) {
uniklog.Errorf("failed to get container's rootfs mount info: %v", err)
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I do not really see the benefit if this checks here. We can directly call or use the logic from selectRootfs

Comment thread pkg/unikontainers/rootfs.go Outdated
Comment on lines +134 to +218
annotValue := rs.annot[annotMountRootfs]
return shouldMountContainerRootfs(rs.annot)
}

func shouldMountContainerRootfs(annot map[string]string) bool {
annotValue := annot[annotMountRootfs]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This would not be necessary if the logic of chooseRootfs remained the same.

Comment thread pkg/unikontainers/rootfs.go Outdated
func RootfsParamsAnnotation() string {
return annotInternalRootfsParams
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unnecessary function

@sidneychang sidneychang force-pushed the feat/shim-rootfs-precompute branch 7 times, most recently from 88a6e4c to e813c54 Compare May 21, 2026 08:59
@sidneychang
Copy link
Copy Markdown
Contributor Author

Thank you @cmainas for the detailed review and helpful suggestions.

I have updated the code according to your comments.

@sidneychang sidneychang changed the title refactor(shim): precompute guest rootfs before task create refactor(shim): precompute guest rootfs after task create May 21, 2026
@sidneychang sidneychang force-pushed the feat/shim-rootfs-precompute branch from 1552ffa to e813c54 Compare May 21, 2026 09:33
Copy link
Copy Markdown
Contributor

@cmainas cmainas left a comment

Choose a reason for hiding this comment

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

Thank you @sidneychang for addressing the comments. The code seems way better now. I have added some not that important comments and uurnc ensures that even if the configuration does not exist, the defaults values will be used. It would be very helpful if we could also do that in the shim, reusing the configuration part from urunc.

Comment thread pkg/unikontainers/unikontainers.go Outdated
if u.Spec.Root != nil {
specRoot = u.Spec.Root.Path
}
rootfsParams, err = ChooseRootfs(u.State.Bundle, specRoot, u.State.Annotations, u.UruncCfg)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

At this point we should assume that the u.Spec.Root exists. Not that important though.

}
res.MonRootfs = monRootfs

return res, nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am not sure if we need to change this function. It is called from chooseRootfs and even in the levle of shim or in the level of urunc, there is no issue of creating it. We just need to make sure to delete it later.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tracking who created the directory might be a nightmare though.

Comment thread pkg/unikontainers/unikontainers.go Outdated
}
}

if _, err = os.Stat(rootfsParams.MonRootfs); err != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems we can directly call MkdiAll. According to the documentation "If path is already a directory, MkdirAll does nothing and returns nil. " from https://pkg.go.dev/os#MkdirAll

@sidneychang sidneychang force-pushed the feat/shim-rootfs-precompute branch from e813c54 to d7beeca Compare May 21, 2026 10:19
@sidneychang sidneychang marked this pull request as ready for review May 21, 2026 10:23
@sidneychang sidneychang force-pushed the feat/shim-rootfs-precompute branch from d7beeca to da580a7 Compare May 21, 2026 15:12
@sidneychang
Copy link
Copy Markdown
Contributor Author

Hi @cmainas
I found that in the runtime code we already get default extra_binaries via UruncConfigFromMap / defaultExtraBinConfig(), but LoadUruncConfig (used by the shim) did not seed that before decoding config.toml.

When config.toml had only [log] and no [extra_binaries]:LoadUruncConfig used cfg := &UruncConfig{} and did not pre-seed defaultExtraBinConfig() (unlike UruncConfigFromMap), so a successful decode left ExtraBins empty → shim ChooseRootfs got an empty virtiofsd path → tryVirtiofs failed fileExists(vfsdPath) → Cloud Hypervisor only supports virtio shared-fs (no 9p fallback), so guest rootfs could not be chosen on shim Create.

Fix: initialize ExtraBins with defaultExtraBinConfig() before toml.DecodeFile, matching runtime behavior:

cfg := &UruncConfig{ExtraBins: defaultExtraBinConfig()}

CI should be green now.

@cmainas
Copy link
Copy Markdown
Contributor

cmainas commented May 22, 2026

Hello @sidneychang , this is a nice catch. However, should not we use the default config for all the fields of the configuration? I mean we should extend your change and initialize cfg variable with he default config before the decoding takes place. In that way all non-specified values will be set to the default ones. However, we need to ensure that https://github.com/BurntSushi/toml does indeed follow this behavior.

@sidneychang sidneychang force-pushed the feat/shim-rootfs-precompute branch from da580a7 to 0601f25 Compare May 22, 2026 09:18
Run ChooseRootfs in the shim after inner task Create, once the bundle
rootfs is mounted (urunc-dev#684). Persist JSON RootfsParams in config.json as
com.urunc.internal.rootfs.params so Exec reuses the choice; read state
then spec annotations because reexec may not yet mirror config.json.

Export ChooseRootfs from unikontainers. When the annotation is absent,
Exec runs the same selection as the former u.chooseRootfs() (podman and
urunc CLI). Ensure MonRootfs exists before rootfs setup in Exec.

Fixes: urunc-dev#684

Signed-off-by: sidneychang <2190206983@qq.com>
@sidneychang sidneychang force-pushed the feat/shim-rootfs-precompute branch from 0601f25 to 9e58d34 Compare May 22, 2026 09:25
@urunc-bot urunc-bot Bot changed the base branch from main to main-pr702 May 22, 2026 12:16
Copy link
Copy Markdown
Contributor

@cmainas cmainas left a comment

Choose a reason for hiding this comment

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

Thank you @sidneychang for this change.

@cmainas cmainas merged commit 36aa933 into urunc-dev:main-pr702 May 22, 2026
34 checks passed
github-actions Bot pushed a commit that referenced this pull request May 22, 2026
Run ChooseRootfs in the shim after inner task Create, once the bundle
rootfs is mounted (#684). Persist JSON RootfsParams in config.json as
com.urunc.internal.rootfs.params so Exec reuses the choice; read state
then spec annotations because reexec may not yet mirror config.json.

Export ChooseRootfs from unikontainers. When the annotation is absent,
Exec runs the same selection as the former u.chooseRootfs() (podman and
urunc CLI). Ensure MonRootfs exists before rootfs setup in Exec.

Fixes: #684

PR: #702
Signed-off-by: sidneychang <2190206983@qq.com>
Reviewed-by: Charalampos Mainas <cmainas@nubificus.co.uk>
Approved-by: Charalampos Mainas <cmainas@nubificus.co.uk>
urunc-bot Bot pushed a commit that referenced this pull request May 22, 2026
Run ChooseRootfs in the shim after inner task Create, once the bundle
rootfs is mounted (#684). Persist JSON RootfsParams in config.json as
com.urunc.internal.rootfs.params so Exec reuses the choice; read state
then spec annotations because reexec may not yet mirror config.json.

Export ChooseRootfs from unikontainers. When the annotation is absent,
Exec runs the same selection as the former u.chooseRootfs() (podman and
urunc CLI). Ensure MonRootfs exists before rootfs setup in Exec.

Fixes: #684

PR: #702
Signed-off-by: sidneychang <2190206983@qq.com>
Reviewed-by: Charalampos Mainas <cmainas@nubificus.co.uk>
Approved-by: Charalampos Mainas <cmainas@nubificus.co.uk>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Move guest rootfs selection earlier to the shim

2 participants