github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/filesystem/directory_posix.go (about)

     1  //go:build !windows
     2  
     3  package filesystem
     4  
     5  import (
     6  	cryptorand "crypto/rand"
     7  	"encoding/binary"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"math/rand"
    12  	"os"
    13  	"runtime"
    14  	"strconv"
    15  	"strings"
    16  	"sync"
    17  	"time"
    18  
    19  	"golang.org/x/sys/unix"
    20  
    21  	"github.com/mutagen-io/mutagen/pkg/state"
    22  )
    23  
    24  // ensureValidName verifies that the provided name does not reference the
    25  // current directory, the parent directory, or contain a path separator
    26  // character.
    27  func ensureValidName(name string) error {
    28  	// Verify that the name does not reference the directory itself or the
    29  	// parent directory.
    30  	if name == "." {
    31  		return errors.New("name is directory reference")
    32  	} else if name == ".." {
    33  		return errors.New("name is parent directory reference")
    34  	}
    35  
    36  	// Verify that the path separator character does not appear in the name.
    37  	if strings.IndexByte(name, os.PathSeparator) != -1 {
    38  		return errors.New("path separator appears in name")
    39  	}
    40  
    41  	// Success.
    42  	return nil
    43  }
    44  
    45  // Directory represents a directory on disk and provides race-free operations on
    46  // the directory's contents. All of its operations avoid the traversal of
    47  // symbolic links.
    48  type Directory struct {
    49  	// descriptor is the file descriptor for the directory, designed to be used
    50  	// in conjunction with POSIX *at functions. It is wrapped by the os.File
    51  	// object below (file) and should not be closed directly.
    52  	descriptor int
    53  	// file is an os.File object which wraps the directory descriptor. It is
    54  	// required for its Readdirnames function, since there's no other portable
    55  	// way to do this from Go.
    56  	file *os.File
    57  	// exhausted indicates that the directory's contents have been read and that
    58  	// a seek is required before reading them again.
    59  	exhausted bool
    60  	// renameatNoReplaceUnsupported is marked if
    61  	// renameatNoReplaceRetryingOnEINTR is found to be unsupported with this
    62  	// directory as a target.
    63  	renameatNoReplaceUnsupported state.Marker
    64  }
    65  
    66  // Close closes the directory.
    67  func (d *Directory) Close() error {
    68  	return d.file.Close()
    69  }
    70  
    71  // Descriptor provides access to the raw file descriptor underlying the
    72  // directory. It should not be used or retained beyond the point in time where
    73  // the Close method is called, and it should not be closed externally. Its
    74  // usefulness is to code which relies on file-descriptor-based operations. This
    75  // method does not exist on Windows systems, so it should only be used in
    76  // POSIX-specific code.
    77  func (d *Directory) Descriptor() int {
    78  	return d.descriptor
    79  }
    80  
    81  // CreateDirectory creates a new directory with the specified name inside the
    82  // directory. The directory will be created with user-only read/write/execute
    83  // permissions.
    84  func (d *Directory) CreateDirectory(name string) error {
    85  	// Verify that the name is valid.
    86  	if err := ensureValidName(name); err != nil {
    87  		return err
    88  	}
    89  
    90  	// Create the directory.
    91  	return mkdiratRetryingOnEINTR(d.descriptor, name, 0700)
    92  }
    93  
    94  // createTemporaryFilePRNGLock serializes access to createTemporaryFilePRNG.
    95  var createTemporaryFilePRNGLock sync.Mutex
    96  
    97  // createTemporaryFilePRNG provides pseudorandom numbers for filenames in
    98  // Directory.CreateTemporaryFile.
    99  var createTemporaryFilePRNG *rand.Rand
   100  
   101  func init() {
   102  	// Read random data to compute a seed for the pseudorandom number generator.
   103  	var seedBytes [8]byte
   104  	if _, err := cryptorand.Read(seedBytes[:]); err != nil {
   105  		panic("unable to read random bytes for seed")
   106  	}
   107  
   108  	// Initialize the pseudorandom number generator.
   109  	createTemporaryFilePRNG = rand.New(rand.NewSource(int64(binary.BigEndian.Uint64(seedBytes[:]))))
   110  }
   111  
   112  // CreateTemporaryFile creates a new temporary file using the specified name
   113  // pattern inside the directory. Pattern behavior follows that of os.CreateTemp.
   114  // The file will be created with user-only read/write permissions.
   115  func (d *Directory) CreateTemporaryFile(pattern string) (string, io.WriteCloser, error) {
   116  	// Verify that the name is valid. This should still be a sensible operation
   117  	// for pattern specifications.
   118  	if err := ensureValidName(pattern); err != nil {
   119  		return "", nil, err
   120  	}
   121  
   122  	// Parse the pattern into prefix and suffix components.
   123  	var prefix, suffix string
   124  	if starIndex := strings.LastIndex(pattern, "*"); starIndex != -1 {
   125  		prefix, suffix = pattern[:starIndex], pattern[starIndex+1:]
   126  	} else {
   127  		prefix = pattern
   128  	}
   129  
   130  	// Iterate until we can find a free file name.
   131  	try := 0
   132  	for {
   133  		// Compute the next potential name using a pseudorandom component.
   134  		createTemporaryFilePRNGLock.Lock()
   135  		random := createTemporaryFilePRNG.Int()
   136  		createTemporaryFilePRNGLock.Unlock()
   137  		name := prefix + strconv.Itoa(random) + suffix
   138  
   139  		// Open the file. Note that we needn't specify O_NOFOLLOW here since
   140  		// we're enforcing that the file doesn't already exist.
   141  		descriptor, err := openatRetryingOnEINTR(d.descriptor, name, unix.O_RDWR|unix.O_CREAT|unix.O_EXCL|unix.O_CLOEXEC, 0600)
   142  		if err != nil {
   143  			if os.IsExist(err) {
   144  				if try++; try < 10000 {
   145  					continue
   146  				}
   147  				return "", nil, errors.New("exhausted potential file names")
   148  			}
   149  			return "", nil, err
   150  		}
   151  
   152  		// Wrap up the descriptor in a file object.
   153  		file := os.NewFile(uintptr(descriptor), name)
   154  
   155  		// Success.
   156  		return name, file, nil
   157  	}
   158  }
   159  
   160  // CreateSymbolicLink creates a new symbolic link with the specified name and
   161  // target inside the directory. The symbolic link is created with the default
   162  // system permissions (which, generally speaking, don't apply to the symbolic
   163  // link itself).
   164  func (d *Directory) CreateSymbolicLink(name, target string) error {
   165  	// Verify that the name is valid.
   166  	if err := ensureValidName(name); err != nil {
   167  		return err
   168  	}
   169  
   170  	// Create the symbolic link.
   171  	return symlinkatRetryingOnEINTR(target, d.descriptor, name)
   172  }
   173  
   174  // SetPermissions sets the permissions on the content within the directory
   175  // specified by name. Ownership information is set first, followed by
   176  // permissions extracted from the mode using ModePermissionsMask. Ownership
   177  // setting can be skipped completely by providing a nil OwnershipSpecification
   178  // or a specification with both components unset. An OwnershipSpecification may
   179  // also include only certain components, in which case only those components
   180  // will be set. Permission setting can be skipped by providing a mode value that
   181  // yields 0 after permission bit masking.
   182  func (d *Directory) SetPermissions(name string, ownership *OwnershipSpecification, mode Mode) error {
   183  	// Verify that the name is valid.
   184  	if err := ensureValidName(name); err != nil {
   185  		return err
   186  	}
   187  
   188  	// Set ownership information, if specified.
   189  	if ownership != nil && (ownership.ownerID != -1 || ownership.groupID != -1) {
   190  		if err := fchownatRetryingOnEINTR(d.descriptor, name, ownership.ownerID, ownership.groupID, unix.AT_SYMLINK_NOFOLLOW); err != nil {
   191  			return fmt.Errorf("unable to set ownership information: %w", err)
   192  		}
   193  	}
   194  
   195  	// Set permissions, if specified.
   196  	//
   197  	// HACK: On Linux, the AT_SYMLINK_NOFOLLOW flag is not supported by fchmodat
   198  	// and will result in an ENOTSUP error, so we have to use a workaround that
   199  	// opens a file and then uses fchmod in order to avoid setting permissions
   200  	// across a symbolic link.
   201  	mode &= ModePermissionsMask
   202  	if mode != 0 {
   203  		if runtime.GOOS == "linux" {
   204  			if f, err := openatRetryingOnEINTR(d.descriptor, name, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0); err != nil {
   205  				return fmt.Errorf("unable to open file: %w", err)
   206  			} else if err = fchmodRetryingOnEINTR(f, uint32(mode)); err != nil {
   207  				closeConsideringEINTR(f)
   208  				return fmt.Errorf("unable to set permission bits on file: %w", err)
   209  			} else if err = closeConsideringEINTR(f); err != nil {
   210  				return fmt.Errorf("unable to close file: %w", err)
   211  			}
   212  		} else {
   213  			if err := fchmodatRetryingOnEINTR(d.descriptor, name, uint32(mode), unix.AT_SYMLINK_NOFOLLOW); err != nil {
   214  				return fmt.Errorf("unable to set permission bits: %w", err)
   215  			}
   216  		}
   217  	}
   218  
   219  	// Success.
   220  	return nil
   221  }
   222  
   223  // open is the underlying open implementation shared by OpenDirectory and
   224  // OpenFile. It returns the file descriptor corresponding to the target, the
   225  // target metadata if the target is a file (nil otherwise), or any error.
   226  func (d *Directory) open(name string, wantDirectory bool) (int, *Metadata, error) {
   227  	// Verify that the name is valid.
   228  	if wantDirectory && name == "." {
   229  		// As a special case, we allow directories to be re-opened on POSIX
   230  		// systems. This is safe since it doesn't allow traversal.
   231  	} else if err := ensureValidName(name); err != nil {
   232  		return -1, nil, err
   233  	}
   234  
   235  	// Open the file for reading while avoiding symbolic link traversal. If a
   236  	// directory has been requested, then enforce its type here.
   237  	flags := unix.O_RDONLY | unix.O_NOFOLLOW | unix.O_CLOEXEC | extraOpenFlags
   238  	if wantDirectory {
   239  		flags |= unix.O_DIRECTORY
   240  	}
   241  	descriptor, err := openatRetryingOnEINTR(d.descriptor, name, flags, 0)
   242  	if err != nil {
   243  		return -1, nil, err
   244  	}
   245  
   246  	// If a file has been requested, then verify that's what we've received.
   247  	// This (along with the directory enforcement above) keeps parity with the
   248  	// Windows implementation, where checking file type is required for the
   249  	// implementation to work at all. Unfortunately there's no O_FILE flag
   250  	// analagous to O_DIRECTORY that we can use with openat, so we have to check
   251  	// this using an fstat operation. There is some overhead to performing this
   252  	// check, of course, and on POSIX we could probably live without it (simply
   253  	// allowing other methods on the resulting object to fail due to an
   254  	// unexpected type), but given the typical filesystem access patterns at
   255  	// play when using this code, the overhead will be minimal since this
   256  	// information should still be in the OS's stat cache.
   257  	var metadata *Metadata
   258  	if !wantDirectory {
   259  		var rawMetadata unix.Stat_t
   260  		if err := fstatRetryingOnEINTR(descriptor, &rawMetadata); err != nil {
   261  			closeConsideringEINTR(descriptor)
   262  			return -1, nil, fmt.Errorf("unable to query file metadata: %w", err)
   263  		} else if Mode(rawMetadata.Mode)&ModeTypeMask != ModeTypeFile {
   264  			closeConsideringEINTR(descriptor)
   265  			return -1, nil, errors.New("path is not a file")
   266  		}
   267  		metadata = &Metadata{
   268  			Name:             name,
   269  			Mode:             Mode(rawMetadata.Mode),
   270  			Size:             uint64(rawMetadata.Size),
   271  			ModificationTime: time.Unix(rawMetadata.Mtim.Unix()),
   272  			DeviceID:         uint64(rawMetadata.Dev),
   273  			FileID:           uint64(rawMetadata.Ino),
   274  		}
   275  	}
   276  
   277  	// Success.
   278  	return descriptor, metadata, nil
   279  }
   280  
   281  // OpenDirectory opens the directory within the directory specified by name. On
   282  // POSIX systems, the directory itself can be re-opened (with a different
   283  // underlying file handle pointing to the same directory) by passing "." to this
   284  // function.
   285  func (d *Directory) OpenDirectory(name string) (*Directory, error) {
   286  	// Call the underlying open method.
   287  	descriptor, _, err := d.open(name, true)
   288  	if err != nil {
   289  		return nil, err
   290  	}
   291  
   292  	// Success.
   293  	return &Directory{
   294  		descriptor: descriptor,
   295  		file:       os.NewFile(uintptr(descriptor), name),
   296  	}, nil
   297  }
   298  
   299  // ReadContentNames queries the directory contents and returns their base names.
   300  // It does not return "." or ".." entries.
   301  func (d *Directory) ReadContentNames() ([]string, error) {
   302  	// If we've already performed a read on the directory's contents, then we
   303  	// need to rewind the directory before performing another read.
   304  	if d.exhausted {
   305  		if offset, err := seekConsideringEINTR(d.descriptor, 0, 0); err != nil {
   306  			return nil, fmt.Errorf("unable to reset directory read pointer: %w", err)
   307  		} else if offset != 0 {
   308  			return nil, errors.New("directory offset is non-zero after seek operation")
   309  		}
   310  		d.exhausted = false
   311  	}
   312  
   313  	// Read content names. Fortunately we can use the os.File implementation for
   314  	// this since it operates on the underlying file descriptor directly. We
   315  	// always mark the directory as exhausted, even if we fail to read it all
   316  	// the way to the end.
   317  	names, err := d.file.Readdirnames(0)
   318  	d.exhausted = true
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  
   323  	// Filter names (without allocating a new slice).
   324  	results := names[:0]
   325  	for _, name := range names {
   326  		// Watch for names that reference the directory itself or the parent
   327  		// directory. The implementation underlying os.File.Readdirnames does
   328  		// filter these out, but that's not guaranteed by its documentation, so
   329  		// it's better to do this explicitly.
   330  		if name == "." || name == ".." {
   331  			continue
   332  		}
   333  
   334  		// Store the name.
   335  		results = append(results, name)
   336  	}
   337  
   338  	// Success.
   339  	return names, nil
   340  }
   341  
   342  // ReadContentMetadata reads metadata for the content within the directory
   343  // specified by name.
   344  func (d *Directory) ReadContentMetadata(name string) (*Metadata, error) {
   345  	// Verify that the name is valid.
   346  	if err := ensureValidName(name); err != nil {
   347  		return nil, err
   348  	}
   349  
   350  	// Perform the actual query operation.
   351  	return d.readContentMetadata(name)
   352  }
   353  
   354  // readContentMetadata reads metadata for the content within the directory
   355  // specified by name, but does not perform a check for name validity.
   356  func (d *Directory) readContentMetadata(name string) (*Metadata, error) {
   357  	// Query metadata.
   358  	var metadata unix.Stat_t
   359  	if err := fstatatRetryingOnEINTR(d.descriptor, name, &metadata, unix.AT_SYMLINK_NOFOLLOW); err != nil {
   360  		return nil, err
   361  	}
   362  
   363  	// Success.
   364  	return &Metadata{
   365  		Name:             name,
   366  		Mode:             Mode(metadata.Mode),
   367  		Size:             uint64(metadata.Size),
   368  		ModificationTime: time.Unix(metadata.Mtim.Unix()),
   369  		DeviceID:         uint64(metadata.Dev),
   370  		FileID:           uint64(metadata.Ino),
   371  	}, nil
   372  }
   373  
   374  // ReadContents queries the directory contents and their associated metadata.
   375  // While the results of this function can be computed as a combination of
   376  // ReadContentNames and ReadContentMetadata, this function may be significantly
   377  // faster than a naïve combination of the two (e.g. due to the usage of
   378  // FindFirstFile/FindNextFile infrastructure on Windows). This function doesn't
   379  // return metadata for "." or ".." entries.
   380  func (d *Directory) ReadContents() ([]*Metadata, error) {
   381  	// Read content names.
   382  	names, err := d.ReadContentNames()
   383  	if err != nil {
   384  		return nil, fmt.Errorf("unable to read directory content names: %w", err)
   385  	}
   386  
   387  	// Allocate the result slice with enough capacity to accommodate all
   388  	// entries.
   389  	results := make([]*Metadata, 0, len(names))
   390  
   391  	// Loop over names and grab their individual metadata.
   392  	for _, name := range names {
   393  		// Grab metadata for this entry. We don't need to validate its name in
   394  		// this scenario since we just received it from the OS. If the file has
   395  		// disappeared between listing and the metadata query, then just pretend
   396  		// that it never existed.
   397  		if m, err := d.readContentMetadata(name); err != nil {
   398  			if os.IsNotExist(err) {
   399  				continue
   400  			}
   401  			return nil, fmt.Errorf("unable to access content metadata: %w", err)
   402  		} else {
   403  			results = append(results, m)
   404  		}
   405  	}
   406  
   407  	// Success.
   408  	return results, nil
   409  }
   410  
   411  // OpenFile opens the file within the directory specified by name.
   412  func (d *Directory) OpenFile(name string) (io.ReadSeekCloser, *Metadata, error) {
   413  	// Perform the open operation.
   414  	descriptor, metadata, err := d.open(name, false)
   415  	if err != nil {
   416  		return nil, nil, err
   417  	}
   418  
   419  	// Convert the file descriptor to a usable type.
   420  	return file(descriptor), metadata, err
   421  }
   422  
   423  // readlinkInitialBufferSize specifies the initial buffer size to use for
   424  // readlinkat operations. It should be large enough to accommodate most symbolic
   425  // links but not so large that every readlinkat operation incurs an inordinate
   426  // amount of allocation overhead. This value is pinched from the os.Readlink
   427  // implementation.
   428  const readlinkInitialBufferSize = 128
   429  
   430  // ReadSymbolicLink reads the target of the symbolic link within the directory
   431  // specified by name.
   432  func (d *Directory) ReadSymbolicLink(name string) (string, error) {
   433  	// Verify that the name is valid.
   434  	if err := ensureValidName(name); err != nil {
   435  		return "", err
   436  	}
   437  
   438  	// Loop until we encounter a condition where we successfully read the
   439  	// symbolic link and with buffer space to spare. This is the only way to
   440  	// approach the problem because readlink and its ilk don't provide any
   441  	// mechanism for determining the untruncated length of the symbolic link.
   442  	for size := readlinkInitialBufferSize; ; size *= 2 {
   443  		// Allocate a buffer.
   444  		buffer := make([]byte, size)
   445  
   446  		// Read the symbolic link target.
   447  		count, err := readlinkatRetryingOnEINTR(d.descriptor, name, buffer)
   448  
   449  		// Handle errors. If we see ERANGE on AIX systems, it's an indication
   450  		// that the buffer size is too small.
   451  		if runtime.GOOS == "aix" && err == unix.ERANGE {
   452  			continue
   453  		} else if err != nil {
   454  			return "", err
   455  		}
   456  
   457  		// Verify that the count is sane. We diverge from the os.Readlink
   458  		// implementation here (which just sets this value to 0 if negative),
   459  		// because POSIX specifically says a return value of -1 is indicative of
   460  		// an error.
   461  		if count < 0 {
   462  			return "", errors.New("unknown readlinkat failure occurred")
   463  		}
   464  
   465  		// If we've managed to read the target and have buffer space to spare,
   466  		// then we know that we have the full link.
   467  		if count < size {
   468  			return string(buffer[:count]), nil
   469  		}
   470  	}
   471  }
   472  
   473  // RemoveDirectory deletes a directory with the specified name inside the
   474  // directory. The removal target must be empty.
   475  func (d *Directory) RemoveDirectory(name string) error {
   476  	// Verify that the name is valid.
   477  	if err := ensureValidName(name); err != nil {
   478  		return err
   479  	}
   480  
   481  	// Remove the directory.
   482  	return unlinkatRetryingOnEINTR(d.descriptor, name, unix.AT_REMOVEDIR)
   483  }
   484  
   485  // RemoveFile deletes a file with the specified name inside the directory.
   486  func (d *Directory) RemoveFile(name string) error {
   487  	// Verify that the name is valid.
   488  	if err := ensureValidName(name); err != nil {
   489  		return err
   490  	}
   491  
   492  	// Remove the file.
   493  	return unlinkatRetryingOnEINTR(d.descriptor, name, 0)
   494  }
   495  
   496  // RemoveSymbolicLink deletes a symbolic link with the specified name inside the
   497  // directory.
   498  func (d *Directory) RemoveSymbolicLink(name string) error {
   499  	return d.RemoveFile(name)
   500  }
   501  
   502  // Rename performs an atomic rename operation from one filesystem location (the
   503  // source) to another (the target). Each location can be specified in one of two
   504  // ways: either by a combination of directory and (non-path) name or by path
   505  // (with corresponding nil Directory object). Different specification mechanisms
   506  // can be used for each location.
   507  //
   508  // This function does not support cross-device renames. To detect whether or not
   509  // an error is due to an attempted cross-device rename, use the
   510  // IsCrossDeviceError function.
   511  func Rename(
   512  	sourceDirectory *Directory, sourceNameOrPath string,
   513  	targetDirectory *Directory, targetNameOrPath string,
   514  	replace bool,
   515  ) error {
   516  	// If a source directory has been provided, then verify that the source name
   517  	// is valid and extract the source directory descriptor.
   518  	sourceDescriptor := unix.AT_FDCWD
   519  	if sourceDirectory != nil {
   520  		if err := ensureValidName(sourceNameOrPath); err != nil {
   521  			return fmt.Errorf("source name invalid: %w", err)
   522  		}
   523  		sourceDescriptor = sourceDirectory.descriptor
   524  	}
   525  
   526  	// If a target directory has been provided, then verify that the target name
   527  	// is valid and extract the target directory descriptor.
   528  	targetDescriptor := unix.AT_FDCWD
   529  	if targetDirectory != nil {
   530  		if err := ensureValidName(targetNameOrPath); err != nil {
   531  			return fmt.Errorf("target name invalid: %w", err)
   532  		}
   533  		targetDescriptor = targetDirectory.descriptor
   534  	}
   535  
   536  	// If we're allowing the target to be replaced, then just attempt a standard
   537  	// rename operation.
   538  	if replace {
   539  		return renameatRetryingOnEINTR(
   540  			sourceDescriptor, sourceNameOrPath,
   541  			targetDescriptor, targetNameOrPath,
   542  		)
   543  	}
   544  
   545  	// Since we're not allowing replacement, we need to ensure that the target
   546  	// doesn't exist. Some platforms provide specialized renameat variants and
   547  	// flags for this purpose, so we'll see if that's the case first. We'll skip
   548  	// this if we've already determined that the target directory's filesystem
   549  	// doesn't support this mechanism.
   550  	if targetDirectory == nil || !targetDirectory.renameatNoReplaceUnsupported.Marked() {
   551  		err := renameatNoReplaceRetryingOnEINTR(
   552  			sourceDescriptor, sourceNameOrPath,
   553  			targetDescriptor, targetNameOrPath,
   554  		)
   555  		if err == nil || (err != unix.ENOTSUP && err != unix.ENOSYS) {
   556  			return err
   557  		} else if err == unix.ENOTSUP && targetDirectory != nil {
   558  			targetDirectory.renameatNoReplaceUnsupported.Mark()
   559  		}
   560  	}
   561  
   562  	// There either isn't a non-replacing variant of renameat available or it
   563  	// isn't supported on this platform or target filesystem. In any case, we're
   564  	// falling back to the slower and less atomic method, so check if the target
   565  	// exists.
   566  	var probeErr error
   567  	if targetDirectory != nil {
   568  		_, probeErr = targetDirectory.ReadContentMetadata(targetNameOrPath)
   569  	} else {
   570  		_, probeErr = os.Lstat(targetNameOrPath)
   571  	}
   572  	if probeErr == nil {
   573  		return os.ErrExist
   574  	} else if !os.IsNotExist(probeErr) {
   575  		return fmt.Errorf("unable to probe target existence: %w", probeErr)
   576  	}
   577  
   578  	// RACE: There's a race window here between the time of our check and the
   579  	// time that the file is renamed. This is a limitation of the POSIX API.
   580  
   581  	// Attempt the rename operation.
   582  	return renameatRetryingOnEINTR(
   583  		sourceDescriptor, sourceNameOrPath,
   584  		targetDescriptor, targetNameOrPath,
   585  	)
   586  }
   587  
   588  // IsCrossDeviceError checks whether or not an error returned from rename
   589  // represents a cross-device error.
   590  func IsCrossDeviceError(err error) bool {
   591  	return err == unix.EXDEV
   592  }