Skip to content

Commit ec3dad9

Browse files
committed
Use os.Root for copy operations in vminitd
Signed-off-by: Derek McGowan <derek@mcg.dev>
1 parent 7741c92 commit ec3dad9

2 files changed

Lines changed: 892 additions & 40 deletions

File tree

internal/transfer/containerfs.go

Lines changed: 117 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"io"
2424
"io/fs"
2525
"os"
26+
"path"
2627
"path/filepath"
2728
"strings"
2829

@@ -69,29 +70,64 @@ func (t *containerFSTransferrer) Transfer(ctx context.Context, src, dst any, opt
6970
return errdefs.ErrNotImplemented
7071
}
7172

73+
// rootRel converts a path expressed in the container's view (which
74+
// may be absolute or contain parent-directory components) into a path
75+
// usable with *os.Root operations. Leading "/" is stripped after
76+
// cleaning, and any leading "../" sequences are collapsed by Clean,
77+
// guaranteeing the result resolves within the root. An empty result
78+
// is mapped to ".", which os.Root treats as the root itself.
79+
func rootRel(p string) string {
80+
p = strings.TrimPrefix(path.Clean("/"+p), "/")
81+
if p == "" {
82+
return "."
83+
}
84+
return p
85+
}
86+
7287
// writePath creates a tar archive from the given path within rootfs
7388
// and writes it to w. When noWalk is true and path is a directory,
74-
// only the directory entry itself is included without walking into it.
75-
func writePath(rootfs, path string, w io.Writer, mediaType string, noWalk bool) error {
89+
// only the directory entry itself is included without walking into
90+
// it.
91+
//
92+
// All filesystem accesses are anchored to rootfs through *os.Root,
93+
// so symlink resolution cannot escape the rootfs even if the
94+
// container concurrently mutates its own filesystem.
95+
func writePath(rootfs, src string, w io.Writer, mediaType string, noWalk bool) error {
7696
if mediaType != mediaTypeTar {
7797
return fmt.Errorf("unsupported media type %q: %w", mediaType, errdefs.ErrNotImplemented)
7898
}
7999

80-
srcPath := filepath.Join(rootfs, filepath.Clean("/"+path))
100+
root, err := os.OpenRoot(rootfs)
101+
if err != nil {
102+
return fmt.Errorf("failed to open rootfs: %w", err)
103+
}
104+
defer root.Close()
105+
106+
relPath := rootRel(src)
81107

82-
fi, err := os.Lstat(srcPath)
108+
fi, err := root.Lstat(relPath)
83109
if err != nil {
84-
return fmt.Errorf("failed to stat %s: %w", path, err)
110+
return fmt.Errorf("failed to stat %s: %w", src, err)
85111
}
86112

113+
// The top-level entry name is the basename of the requested
114+
// path. When the caller asks for the whole filesystem (path "/"),
115+
// relPath is "." and baseName is "."; child entries then drop
116+
// the leading "./" via path.Join, so the tar contains
117+
// "bin/sh" rather than leaking the host bundle's directory name.
118+
baseName := path.Base(relPath)
119+
87120
tw := tar.NewWriter(w)
88-
defer tw.Close()
89121

90122
if !fi.IsDir() || noWalk {
91-
return writeTarEntry(tw, srcPath, fi, filepath.Base(srcPath))
123+
if err := writeTarEntry(root, tw, relPath, fi, baseName); err != nil {
124+
tw.Close()
125+
return err
126+
}
127+
return tw.Close()
92128
}
93129

94-
return filepath.WalkDir(srcPath, func(filePath string, d fs.DirEntry, err error) error {
130+
if err := fs.WalkDir(root.FS(), relPath, func(walkPath string, d fs.DirEntry, err error) error {
95131
if err != nil {
96132
return err
97133
}
@@ -100,31 +136,36 @@ func writePath(rootfs, path string, w io.Writer, mediaType string, noWalk bool)
100136
return err
101137
}
102138

103-
// Compute relative path for the tar header
104-
rel, err := filepath.Rel(srcPath, filePath)
105-
if err != nil {
106-
return err
107-
}
108-
if rel == "." {
109-
rel = filepath.Base(srcPath)
139+
// walkPath is always slash-separated (fs.FS contract) and
140+
// rooted at relPath. Strip the prefix to get the entry's
141+
// path within the walk.
142+
rel := strings.TrimPrefix(strings.TrimPrefix(walkPath, relPath), "/")
143+
var name string
144+
if rel == "" {
145+
name = baseName
110146
} else {
111-
rel = filepath.Join(filepath.Base(srcPath), rel)
147+
name = path.Join(baseName, rel)
112148
}
113149

114-
return writeTarEntry(tw, filePath, info, rel)
115-
})
150+
return writeTarEntry(root, tw, walkPath, info, name)
151+
}); err != nil {
152+
tw.Close()
153+
return err
154+
}
155+
return tw.Close()
116156
}
117157

118-
func writeTarEntry(tw *tar.Writer, filePath string, fi os.FileInfo, name string) error {
158+
// writeTarEntry writes a single tar entry. srcPath is interpreted
159+
// relative to root, so symlink resolution cannot escape the rootfs.
160+
func writeTarEntry(root *os.Root, tw *tar.Writer, srcPath string, fi os.FileInfo, name string) error {
119161
header, err := tar.FileInfoHeader(fi, "")
120162
if err != nil {
121163
return err
122164
}
123165
header.Name = name
124166

125-
// Resolve symlink target
126167
if fi.Mode()&os.ModeSymlink != 0 {
127-
link, err := os.Readlink(filePath)
168+
link, err := root.Readlink(srcPath)
128169
if err != nil {
129170
return err
130171
}
@@ -136,7 +177,7 @@ func writeTarEntry(tw *tar.Writer, filePath string, fi os.FileInfo, name string)
136177
}
137178

138179
if fi.Mode().IsRegular() {
139-
f, err := os.Open(filePath)
180+
f, err := root.Open(srcPath)
140181
if err != nil {
141182
return err
142183
}
@@ -149,15 +190,40 @@ func writeTarEntry(tw *tar.Writer, filePath string, fi os.FileInfo, name string)
149190
return nil
150191
}
151192

152-
// readPath reads a tar archive from r and extracts it to the given path
193+
// readPath reads a tar archive from r and extracts it under path
153194
// within rootfs. When preserveOwnership is true, extracted files have
154195
// their UID/GID set from the tar headers.
155-
func readPath(r io.Reader, rootfs, path, mediaType string, preserveOwnership bool) error {
196+
//
197+
// The destination directory is opened as a sub-*os.Root so the
198+
// destination boundary is enforced by os.Root rather than by lexical
199+
// path checks. Pre-existing symlinks within the rootfs, symlinks
200+
// created by earlier entries in the same archive, absolute symlink
201+
// targets, and tar entry names containing "../" all resolve within
202+
// the destination's sub-root and cannot redirect writes outside it.
203+
func readPath(r io.Reader, rootfs, dstPath, mediaType string, preserveOwnership bool) error {
156204
if mediaType != mediaTypeTar {
157205
return fmt.Errorf("unsupported media type %q: %w", mediaType, errdefs.ErrNotImplemented)
158206
}
159207

160-
dstPath := filepath.Join(rootfs, filepath.Clean("/"+path))
208+
root, err := os.OpenRoot(rootfs)
209+
if err != nil {
210+
return fmt.Errorf("failed to open rootfs: %w", err)
211+
}
212+
defer root.Close()
213+
214+
relDst := rootRel(dstPath)
215+
216+
dst := root
217+
if relDst != "." {
218+
if err := root.MkdirAll(relDst, 0755); err != nil {
219+
return fmt.Errorf("failed to create destination: %w", err)
220+
}
221+
dst, err = root.OpenRoot(relDst)
222+
if err != nil {
223+
return fmt.Errorf("failed to open destination: %w", err)
224+
}
225+
defer dst.Close()
226+
}
161227

162228
tr := tar.NewReader(r)
163229
for {
@@ -169,30 +235,33 @@ func readPath(r io.Reader, rootfs, path, mediaType string, preserveOwnership boo
169235
return fmt.Errorf("failed to read tar header: %w", err)
170236
}
171237

172-
target := filepath.Join(dstPath, filepath.Clean("/"+header.Name))
173-
174-
// Ensure the target is within the destination directory
175-
if !strings.HasPrefix(target, filepath.Clean(dstPath)+string(os.PathSeparator)) && target != filepath.Clean(dstPath) {
176-
return fmt.Errorf("tar entry %q would escape destination", header.Name)
238+
// Clean the entry name relative to "/" so any "../" sequences
239+
// collapse before we hand the path to dst. dst itself enforces
240+
// the destination boundary.
241+
entryName := strings.TrimPrefix(path.Clean("/"+header.Name), "/")
242+
if entryName == "" {
243+
// Names that resolve to the destination itself (e.g. "."
244+
// or "/") have nothing to extract.
245+
continue
177246
}
178247

179-
if err := extractTarEntry(target, header, tr, preserveOwnership); err != nil {
248+
if err := extractTarEntry(dst, entryName, header, tr, preserveOwnership); err != nil {
180249
return err
181250
}
182251
}
183252
}
184253

185-
func extractTarEntry(target string, header *tar.Header, r io.Reader, preserveOwnership bool) error {
254+
func extractTarEntry(dst *os.Root, target string, header *tar.Header, r io.Reader, preserveOwnership bool) error {
186255
switch header.Typeflag {
187256
case tar.TypeDir:
188-
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
257+
if err := dst.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
189258
return err
190259
}
191260
case tar.TypeReg:
192-
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
261+
if err := dst.MkdirAll(path.Dir(target), 0755); err != nil {
193262
return err
194263
}
195-
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
264+
f, err := dst.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
196265
if err != nil {
197266
return err
198267
}
@@ -204,23 +273,31 @@ func extractTarEntry(target string, header *tar.Header, r io.Reader, preserveOwn
204273
return err
205274
}
206275
case tar.TypeSymlink:
207-
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
276+
if err := dst.MkdirAll(path.Dir(target), 0755); err != nil {
208277
return err
209278
}
210-
if err := os.Symlink(header.Linkname, target); err != nil {
279+
// The symlink target string is stored verbatim. When later
280+
// traversed through dst it will be resolved within the
281+
// destination sub-root, so an absolute or "../"-laden target
282+
// cannot redirect reads or writes outside it.
283+
if err := dst.Symlink(header.Linkname, target); err != nil {
211284
return err
212285
}
213286
case tar.TypeLink:
214-
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
287+
if err := dst.MkdirAll(path.Dir(target), 0755); err != nil {
215288
return err
216289
}
217-
if err := os.Link(header.Linkname, target); err != nil {
290+
// Hardlink source names another entry in the same archive.
291+
// Clean it the same way as the entry name; dst.Link enforces
292+
// that both ends remain inside the destination sub-root.
293+
linkSrc := strings.TrimPrefix(path.Clean("/"+header.Linkname), "/")
294+
if err := dst.Link(linkSrc, target); err != nil {
218295
return err
219296
}
220297
}
221298

222299
if preserveOwnership {
223-
if err := os.Lchown(target, header.Uid, header.Gid); err != nil {
300+
if err := dst.Lchown(target, header.Uid, header.Gid); err != nil {
224301
return fmt.Errorf("failed to chown %s: %w", target, err)
225302
}
226303
}

0 commit comments

Comments
 (0)