-
Notifications
You must be signed in to change notification settings - Fork 16
Use virtiofs to support bind-mounts #81
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| //go:build linux | ||
|
|
||
| /* | ||
| Copyright The containerd Authors. | ||
|
|
||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
|
|
||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
|
|
||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "os" | ||
| "strings" | ||
|
|
||
| "github.com/containerd/containerd/v2/core/mount" | ||
| "github.com/containerd/log" | ||
| ) | ||
|
|
||
| type bindMounts []bindMount | ||
|
|
||
| type bindMount struct { | ||
| tag string | ||
| target string | ||
| } | ||
|
|
||
| func (b *bindMounts) String() string { | ||
| ss := make([]string, 0, len(*b)) | ||
| for _, bm := range *b { | ||
| ss = append(ss, bm.tag+":"+bm.target) | ||
| } | ||
| return strings.Join(ss, ",") | ||
| } | ||
|
|
||
| func (b *bindMounts) Set(value string) error { | ||
| tag, target, ok := strings.Cut(value, ":") | ||
| if !ok || len(tag) == 0 || len(target) == 0 { | ||
| return fmt.Errorf("invalid bind mount %q: expected format: tag:target", value) | ||
| } | ||
| *b = append(*b, bindMount{ | ||
| tag: tag, | ||
| target: target, | ||
| }) | ||
| return nil | ||
| } | ||
|
|
||
| func (b *bindMounts) mountAll(ctx context.Context) error { | ||
| for _, bm := range *b { | ||
| log.G(ctx).WithFields(log.Fields{ | ||
| "tag": bm.tag, | ||
| "target": bm.target, | ||
| }).Info("mounting virtiofs filesystem") | ||
|
|
||
| if err := os.MkdirAll(bm.target, 0700); err != nil { | ||
| return fmt.Errorf("failed to create bind mount target directory %s: %w", bm.target, err) | ||
| } | ||
| if err := mount.All([]mount.Mount{{ | ||
| Type: "virtiofs", | ||
| Source: bm.tag, | ||
| Target: bm.target, | ||
| }}, "/"); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| //go:build linux | ||
|
|
||
| /* | ||
| Copyright The containerd Authors. | ||
|
|
||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
|
|
||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
|
|
||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestParseBindMounts(t *testing.T) { | ||
| testcases := []struct { | ||
| name string | ||
| inputs []string | ||
| want bindMounts | ||
| wantStr string | ||
| }{ | ||
| { | ||
| name: "single bind mount", | ||
| inputs: []string{"foo:/mnt/foo"}, | ||
| want: bindMounts{ | ||
| {tag: "foo", target: "/mnt/foo"}, | ||
| }, | ||
| wantStr: "foo:/mnt/foo", | ||
| }, | ||
| { | ||
| name: "multiple bind mounts", | ||
| inputs: []string{"foo:/mnt/foo", "bar:/mnt/bar"}, | ||
| want: bindMounts{ | ||
| {tag: "foo", target: "/mnt/foo"}, | ||
| {tag: "bar", target: "/mnt/bar"}, | ||
| }, | ||
| wantStr: "foo:/mnt/foo,bar:/mnt/bar", | ||
| }, | ||
| { | ||
| name: "bind mount with nested path", | ||
| inputs: []string{"config:/mnt/etc/config"}, | ||
| want: bindMounts{ | ||
| {tag: "config", target: "/mnt/etc/config"}, | ||
| }, | ||
| wantStr: "config:/mnt/etc/config", | ||
| }, | ||
| } | ||
|
|
||
| for _, tc := range testcases { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| var b bindMounts | ||
| for _, input := range tc.inputs { | ||
| err := b.Set(input) | ||
| assert.NoError(t, err) | ||
| } | ||
| assert.Equal(t, tc.want, b) | ||
| // Try to convert back the parsed struct into a string to check if it matches the expected output. | ||
| assert.Equal(t, tc.wantStr, b.String()) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestParseBindMountsError(t *testing.T) { | ||
| testcases := []struct { | ||
| name string | ||
| input string | ||
| }{ | ||
| { | ||
| name: "missing target", | ||
| input: "foo", | ||
| }, | ||
| { | ||
| name: "empty tag", | ||
| input: ":foo", | ||
| }, | ||
| { | ||
| name: "empty target", | ||
| input: "foo:", | ||
| }, | ||
| { | ||
| name: "empty string", | ||
| input: "", | ||
| }, | ||
| } | ||
|
|
||
| for _, tc := range testcases { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| var b bindMounts | ||
| err := b.Set(tc.input) | ||
| assert.Error(t, err) | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # Bind Mounts | ||
|
|
||
| Nerdbox supports bind mounts from the host into containers running inside the | ||
| VM. Bind mounts are implemented using virtiofs to share host paths with the VM, | ||
| which then bind-mounts them into containers. | ||
|
|
||
| ## How It Works | ||
|
|
||
| When a bind mount is specified in the container spec: | ||
|
|
||
| 1. The shim transforms the bind mount into a virtiofs share | ||
| 2. The host path is shared with the VM via virtiofs with a unique tag | ||
| 3. Inside the VM, virtiofs is mounted at a temporary location (`/mnt/bind-{hash}`) | ||
| 4. The container runtime bind-mounts from that location into the container | ||
|
|
||
| ## Directory Bind Mounts | ||
|
|
||
| For directory bind mounts, the directory is shared directly via virtiofs: | ||
|
|
||
| ``` | ||
| Host: /host/data/ → virtiofs share → VM: /mnt/bind-{hash}/ → Container: /container/data/ | ||
| ``` | ||
|
|
||
| ## File Bind Mounts | ||
|
|
||
| When bind-mounting a single file, nerdbox shares the **parent directory** of | ||
| the file via virtiofs, then bind-mounts the specific file into the container: | ||
|
|
||
| ``` | ||
| Host: /host/config/app.yaml | ||
| ↓ | ||
| virtiofs shares: /host/config/ (parent directory) | ||
| ↓ | ||
| VM: /mnt/bind-{hash}/app.yaml | ||
| ↓ | ||
| Container: /container/app.yaml | ||
| ``` | ||
|
|
||
| ### Security Implications | ||
|
|
||
| When using file bind mounts, be aware that the **entire parent directory** is | ||
| exposed to the VM, not just the single file. This has security implications if | ||
| the VM is considered a security boundary: | ||
|
|
||
| - All files in the parent directory become accessible to the VM | ||
| - If an attacker compromises the VM, they can access any file in that directory | ||
| - Sensitive files that happen to be siblings of the bind-mounted file are exposed | ||
|
|
||
| **Recommendations:** | ||
|
|
||
| - Avoid bind-mounting files from directories containing secrets, credentials, | ||
| or sensitive data | ||
| - If the VM is treated as a security boundary, audit what gets exposed when | ||
| using file bind mounts | ||
| - Place files intended for bind-mounting in dedicated directories with no other | ||
| sensitive content | ||
| - Consider using directory bind mounts with only the necessary files instead of | ||
| file bind mounts |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -18,13 +18,17 @@ package task | |||||
|
|
||||||
| import ( | ||||||
| "context" | ||||||
| "crypto/sha256" | ||||||
| "fmt" | ||||||
| "os" | ||||||
| "path/filepath" | ||||||
| "strings" | ||||||
|
|
||||||
| "github.com/containerd/containerd/api/types" | ||||||
| "github.com/containerd/errdefs" | ||||||
| "github.com/containerd/log" | ||||||
|
|
||||||
| "github.com/containerd/nerdbox/internal/shim/task/bundle" | ||||||
| "github.com/containerd/nerdbox/internal/vm" | ||||||
| ) | ||||||
|
|
||||||
|
|
@@ -146,3 +150,70 @@ func filterOptions(options []string) []string { | |||||
| } | ||||||
| return filtered | ||||||
| } | ||||||
|
|
||||||
| type bindMounter struct { | ||||||
| mounts []bindMount | ||||||
| } | ||||||
|
|
||||||
| type bindMount struct { | ||||||
| tag string | ||||||
| hostSrc string | ||||||
| vmTarget string | ||||||
| } | ||||||
|
|
||||||
| func (bm *bindMounter) FromBundle(ctx context.Context, b *bundle.Bundle) error { | ||||||
| for i, m := range b.Spec.Mounts { | ||||||
| if m.Type != "bind" { | ||||||
| continue | ||||||
| } | ||||||
|
|
||||||
| log.G(ctx).WithField("mount", m).Debug("transforming bind mount into a virtiofs mount") | ||||||
|
|
||||||
| fi, err := os.Stat(m.Source) | ||||||
| if err != nil { | ||||||
| return fmt.Errorf("failed to stat bind mount source %s: %w", m.Source, err) | ||||||
| } | ||||||
|
|
||||||
| hash := sha256.Sum256([]byte(m.Destination)) | ||||||
| tag := fmt.Sprintf("bind-%x", hash[:8]) | ||||||
|
||||||
| tag := fmt.Sprintf("bind-%x", hash[:8]) | |
| tag := fmt.Sprintf("bind-%x", hash) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need consistent hashing or could a random element be injected?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC, I think this would also need to take the source into account if we want to have multiple containers share the same VM in the future?
Otherwise there would be a tag collision beetween mounts with the same destination (in container) but different source (on the host)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe considering use temporary bind mount in a new empty temporary dir in the host for this? is that practical for libkrun?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The directory is created with 0700 permissions which restricts access to the owner only. Consider if this permission level is appropriate for all use cases, as containers may need broader access depending on their user configuration.