Skip to content

Commit 6023bb1

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

2 files changed

Lines changed: 855 additions & 31 deletions

File tree

internal/transfer/containerfs.go

Lines changed: 100 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -69,29 +69,61 @@ func (t *containerFSTransferrer) Transfer(ctx context.Context, src, dst any, opt
6969
return errdefs.ErrNotImplemented
7070
}
7171

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

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

82-
fi, err := os.Lstat(srcPath)
107+
fi, err := root.Lstat(relPath)
83108
if err != nil {
84109
return fmt.Errorf("failed to stat %s: %w", path, err)
85110
}
86111

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

90122
if !fi.IsDir() || noWalk {
91-
return writeTarEntry(tw, srcPath, fi, filepath.Base(srcPath))
123+
return writeTarEntry(root, tw, relPath, fi, baseName)
92124
}
93125

94-
return filepath.WalkDir(srcPath, func(filePath string, d fs.DirEntry, err error) error {
126+
return fs.WalkDir(root.FS(), relPath, func(walkPath string, d fs.DirEntry, err error) error {
95127
if err != nil {
96128
return err
97129
}
@@ -100,31 +132,32 @@ func writePath(rootfs, path string, w io.Writer, mediaType string, noWalk bool)
100132
return err
101133
}
102134

103-
// Compute relative path for the tar header
104-
rel, err := filepath.Rel(srcPath, filePath)
135+
rel, err := filepath.Rel(relPath, walkPath)
105136
if err != nil {
106137
return err
107138
}
139+
var name string
108140
if rel == "." {
109-
rel = filepath.Base(srcPath)
141+
name = baseName
110142
} else {
111-
rel = filepath.Join(filepath.Base(srcPath), rel)
143+
name = filepath.Join(baseName, rel)
112144
}
113145

114-
return writeTarEntry(tw, filePath, info, rel)
146+
return writeTarEntry(root, tw, walkPath, info, name)
115147
})
116148
}
117149

118-
func writeTarEntry(tw *tar.Writer, filePath string, fi os.FileInfo, name string) error {
150+
// writeTarEntry writes a single tar entry. srcPath is interpreted
151+
// relative to root, so symlink resolution cannot escape the rootfs.
152+
func writeTarEntry(root *os.Root, tw *tar.Writer, srcPath string, fi os.FileInfo, name string) error {
119153
header, err := tar.FileInfoHeader(fi, "")
120154
if err != nil {
121155
return err
122156
}
123157
header.Name = name
124158

125-
// Resolve symlink target
126159
if fi.Mode()&os.ModeSymlink != 0 {
127-
link, err := os.Readlink(filePath)
160+
link, err := root.Readlink(srcPath)
128161
if err != nil {
129162
return err
130163
}
@@ -136,7 +169,7 @@ func writeTarEntry(tw *tar.Writer, filePath string, fi os.FileInfo, name string)
136169
}
137170

138171
if fi.Mode().IsRegular() {
139-
f, err := os.Open(filePath)
172+
f, err := root.Open(srcPath)
140173
if err != nil {
141174
return err
142175
}
@@ -149,15 +182,40 @@ func writeTarEntry(tw *tar.Writer, filePath string, fi os.FileInfo, name string)
149182
return nil
150183
}
151184

152-
// readPath reads a tar archive from r and extracts it to the given path
185+
// readPath reads a tar archive from r and extracts it under path
153186
// within rootfs. When preserveOwnership is true, extracted files have
154187
// their UID/GID set from the tar headers.
188+
//
189+
// The destination directory is opened as a sub-*os.Root so the
190+
// destination boundary is enforced by os.Root rather than by lexical
191+
// path checks. Pre-existing symlinks within the rootfs, symlinks
192+
// created by earlier entries in the same archive, absolute symlink
193+
// targets, and tar entry names containing "../" all resolve within
194+
// the destination's sub-root and cannot redirect writes outside it.
155195
func readPath(r io.Reader, rootfs, path, mediaType string, preserveOwnership bool) error {
156196
if mediaType != mediaTypeTar {
157197
return fmt.Errorf("unsupported media type %q: %w", mediaType, errdefs.ErrNotImplemented)
158198
}
159199

160-
dstPath := filepath.Join(rootfs, filepath.Clean("/"+path))
200+
root, err := os.OpenRoot(rootfs)
201+
if err != nil {
202+
return fmt.Errorf("failed to open rootfs: %w", err)
203+
}
204+
defer root.Close()
205+
206+
relDst := rootRel(path)
207+
208+
dst := root
209+
if relDst != "." {
210+
if err := root.MkdirAll(relDst, 0755); err != nil {
211+
return fmt.Errorf("failed to create destination: %w", err)
212+
}
213+
dst, err = root.OpenRoot(relDst)
214+
if err != nil {
215+
return fmt.Errorf("failed to open destination: %w", err)
216+
}
217+
defer dst.Close()
218+
}
161219

162220
tr := tar.NewReader(r)
163221
for {
@@ -169,30 +227,33 @@ func readPath(r io.Reader, rootfs, path, mediaType string, preserveOwnership boo
169227
return fmt.Errorf("failed to read tar header: %w", err)
170228
}
171229

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)
230+
// Clean the entry name relative to "/" so any "../" sequences
231+
// collapse before we hand the path to dst. dst itself enforces
232+
// the destination boundary.
233+
entryName := strings.TrimPrefix(filepath.Clean("/"+header.Name), "/")
234+
if entryName == "" {
235+
// Names that resolve to the destination itself (e.g. "."
236+
// or "/") have nothing to extract.
237+
continue
177238
}
178239

179-
if err := extractTarEntry(target, header, tr, preserveOwnership); err != nil {
240+
if err := extractTarEntry(dst, entryName, header, tr, preserveOwnership); err != nil {
180241
return err
181242
}
182243
}
183244
}
184245

185-
func extractTarEntry(target string, header *tar.Header, r io.Reader, preserveOwnership bool) error {
246+
func extractTarEntry(dst *os.Root, target string, header *tar.Header, r io.Reader, preserveOwnership bool) error {
186247
switch header.Typeflag {
187248
case tar.TypeDir:
188-
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
249+
if err := dst.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
189250
return err
190251
}
191252
case tar.TypeReg:
192-
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
253+
if err := dst.MkdirAll(filepath.Dir(target), 0755); err != nil {
193254
return err
194255
}
195-
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
256+
f, err := dst.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
196257
if err != nil {
197258
return err
198259
}
@@ -204,23 +265,31 @@ func extractTarEntry(target string, header *tar.Header, r io.Reader, preserveOwn
204265
return err
205266
}
206267
case tar.TypeSymlink:
207-
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
268+
if err := dst.MkdirAll(filepath.Dir(target), 0755); err != nil {
208269
return err
209270
}
210-
if err := os.Symlink(header.Linkname, target); err != nil {
271+
// The symlink target string is stored verbatim. When later
272+
// traversed through dst it will be resolved within the
273+
// destination sub-root, so an absolute or "../"-laden target
274+
// cannot redirect reads or writes outside it.
275+
if err := dst.Symlink(header.Linkname, target); err != nil {
211276
return err
212277
}
213278
case tar.TypeLink:
214-
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
279+
if err := dst.MkdirAll(filepath.Dir(target), 0755); err != nil {
215280
return err
216281
}
217-
if err := os.Link(header.Linkname, target); err != nil {
282+
// Hardlink source names another entry in the same archive.
283+
// Clean it the same way as the entry name; dst.Link enforces
284+
// that both ends remain inside the destination sub-root.
285+
linkSrc := strings.TrimPrefix(filepath.Clean("/"+header.Linkname), "/")
286+
if err := dst.Link(linkSrc, target); err != nil {
218287
return err
219288
}
220289
}
221290

222291
if preserveOwnership {
223-
if err := os.Lchown(target, header.Uid, header.Gid); err != nil {
292+
if err := dst.Lchown(target, header.Uid, header.Gid); err != nil {
224293
return fmt.Errorf("failed to chown %s: %w", target, err)
225294
}
226295
}

0 commit comments

Comments
 (0)