1/*
2Copyright 2014 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17// TODO(thockin): This whole pkg is pretty linux-centric.  As soon as we have
18// an alternate platform, we will need to abstract further.
19
20package mount
21
22import (
23	"fmt"
24	"os"
25	"path/filepath"
26	"strings"
27
28	utilexec "k8s.io/utils/exec"
29)
30
31const (
32	// Default mount command if mounter path is not specified.
33	defaultMountCommand = "mount"
34	// Log message where sensitive mount options were removed
35	sensitiveOptionsRemoved = "<masked>"
36)
37
38// Interface defines the set of methods to allow for mount operations on a system.
39type Interface interface {
40	// Mount mounts source to target as fstype with given options.
41	// options MUST not contain sensitive material (like passwords).
42	Mount(source string, target string, fstype string, options []string) error
43	// MountSensitive is the same as Mount() but this method allows
44	// sensitiveOptions to be passed in a separate parameter from the normal
45	// mount options and ensures the sensitiveOptions are never logged. This
46	// method should be used by callers that pass sensitive material (like
47	// passwords) as mount options.
48	MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error
49	// Unmount unmounts given target.
50	Unmount(target string) error
51	// List returns a list of all mounted filesystems.  This can be large.
52	// On some platforms, reading mounts directly from the OS is not guaranteed
53	// consistent (i.e. it could change between chunked reads). This is guaranteed
54	// to be consistent.
55	List() ([]MountPoint, error)
56	// IsLikelyNotMountPoint uses heuristics to determine if a directory
57	// is not a mountpoint.
58	// It should return ErrNotExist when the directory does not exist.
59	// IsLikelyNotMountPoint does NOT properly detect all mountpoint types
60	// most notably linux bind mounts and symbolic link. For callers that do not
61	// care about such situations, this is a faster alternative to calling List()
62	// and scanning that output.
63	IsLikelyNotMountPoint(file string) (bool, error)
64	// GetMountRefs finds all mount references to pathname, returning a slice of
65	// paths. Pathname can be a mountpoint path or a normal	directory
66	// (for bind mount). On Linux, pathname is excluded from the slice.
67	// For example, if /dev/sdc was mounted at /path/a and /path/b,
68	// GetMountRefs("/path/a") would return ["/path/b"]
69	// GetMountRefs("/path/b") would return ["/path/a"]
70	// On Windows there is no way to query all mount points; as long as pathname is
71	// a valid mount, it will be returned.
72	GetMountRefs(pathname string) ([]string, error)
73}
74
75// Compile-time check to ensure all Mounter implementations satisfy
76// the mount interface.
77var _ Interface = &Mounter{}
78
79// MountPoint represents a single line in /proc/mounts or /etc/fstab.
80type MountPoint struct {
81	Device string
82	Path   string
83	Type   string
84	Opts   []string // Opts may contain sensitive mount options (like passwords) and MUST be treated as such (e.g. not logged).
85	Freq   int
86	Pass   int
87}
88
89type MountErrorType string
90
91const (
92	FilesystemMismatch  MountErrorType = "FilesystemMismatch"
93	HasFilesystemErrors MountErrorType = "HasFilesystemErrors"
94	UnformattedReadOnly MountErrorType = "UnformattedReadOnly"
95	FormatFailed        MountErrorType = "FormatFailed"
96	GetDiskFormatFailed MountErrorType = "GetDiskFormatFailed"
97	UnknownMountError   MountErrorType = "UnknownMountError"
98)
99
100type MountError struct {
101	Type    MountErrorType
102	Message string
103}
104
105func (mountError MountError) String() string {
106	return mountError.Message
107}
108
109func (mountError MountError) Error() string {
110	return mountError.Message
111}
112
113func NewMountError(mountErrorValue MountErrorType, format string, args ...interface{}) error {
114	mountError := MountError{
115		Type:    mountErrorValue,
116		Message: fmt.Sprintf(format, args...),
117	}
118	return mountError
119}
120
121// SafeFormatAndMount probes a device to see if it is formatted.
122// Namely it checks to see if a file system is present. If so it
123// mounts it otherwise the device is formatted first then mounted.
124type SafeFormatAndMount struct {
125	Interface
126	Exec utilexec.Interface
127}
128
129// FormatAndMount formats the given disk, if needed, and mounts it.
130// That is if the disk is not formatted and it is not being mounted as
131// read-only it will format it first then mount it. Otherwise, if the
132// disk is already formatted or it is being mounted as read-only, it
133// will be mounted without formatting.
134// options MUST not contain sensitive material (like passwords).
135func (mounter *SafeFormatAndMount) FormatAndMount(source string, target string, fstype string, options []string) error {
136	return mounter.FormatAndMountSensitive(source, target, fstype, options, nil /* sensitiveOptions */)
137}
138
139// FormatAndMountSensitive is the same as FormatAndMount but this method allows
140// sensitiveOptions to be passed in a separate parameter from the normal mount
141// options and ensures the sensitiveOptions are never logged. This method should
142// be used by callers that pass sensitive material (like passwords) as mount
143// options.
144func (mounter *SafeFormatAndMount) FormatAndMountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error {
145	return mounter.formatAndMountSensitive(source, target, fstype, options, sensitiveOptions)
146}
147
148// getMountRefsByDev finds all references to the device provided
149// by mountPath; returns a list of paths.
150// Note that mountPath should be path after the evaluation of any symblolic links.
151func getMountRefsByDev(mounter Interface, mountPath string) ([]string, error) {
152	mps, err := mounter.List()
153	if err != nil {
154		return nil, err
155	}
156
157	// Finding the device mounted to mountPath.
158	diskDev := ""
159	for i := range mps {
160		if mountPath == mps[i].Path {
161			diskDev = mps[i].Device
162			break
163		}
164	}
165
166	// Find all references to the device.
167	var refs []string
168	for i := range mps {
169		if mps[i].Device == diskDev || mps[i].Device == mountPath {
170			if mps[i].Path != mountPath {
171				refs = append(refs, mps[i].Path)
172			}
173		}
174	}
175	return refs, nil
176}
177
178// GetDeviceNameFromMount given a mnt point, find the device from /proc/mounts
179// returns the device name, reference count, and error code.
180func GetDeviceNameFromMount(mounter Interface, mountPath string) (string, int, error) {
181	mps, err := mounter.List()
182	if err != nil {
183		return "", 0, err
184	}
185
186	// Find the device name.
187	// FIXME if multiple devices mounted on the same mount path, only the first one is returned.
188	device := ""
189	// If mountPath is symlink, need get its target path.
190	slTarget, err := filepath.EvalSymlinks(mountPath)
191	if err != nil {
192		slTarget = mountPath
193	}
194	for i := range mps {
195		if mps[i].Path == slTarget {
196			device = mps[i].Device
197			break
198		}
199	}
200
201	// Find all references to the device.
202	refCount := 0
203	for i := range mps {
204		if mps[i].Device == device {
205			refCount++
206		}
207	}
208	return device, refCount, nil
209}
210
211// IsNotMountPoint determines if a directory is a mountpoint.
212// It should return ErrNotExist when the directory does not exist.
213// IsNotMountPoint is more expensive than IsLikelyNotMountPoint.
214// IsNotMountPoint detects bind mounts in linux.
215// IsNotMountPoint enumerates all the mountpoints using List() and
216// the list of mountpoints may be large, then it uses
217// isMountPointMatch to evaluate whether the directory is a mountpoint.
218func IsNotMountPoint(mounter Interface, file string) (bool, error) {
219	// IsLikelyNotMountPoint provides a quick check
220	// to determine whether file IS A mountpoint.
221	notMnt, notMntErr := mounter.IsLikelyNotMountPoint(file)
222	if notMntErr != nil && os.IsPermission(notMntErr) {
223		// We were not allowed to do the simple stat() check, e.g. on NFS with
224		// root_squash. Fall back to /proc/mounts check below.
225		notMnt = true
226		notMntErr = nil
227	}
228	if notMntErr != nil {
229		return notMnt, notMntErr
230	}
231	// identified as mountpoint, so return this fact.
232	if notMnt == false {
233		return notMnt, nil
234	}
235
236	// Resolve any symlinks in file, kernel would do the same and use the resolved path in /proc/mounts.
237	resolvedFile, err := filepath.EvalSymlinks(file)
238	if err != nil {
239		return true, err
240	}
241
242	// check all mountpoints since IsLikelyNotMountPoint
243	// is not reliable for some mountpoint types.
244	mountPoints, mountPointsErr := mounter.List()
245	if mountPointsErr != nil {
246		return notMnt, mountPointsErr
247	}
248	for _, mp := range mountPoints {
249		if isMountPointMatch(mp, resolvedFile) {
250			notMnt = false
251			break
252		}
253	}
254	return notMnt, nil
255}
256
257// MakeBindOpts detects whether a bind mount is being requested and makes the remount options to
258// use in case of bind mount, due to the fact that bind mount doesn't respect mount options.
259// The list equals:
260//   options - 'bind' + 'remount' (no duplicate)
261func MakeBindOpts(options []string) (bool, []string, []string) {
262	bind, bindOpts, bindRemountOpts, _ := MakeBindOptsSensitive(options, nil /* sensitiveOptions */)
263	return bind, bindOpts, bindRemountOpts
264}
265
266// MakeBindOptsSensitive is the same as MakeBindOpts but this method allows
267// sensitiveOptions to be passed in a separate parameter from the normal mount
268// options and ensures the sensitiveOptions are never logged. This method should
269// be used by callers that pass sensitive material (like passwords) as mount
270// options.
271func MakeBindOptsSensitive(options []string, sensitiveOptions []string) (bool, []string, []string, []string) {
272	// Because we have an FD opened on the subpath bind mount, the "bind" option
273	// needs to be included, otherwise the mount target will error as busy if you
274	// remount as readonly.
275	//
276	// As a consequence, all read only bind mounts will no longer change the underlying
277	// volume mount to be read only.
278	bindRemountOpts := []string{"bind", "remount"}
279	bindRemountSensitiveOpts := []string{}
280	bind := false
281	bindOpts := []string{"bind"}
282
283	// _netdev is a userspace mount option and does not automatically get added when
284	// bind mount is created and hence we must carry it over.
285	if checkForNetDev(options, sensitiveOptions) {
286		bindOpts = append(bindOpts, "_netdev")
287	}
288
289	for _, option := range options {
290		switch option {
291		case "bind":
292			bind = true
293			break
294		case "remount":
295			break
296		default:
297			bindRemountOpts = append(bindRemountOpts, option)
298		}
299	}
300
301	for _, sensitiveOption := range sensitiveOptions {
302		switch sensitiveOption {
303		case "bind":
304			bind = true
305			break
306		case "remount":
307			break
308		default:
309			bindRemountSensitiveOpts = append(bindRemountSensitiveOpts, sensitiveOption)
310		}
311	}
312
313	return bind, bindOpts, bindRemountOpts, bindRemountSensitiveOpts
314}
315
316func checkForNetDev(options []string, sensitiveOptions []string) bool {
317	for _, option := range options {
318		if option == "_netdev" {
319			return true
320		}
321	}
322	for _, sensitiveOption := range sensitiveOptions {
323		if sensitiveOption == "_netdev" {
324			return true
325		}
326	}
327	return false
328}
329
330// PathWithinBase checks if give path is within given base directory.
331func PathWithinBase(fullPath, basePath string) bool {
332	rel, err := filepath.Rel(basePath, fullPath)
333	if err != nil {
334		return false
335	}
336	if StartsWithBackstep(rel) {
337		// Needed to escape the base path.
338		return false
339	}
340	return true
341}
342
343// StartsWithBackstep checks if the given path starts with a backstep segment.
344func StartsWithBackstep(rel string) bool {
345	// normalize to / and check for ../
346	return rel == ".." || strings.HasPrefix(filepath.ToSlash(rel), "../")
347}
348
349// sanitizedOptionsForLogging will return a comma seperated string containing
350// options and sensitiveOptions. Each entry in sensitiveOptions will be
351// replaced with the string sensitiveOptionsRemoved
352// e.g. o1,o2,<masked>,<masked>
353func sanitizedOptionsForLogging(options []string, sensitiveOptions []string) string {
354	seperator := ""
355	if len(options) > 0 && len(sensitiveOptions) > 0 {
356		seperator = ","
357	}
358
359	sensitiveOptionsStart := ""
360	sensitiveOptionsEnd := ""
361	if len(sensitiveOptions) > 0 {
362		sensitiveOptionsStart = strings.Repeat(sensitiveOptionsRemoved+",", len(sensitiveOptions)-1)
363		sensitiveOptionsEnd = sensitiveOptionsRemoved
364	}
365
366	return strings.Join(options, ",") +
367		seperator +
368		sensitiveOptionsStart +
369		sensitiveOptionsEnd
370}
371