1// SPDX-License-Identifier: MIT
2
3package vm
4
5import (
6 "errors"
7 "fmt"
8 "io"
9 "os"
10 "path/filepath"
11 "syscall"
12)
13
14// layout is where the files of one VM live and how Firecracker names them.
15//
16// With the jailer, Firecracker sees only its chroot: <jailBase>/firecracker/<id>/root,
17// so shared files are hard-linked into it and named "/name"; per-job files are
18// created there and handed to the jailed uid. Without the jailer (development
19// and tests), Firecracker sees the real filesystem: shared files are used in
20// place and per-job files live in <stateDir>/vm/<id>.
21type layout struct {
22 jailed bool
23 root string // host path of the VM's working directory
24 dir string // the directory removed whole at cleanup
25 uid int
26}
27
28// newLayout: execName is the base name of the Firecracker binary, which the
29// jailer uses as the first directory below its chroot base.
30func newLayout(stateDir, execName, id string, jailed bool, uid int) layout {
31 if jailed {
32 dir := filepath.Join(stateDir, "jail", execName, id)
33 return layout{jailed: true, root: filepath.Join(dir, "root"), dir: dir, uid: uid}
34 }
35 dir := filepath.Join(stateDir, "vm", id)
36 return layout{root: dir, dir: dir, uid: uid}
37}
38
39// jailBase is the --chroot-base-dir given to the jailer.
40func jailBase(stateDir string) string { return filepath.Join(stateDir, "jail") }
41
42// host returns the host path of a per-job file called name.
43func (l layout) host(name string) string { return filepath.Join(l.root, name) }
44
45// fc returns the path Firecracker must be given for a per-job file.
46func (l layout) fc(name string) string {
47 if l.jailed {
48 return "/" + name
49 }
50 return l.host(name)
51}
52
53// share makes the read-only shared file src available to Firecracker and
54// returns its path for Firecracker. In a jail it is hard-linked, which requires
55// the same filesystem as the state directory, and src must be readable by the
56// jailed uid (the link shares the inode: it is never chowned).
57func (l layout) share(src, name string) (string, error) {
58 if !l.jailed {
59 return src, nil
60 }
61 dst := l.host(name)
62 if err := os.Link(src, dst); err != nil {
63 if errors.Is(err, syscall.EXDEV) {
64 return "", fmt.Errorf("cannot link %s into the jail: it must be on the same filesystem as the state directory (hard link across filesystems)", src)
65 }
66 return "", fmt.Errorf("link %s into the jail: %w", src, err)
67 }
68 return "/" + name, nil
69}
70
71// own hands a per-job file in the jail to the jailed uid; no-op without jailer.
72// It never follows a symlink (Lchown): the jail is writable by the jailed uid
73// once Firecracker runs, so a link planted there must not redirect the chown.
74func (l layout) own(name string) error {
75 if !l.jailed {
76 return nil
77 }
78 return os.Lchown(l.host(name), l.uid, l.uid)
79}
80
81// checkReadable verifies that other users can read the shared file, which the
82// jailed uid needs. Called once per file at driver start for jailed mode.
83func checkWorldReadable(path string) error {
84 st, err := os.Stat(path)
85 if err != nil {
86 return err
87 }
88 if st.Mode().Perm()&0o004 == 0 {
89 return fmt.Errorf("%s is not world-readable (mode %v): the jailed uid could not open it", path, st.Mode().Perm())
90 }
91 return nil
92}
93
94// copyFile copies src to dst (mode 0600) and fsyncs it.
95func copyFile(src, dst string) error {
96 in, err := os.Open(src)
97 if err != nil {
98 return err
99 }
100 defer func() { _ = in.Close() }()
101 out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
102 if err != nil {
103 return err
104 }
105 if _, err := io.Copy(out, in); err != nil {
106 _ = out.Close()
107 return err
108 }
109 if err := out.Sync(); err != nil {
110 _ = out.Close()
111 return err
112 }
113 return out.Close()
114}