summaryrefslogtreecommitdiff
path: root/pkg/copy/copy.go
blob: 13893deb2abb620de3c67caf662a54a643ca0059 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
package copy

import (
	"io"
	"os"
	"path/filepath"
	"strings"

	buildahCopiah "github.com/containers/buildah/copier"
	"github.com/containers/storage/pkg/archive"
	securejoin "github.com/cyphar/filepath-securejoin"
	"github.com/pkg/errors"
)

// ********************************* NOTE *************************************
//
// Most security bugs are caused by attackers playing around with symlinks
// trying to escape from the container onto the host and/or trick into data
// corruption on the host.  Hence, file operations on containers (including
// *stat) should always be handled by `github.com/containers/buildah/copier`
// which makes sure to evaluate files in a chroot'ed environment.
//
// Please make sure to add verbose comments when changing code to make the
// lives of future readers easier.
//
// ****************************************************************************

// Copier copies data from a source to a destination CopyItem.
type Copier struct {
	copyFunc     func() error
	cleanUpFuncs []deferFunc
}

// cleanUp releases resources the Copier may hold open.
func (c *Copier) cleanUp() {
	for _, f := range c.cleanUpFuncs {
		f()
	}
}

// Copy data from a source to a destination CopyItem.
func (c *Copier) Copy() error {
	defer c.cleanUp()
	return c.copyFunc()
}

// GetCopiers returns a Copier to copy the source item to destination.  Use
// extract to untar the source if it's a tar archive.
func GetCopier(source *CopyItem, destination *CopyItem, extract bool) (*Copier, error) {
	copier := &Copier{}

	// First, do the man-page dance.  See podman-cp(1) for details.
	if err := enforceCopyRules(source, destination); err != nil {
		return nil, err
	}

	// Destination is a stream (e.g., stdout or an http body).
	if destination.info.IsStream {
		// Source is a stream (e.g., stdin or an http body).
		if source.info.IsStream {
			copier.copyFunc = func() error {
				_, err := io.Copy(destination.writer, source.reader)
				return err
			}
			return copier, nil
		}
		root, glob, err := source.buildahGlobs()
		if err != nil {
			return nil, err
		}
		copier.copyFunc = func() error {
			return buildahCopiah.Get(root, "", source.getOptions(), []string{glob}, destination.writer)
		}
		return copier, nil
	}

	// Destination is either a file or a directory.
	if source.info.IsStream {
		copier.copyFunc = func() error {
			return buildahCopiah.Put(destination.root, destination.resolved, source.putOptions(), source.reader)
		}
		return copier, nil
	}

	tarOptions := &archive.TarOptions{
		Compression: archive.Uncompressed,
		CopyPass:    true,
	}

	root := destination.root
	dir := destination.resolved
	if !source.info.IsDir {
		// When copying a file, make sure to rename the
		// destination base path.
		nameMap := make(map[string]string)
		nameMap[filepath.Base(source.resolved)] = filepath.Base(destination.resolved)
		tarOptions.RebaseNames = nameMap
		dir = filepath.Dir(dir)
	}

	var tarReader io.ReadCloser
	if extract && archive.IsArchivePath(source.resolved) {
		if !destination.info.IsDir {
			return nil, errors.Errorf("cannot extract archive %q to file %q", source.original, destination.original)
		}

		reader, err := os.Open(source.resolved)
		if err != nil {
			return nil, err
		}
		copier.cleanUpFuncs = append(copier.cleanUpFuncs, func() { reader.Close() })

		// The stream from stdin may be compressed (e.g., via gzip).
		decompressedStream, err := archive.DecompressStream(reader)
		if err != nil {
			return nil, err
		}

		copier.cleanUpFuncs = append(copier.cleanUpFuncs, func() { decompressedStream.Close() })
		tarReader = decompressedStream
	} else {
		reader, err := archive.TarWithOptions(source.resolved, tarOptions)
		if err != nil {
			return nil, err
		}
		copier.cleanUpFuncs = append(copier.cleanUpFuncs, func() { reader.Close() })
		tarReader = reader
	}

	copier.copyFunc = func() error {
		return buildahCopiah.Put(root, dir, source.putOptions(), tarReader)
	}
	return copier, nil
}

// enforceCopyRules enforces the rules for copying from a source to a
// destination as mentioned in the podman-cp(1) man page.  Please refer to the
// man page and/or the inline comments for further details.  Note that source
// and destination are passed by reference and the their data may be changed.
func enforceCopyRules(source, destination *CopyItem) error {
	if source.statError != nil {
		return source.statError
	}

	// We can copy everything to a stream.
	if destination.info.IsStream {
		return nil
	}

	if source.info.IsStream {
		if !(destination.info.IsDir || destination.info.IsStream) {
			return errors.New("destination must be a directory or stream when copying from a stream")
		}
		return nil
	}

	// Source is a *directory*.
	if source.info.IsDir {
		if destination.statError != nil {
			// It's okay if the destination does not exist.  We
			// made sure before that it's parent exists, so it
			// would be created while copying.
			if os.IsNotExist(destination.statError) {
				return nil
			}
			// Could be a permission error.
			return destination.statError
		}

		// If the destination exists and is not a directory, we have a
		// problem.
		if !destination.info.IsDir {
			return errors.Errorf("cannot copy directory %q to file %q", source.original, destination.original)
		}

		// If the destination exists and is a directory, we need to
		// append the source base directory to it.  This makes sure
		// that copying "/foo/bar" "/tmp" will copy to "/tmp/bar" (and
		// not "/tmp").
		newDestination, err := securejoin.SecureJoin(destination.resolved, filepath.Base(source.resolved))
		if err != nil {
			return err
		}
		destination.resolved = newDestination
		return nil
	}

	// Source is a *file*.
	if destination.statError != nil {
		// It's okay if the destination does not exist, unless it ends
		// with "/".
		if !os.IsNotExist(destination.statError) {
			return destination.statError
		} else if strings.HasSuffix(destination.resolved, "/") {
			// Note: this is practically unreachable code as the
			// existence of parent directories is enforced early
			// on.  It's left here as an extra security net.
			return errors.Errorf("destination directory %q must exist (trailing %q)", destination.original, "/")
		}
		// Does not exist and does not end with "/".
		return nil
	}

	// If the destination is a file, we're good.  We will overwrite the
	// contents while copying.
	if !destination.info.IsDir {
		return nil
	}

	// If the destination exists and is a directory, we need to append the
	// source base directory to it.  This makes sure that copying
	// "/foo/bar" "/tmp" will copy to "/tmp/bar" (and not "/tmp").
	newDestination, err := securejoin.SecureJoin(destination.resolved, filepath.Base(source.resolved))
	if err != nil {
		return err
	}

	destination.resolved = newDestination
	return nil
}