]> ruderich.org/simon Gitweb - safcm/safcm.git/blob - remote/sync/files.go
Use SPDX license identifiers
[safcm/safcm.git] / remote / sync / files.go
1 // MsgSyncReq: copy files to the remote host
2
3 // SPDX-License-Identifier: GPL-3.0-or-later
4 // Copyright (C) 2021-2024  Simon Ruderich
5
6 //go:build !windows
7 // +build !windows
8
9 package sync
10
11 import (
12         "bytes"
13         "fmt"
14         "io"
15         "io/fs"
16         "math/rand"
17         "net/http"
18         "os"
19         "os/user"
20         slashpath "path"
21         "path/filepath"
22         "sort"
23         "strconv"
24         "strings"
25         "time"
26
27         "github.com/ianbruene/go-difflib/difflib"
28         "golang.org/x/sys/unix"
29
30         "ruderich.org/simon/safcm"
31 )
32
33 // NOTE: Don't use plain os.Open, os.OpenFile, os.Remove or any other function
34 // which uses absolute paths. These are vulnerable to symlink attacks. Always
35 // use *at syscalls. See below for details.
36
37 // openReadonlyFlags are flags for open* syscalls to safely read a file or
38 // directory.
39 //
40 // O_NOFOLLOW prevents symlink attacks in the last path component
41 // O_NONBLOCK is necessary to prevent blocking on FIFOs
42 const openReadonlyFlags = unix.O_RDONLY | unix.O_NOFOLLOW | unix.O_NONBLOCK
43
44 func (s *Sync) syncFiles() error {
45         // To create random file names for symlinks
46         rand.Seed(time.Now().UnixNano())
47
48         // Sort for deterministic order and so parent directories are present
49         // when files in them are created
50         var files []*safcm.File
51         for _, x := range s.req.Files {
52                 files = append(files, x)
53         }
54         sort.Slice(files, func(i, j int) bool {
55                 return files[i].Path < files[j].Path
56         })
57
58         for _, x := range files {
59                 var changed bool
60                 err := s.syncFile(x, &changed)
61                 if err != nil {
62                         return fmt.Errorf("%q: %v", x.Path, err)
63                 }
64                 if changed {
65                         s.queueTriggers(x)
66                 }
67         }
68
69         return nil
70 }
71
72 func (s *Sync) syncFile(file *safcm.File, changed *bool) error {
73         // The general strategy is "update by rename": If any property of a
74         // file changes the new version will be written to a temporary file
75         // and then renamed "over" the original file. This is simple and
76         // prevents race conditions where the file is partially readable while
77         // changes to permissions or owner/group are applied. However, this
78         // strategy does not work for directories which must be removed first
79         // (was directory), must remove the existing file (will be directory)
80         // or must be directly modified (changed permissions or owner). In the
81         // first two cases the old path is removed. In the last the directory
82         // is modified (carefully) in place.
83         //
84         // The implementation is careful not to follow any symlinks to prevent
85         // possible race conditions which can be exploited and are especially
86         // dangerous when running with elevated privileges (which will most
87         // likely be the case). This includes not using absolute paths in
88         // syscalls to prevent symlink attacks when a directory is writable by
89         // other users (e.g. when syncing a file to /home/user/dir/file the
90         // user could create dir as symlink to another directory and file
91         // would be written there). To prevent this *at syscalls are used and
92         // all symlinks in the path are rejected. This still permits the user
93         // to move dir during the sync but only to places which are writable
94         // by the user which cannot be prevented.
95
96         err := s.fileResolveIds(file)
97         if err != nil {
98                 return err
99         }
100
101         change := safcm.FileChange{
102                 Path: file.Path,
103                 New: safcm.FileChangeInfo{
104                         Mode:  file.Mode,
105                         User:  file.User,
106                         Uid:   file.Uid,
107                         Group: file.Group,
108                         Gid:   file.Gid,
109                 },
110         }
111
112         debugf := func(format string, a ...interface{}) {
113                 s.log.Debugf("files: %q (%s): %s",
114                         file.Path, file.OrigGroup, fmt.Sprintf(format, a...))
115         }
116         verbosef := func(format string, a ...interface{}) {
117                 s.log.Verbosef("files: %q (%s): %s",
118                         file.Path, file.OrigGroup, fmt.Sprintf(format, a...))
119         }
120
121         parentFd, baseName, err := OpenParentDirectoryNoSymlinks(file.Path)
122         if err != nil {
123                 if os.IsNotExist(err) && s.req.DryRun {
124                         change.Created = true
125                         debugf("will create (parent missing)")
126                         *changed = true
127                         debugf("dry-run, skipping changes")
128                         s.resp.FileChanges = append(s.resp.FileChanges, change)
129                         return nil
130                 }
131                 return err
132         }
133         defer unix.Close(parentFd)
134
135         var oldStat unix.Stat_t
136 reopen:
137         oldFh, err := OpenAtNoFollow(parentFd, baseName)
138         if err != nil {
139                 if err == unix.ELOOP || err == unix.EMLINK {
140                         // ELOOP from symbolic link, this is fine
141                         err := unix.Fstatat(parentFd, baseName, &oldStat,
142                                 unix.AT_SYMLINK_NOFOLLOW)
143                         if err != nil {
144                                 return err
145                         }
146                         if oldStat.Mode&unix.S_IFMT != unix.S_IFLNK {
147                                 debugf("type changed from symlink, retrying")
148                                 goto reopen
149                         }
150                 } else if os.IsNotExist(err) {
151                         change.Created = true
152                         debugf("will create")
153                 } else {
154                         return err
155                 }
156         } else {
157                 defer oldFh.Close()
158
159                 err := unix.Fstat(int(oldFh.Fd()), &oldStat)
160                 if err != nil {
161                         return err
162                 }
163         }
164
165         var oldData []byte
166         var changeType, changePerm, changeUserOrGroup, changeData bool
167         if !change.Created {
168                 // Manually convert to FileMode; from src/os/stat_linux.go in
169                 // Go's sources (stat_*.go for other UNIX systems are
170                 // identical, except for stat_darwin.go which has an extra
171                 // S_IFWHT)
172                 mode := fs.FileMode(oldStat.Mode & 0777)
173                 switch oldStat.Mode & unix.S_IFMT {
174                 case unix.S_IFBLK:
175                         mode |= fs.ModeDevice
176                 case unix.S_IFCHR:
177                         mode |= fs.ModeDevice | fs.ModeCharDevice
178                 case unix.S_IFDIR:
179                         mode |= fs.ModeDir
180                 case unix.S_IFIFO:
181                         mode |= fs.ModeNamedPipe
182                 case unix.S_IFLNK:
183                         mode |= fs.ModeSymlink
184                 case unix.S_IFREG:
185                         // nothing to do
186                 case unix.S_IFSOCK:
187                         mode |= fs.ModeSocket
188                 // Guard against unknown file types (e.g. on darwin); not in
189                 // stat_*.go
190                 default:
191                         return fmt.Errorf("unexpected file mode %v",
192                                 oldStat.Mode&unix.S_IFMT)
193                 }
194                 if oldStat.Mode&unix.S_ISGID != 0 {
195                         mode |= fs.ModeSetgid
196                 }
197                 if oldStat.Mode&unix.S_ISUID != 0 {
198                         mode |= fs.ModeSetuid
199                 }
200                 if oldStat.Mode&unix.S_ISVTX != 0 {
201                         mode |= fs.ModeSticky
202                 }
203
204                 // Compare permissions
205                 change.Old.Mode = mode
206                 if change.Old.Mode.Type() == fs.ModeSymlink {
207                         // Some BSD systems permit changing permissions of
208                         // symlinks but ignore them on traversal. To keep it
209                         // simple we don't support that and always use 0777
210                         // for symlink permissions (the value on GNU/Linux)
211                         // when comparing. The actual permissions on the file
212                         // system might be different on BSD systems.
213                         //
214                         // TODO: Add proper support for symlinks on BSD
215                         change.Old.Mode |= 0777
216                 }
217                 if change.Old.Mode != file.Mode {
218                         if change.Old.Mode.Type() != file.Mode.Type() {
219                                 changeType = true
220                                 debugf("type differs %s -> %s",
221                                         change.Old.Mode.Type(),
222                                         file.Mode.Type())
223                         } else {
224                                 // Be careful with .Perm() which does not
225                                 // contain the setuid/setgid/sticky bits!
226                                 changePerm = true
227                                 debugf("permission differs %s -> %s",
228                                         change.Old.Mode, file.Mode)
229                         }
230                 }
231
232                 // Compare user/group
233                 change.Old.Uid = int(oldStat.Uid)
234                 change.Old.Gid = int(oldStat.Gid)
235                 if change.Old.Uid != file.Uid || change.Old.Gid != file.Gid {
236                         changeUserOrGroup = true
237                         debugf("uid/gid differs %d/%d -> %d/%d",
238                                 change.Old.Uid, change.Old.Gid,
239                                 file.Uid, file.Gid)
240                 }
241                 u, g, err := resolveIdsToNames(change.Old.Uid, change.Old.Gid)
242                 // Errors are not relevant as this is only used to report the
243                 // change. If the user/group no longer exits only the ids will
244                 // be reported.
245                 if err == nil {
246                         change.Old.User = u
247                         change.Old.Group = g
248                 }
249
250                 // Compare file content (if possible)
251                 switch change.Old.Mode.Type() { //nolint:exhaustive
252                 case 0: // regular file
253                         x, err := io.ReadAll(oldFh)
254                         if err != nil {
255                                 return fmt.Errorf("reading old content: %v",
256                                         err)
257                         }
258                         oldData = x
259                 case fs.ModeSymlink:
260                         buf := make([]byte, unix.PathMax)
261                         n, err := unix.Readlinkat(parentFd, baseName, buf)
262                         if err != nil {
263                                 return fmt.Errorf("reading old content: %v",
264                                         err)
265                         }
266                         oldData = buf[:n]
267                 }
268                 if !changeType && file.Mode.Type() != fs.ModeDir {
269                         if !bytes.Equal(oldData, file.Data) {
270                                 changeData = true
271                                 debugf("content differs")
272                         }
273                 }
274         }
275
276         // No changes
277         if !change.Created && !changeType &&
278                 !changePerm && !changeUserOrGroup &&
279                 !changeData {
280                 debugf("unchanged")
281                 return nil
282         }
283         *changed = true
284
285         // Don't show a diff with the full content for newly created files or
286         // on type changes. This is just noise for the user as the new file
287         // content is obvious. But we always want to see a diff when files are
288         // replaced because this destroys data.
289         if !change.Created &&
290                 (change.Old.Mode.Type() == 0 ||
291                         change.Old.Mode.Type() == fs.ModeSymlink) {
292                 change.DataDiff, err = diffData(oldData, file.Data)
293                 if err != nil {
294                         return err
295                 }
296         }
297
298         // Add change here so it is stored even when applying it fails. This
299         // way the user knows exactly what was attempted.
300         s.resp.FileChanges = append(s.resp.FileChanges, change)
301
302         if change.Created {
303                 verbosef("creating")
304         } else {
305                 verbosef("updating")
306         }
307
308         if s.req.DryRun {
309                 debugf("dry-run, skipping changes")
310                 return nil
311         }
312
313         // We cannot rename over directories and vice versa
314         if changeType && (change.Old.Mode.IsDir() || file.Mode.IsDir()) {
315                 debugf("removing (due to type change)")
316                 // In the past os.RemoveAll() was used here. However, this is
317                 // difficult to implement manually with *at syscalls. To keep
318                 // it simple only permit removing files and empty directories
319                 // here. This also has the bonus of preventing data loss when
320                 // (accidentally) replacing a directory tree with a file.
321                 const msg = "will not replace non-empty directory, " +
322                         "please remove manually"
323                 err := unix.Unlinkat(parentFd, baseName, 0 /* flags */)
324                 if err != nil && !os.IsNotExist(err) {
325                         err2 := unix.Unlinkat(parentFd, baseName,
326                                 unix.AT_REMOVEDIR)
327                         if err2 != nil && !os.IsNotExist(err2) {
328                                 // See src/os/file_unix.go in Go's sources
329                                 if err2 == unix.ENOTDIR {
330                                         return err
331                                 } else if err2 == unix.ENOTEMPTY {
332                                         return fmt.Errorf(msg)
333                                 } else {
334                                         return err2
335                                 }
336                         }
337                 }
338         }
339
340         // Directory: create new directory, also type change to directory
341         if file.Mode.IsDir() && (change.Created || changeType) {
342                 debugf("creating directory")
343                 err := unix.Mkdirat(parentFd, baseName, 0700)
344                 if err != nil {
345                         return err
346                 }
347                 // We must be careful not to chmod arbitrary files. If the
348                 // target directory is writable then it might have changed to
349                 // a symlink at this point. There's no lchmod and fchmodat is
350                 // incomplete so open the directory.
351                 debugf("chmodding %s", file.Mode)
352                 dh, err := OpenAtNoFollow(parentFd, baseName)
353                 if err != nil {
354                         return err
355                 }
356                 defer dh.Close()
357
358                 err = dh.Chmod(file.Mode)
359                 if err != nil {
360                         return err
361                 }
362                 // Less restrictive access is not relevant here because there
363                 // are no files present yet.
364                 debugf("chowning %d/%d", file.Uid, file.Gid)
365                 err = dh.Chown(file.Uid, file.Gid)
366                 if err != nil {
367                         return err
368                 }
369                 return nil
370         }
371         // Directory: changed permission or user/group
372         if file.Mode.IsDir() {
373                 // We don't know if the new permission or if the new
374                 // user/group is more restrictive (e.g. root:root 0750 ->
375                 // user:group 0700; applying group first gives group
376                 // unexpected access). To prevent a short window where the
377                 // access might be too lax we temporarily deny all access.
378                 if changePerm && changeUserOrGroup {
379                         // Only drop group and other permission because user
380                         // has access anyway (either before or after the
381                         // change). This also prevents temporary errors during
382                         // the error when the user tries to access this
383                         // directory (access for the group will fail though).
384                         mode := change.Old.Mode & fs.ModePerm & 0700
385                         // Retain setgid/sticky so that the behavior does not
386                         // change when creating and removing files.
387                         mode |= change.Old.Mode & fs.ModeSetgid
388                         mode |= change.Old.Mode & fs.ModeSticky
389                         debugf("chmodding %#o (temporary)", mode)
390                         err := oldFh.Chmod(mode)
391                         if err != nil {
392                                 return err
393                         }
394                 }
395                 if changeUserOrGroup {
396                         debugf("chowning %d/%d", file.Uid, file.Gid)
397                         err := oldFh.Chown(file.Uid, file.Gid)
398                         if err != nil {
399                                 return err
400                         }
401                 }
402                 if changePerm {
403                         debugf("chmodding %s", file.Mode)
404                         err := oldFh.Chmod(file.Mode)
405                         if err != nil {
406                                 return err
407                         }
408                 }
409                 return nil
410         }
411
412         dir := slashpath.Dir(file.Path) // only used in debug messages
413         // Create hidden file which should be ignored by most other tools and
414         // thus not affect anything during creation
415         tmpBase := "." + baseName
416
417         switch file.Mode.Type() {
418         case 0: // regular file
419                 debugf("creating temporary file %q",
420                         slashpath.Join(dir, tmpBase+"*"))
421                 x, err := WriteTempAt(parentFd, tmpBase, file.Data,
422                         file.Uid, file.Gid, file.Mode)
423                 if err != nil {
424                         return err
425                 }
426                 tmpBase = x
427
428         case fs.ModeSymlink:
429                 i := 0
430         retry:
431                 x := tmpBase + strconv.Itoa(rand.Int())
432                 debugf("creating temporary symlink %q",
433                         slashpath.Join(dir, x))
434                 err := unix.Symlinkat(string(file.Data), parentFd, x)
435                 if err != nil {
436                         if os.IsExist(err) && i < 10000 {
437                                 i++
438                                 goto retry
439                         }
440                         return err
441                 }
442                 tmpBase = x
443
444                 err = unix.Fchownat(parentFd, tmpBase, file.Uid, file.Gid,
445                         unix.AT_SYMLINK_NOFOLLOW)
446                 if err != nil {
447                         unix.Unlinkat(parentFd, tmpBase, 0 /* flags */) //nolint:errcheck
448                         return err
449                 }
450                 // Permissions are irrelevant for symlinks (on most systems)
451
452         default:
453                 panic(fmt.Sprintf("invalid file type %s", file.Mode))
454         }
455
456         debugf("renaming %q", slashpath.Join(dir, tmpBase))
457         err = unix.Renameat(parentFd, tmpBase, parentFd, baseName)
458         if err != nil {
459                 unix.Unlinkat(parentFd, tmpBase, 0 /* flags */) //nolint:errcheck
460                 return err
461         }
462         // To guarantee durability fsync must be called on a parent directory
463         // after adding, renaming or removing files therein.
464         //
465         // Calling sync on the files itself is not enough according to POSIX;
466         // man 2 fsync: "Calling fsync() does not necessarily ensure that the
467         // entry in the directory containing the file has also reached disk.
468         // For that an explicit fsync() on a file descriptor for the directory
469         // is also needed."
470         err = unix.Fsync(parentFd)
471         if err != nil {
472                 return err
473         }
474
475         return nil
476 }
477
478 func (s *Sync) fileResolveIds(file *safcm.File) error {
479         if file.User != "" && file.Uid != -1 {
480                 return fmt.Errorf("cannot set both User (%q) and Uid (%d)",
481                         file.User, file.Uid)
482         }
483         if file.Group != "" && file.Gid != -1 {
484                 return fmt.Errorf("cannot set both Group (%q) and Gid (%d)",
485                         file.Group, file.Gid)
486         }
487
488         if file.User == "" && file.Uid == -1 {
489                 file.User = s.defaultUser
490         }
491         if file.User != "" {
492                 x, err := user.Lookup(file.User)
493                 if err != nil {
494                         return err
495                 }
496                 id, err := strconv.Atoi(x.Uid)
497                 if err != nil {
498                         return err
499                 }
500                 file.Uid = id
501         }
502
503         if file.Group == "" && file.Gid == -1 {
504                 file.Group = s.defaultGroup
505         }
506         if file.Group != "" {
507                 x, err := user.LookupGroup(file.Group)
508                 if err != nil {
509                         return err
510                 }
511                 id, err := strconv.Atoi(x.Gid)
512                 if err != nil {
513                         return err
514                 }
515                 file.Gid = id
516         }
517
518         return nil
519 }
520
521 // OpenParentDirectoryNoSymlinks opens the dirname of path without following
522 // any symlinks and returns a file descriptor to it and the basename of path.
523 // To prevent symlink attacks in earlier path components when these are
524 // writable by other users it starts at the root (or current directory for
525 // relative paths) and uses openat (with O_NOFOLLOW) for each path component.
526 // If a symlink is encountered it returns an error. However, it's impossible
527 // to guarantee that the returned descriptor refers to the same location as
528 // given in path because users with write access can rename path components.
529 // But this is not required to prevent the mentioned attacks.
530 func OpenParentDirectoryNoSymlinks(path string) (int, string, error) {
531         // Slash separated paths are used for the configuration
532         parts := strings.Split(path, "/")
533
534         var dir string
535         if path == "/" {
536                 // Root: use root itself as base name because root is the
537                 // parent of itself
538                 dir = "/"
539                 parts = []string{"/"}
540         } else if parts[0] == "" {
541                 // Absolute path
542                 dir = "/"
543                 parts = parts[1:]
544         } else if path == "." {
545                 // Current directory: open parent directory and use current
546                 // directory name as base name
547                 wd, err := os.Getwd()
548                 if err != nil {
549                         return -1, "", fmt.Errorf(
550                                 "failed to get working directory: %w", err)
551                 }
552                 dir = ".."
553                 parts = []string{filepath.Base(wd)}
554         } else {
555                 // Relative path: start at the current directory
556                 dir = "."
557                 if parts[0] == "." {
558                         parts = parts[1:]
559                 }
560         }
561
562         dirFd, err := unix.Openat(unix.AT_FDCWD, dir,
563                 openReadonlyFlags, 0 /* mode */)
564         if err != nil {
565                 return -1, "", err
566         }
567         // Walk path one directory at a time to ensure there are no symlinks
568         // in the path. This prevents users with write access to change the
569         // path to point to arbitrary locations. O_NOFOLLOW when opening the
570         // path is not enough as only the last path component is checked.
571         for i, name := range parts[:len(parts)-1] {
572                 fd, err := unix.Openat(dirFd, name, openReadonlyFlags, 0)
573                 if err != nil {
574                         unix.Close(dirFd)
575                         if err == unix.ELOOP || err == unix.EMLINK {
576                                 x := filepath.Join(append([]string{dir},
577                                         parts[:i+1]...)...)
578                                 return -1, "", fmt.Errorf(
579                                         "symlink not permitted in path: %q",
580                                         x)
581                         }
582                         return -1, "", err
583                 }
584                 unix.Close(dirFd)
585                 dirFd = fd
586         }
587
588         return dirFd, parts[len(parts)-1], nil
589 }
590
591 func resolveIdsToNames(uid, gid int) (string, string, error) {
592         u, err := user.LookupId(strconv.Itoa(uid))
593         if err != nil {
594                 return "", "", err
595         }
596         g, err := user.LookupGroupId(strconv.Itoa(gid))
597         if err != nil {
598                 return "", "", err
599         }
600         return u.Username, g.Name, nil
601 }
602
603 func diffData(oldData []byte, newData []byte) (string, error) {
604         oldBin := !strings.HasPrefix(http.DetectContentType(oldData), "text/")
605         newBin := !strings.HasPrefix(http.DetectContentType(newData), "text/")
606         if oldBin && newBin {
607                 return fmt.Sprintf("Binary files differ (%d -> %d bytes), "+
608                         "cannot show diff", len(oldData), len(newData)), nil
609         }
610         if oldBin {
611                 oldData = []byte(fmt.Sprintf("<binary content, %d bytes>\n",
612                         len(oldData)))
613         }
614         if newBin {
615                 newData = []byte(fmt.Sprintf("<binary content, %d bytes>\n",
616                         len(newData)))
617         }
618
619         // TODO: difflib shows empty context lines at the end of the file
620         // which should not be there
621         // TODO: difflib has issues with missing newlines in either side
622         result, err := difflib.GetUnifiedDiffString(difflib.LineDiffParams{
623                 A:       difflib.SplitLines(string(oldData)),
624                 B:       difflib.SplitLines(string(newData)),
625                 Context: 3,
626         })
627         if err != nil {
628                 return "", err
629         }
630         return result, nil
631 }
632
633 func OpenFileNoSymlinks(path string) (*os.File, error) {
634         parentFd, baseName, err := OpenParentDirectoryNoSymlinks(path)
635         if err != nil {
636                 return nil, err
637         }
638         defer unix.Close(parentFd)
639         return OpenAtNoFollow(parentFd, baseName)
640 }
641
642 func OpenAtNoFollow(dirFd int, base string) (*os.File, error) {
643         fd, err := unix.Openat(dirFd, base, openReadonlyFlags, 0 /* mode */)
644         if err != nil {
645                 return nil, err
646         }
647         return os.NewFile(uintptr(fd), ""), nil
648 }
649
650 func WriteTempAt(dirFd int, base string, data []byte, uid, gid int,
651         mode fs.FileMode) (string, error) {
652
653         fh, tmpBase, err := createTempAt(dirFd, base)
654         if err != nil {
655                 return "", err
656         }
657
658         _, err = fh.Write(data)
659         if err != nil {
660                 fh.Close()
661                 unix.Unlinkat(dirFd, tmpBase, 0 /* flags */) //nolint:errcheck
662                 return "", err
663         }
664         // createTempAt() creates the file with 0600
665         err = fh.Chown(uid, gid)
666         if err != nil {
667                 fh.Close()
668                 unix.Unlinkat(dirFd, tmpBase, 0) //nolint:errcheck
669                 return "", err
670         }
671         err = fh.Chmod(mode)
672         if err != nil {
673                 fh.Close()
674                 unix.Unlinkat(dirFd, tmpBase, 0) //nolint:errcheck
675                 return "", err
676         }
677         err = fh.Sync()
678         if err != nil {
679                 fh.Close()
680                 unix.Unlinkat(dirFd, tmpBase, 0) //nolint:errcheck
681                 return "", err
682         }
683         err = fh.Close()
684         if err != nil {
685                 unix.Unlinkat(dirFd, tmpBase, 0) //nolint:errcheck
686                 return "", err
687         }
688
689         return tmpBase, nil
690 }
691
692 // createTempAt works similar to os.CreateTemp but uses unix.Openat to create
693 // the temporary file.
694 func createTempAt(dirFd int, base string) (*os.File, string, error) {
695         var tmpBase string
696
697         i := 0
698 retry:
699         tmpBase = base + strconv.Itoa(rand.Int())
700
701         fd, err := unix.Openat(dirFd, tmpBase,
702                 unix.O_RDWR|unix.O_CREAT|unix.O_EXCL, 0600)
703         if err != nil {
704                 if os.IsExist(err) && i < 10000 {
705                         i++
706                         goto retry
707                 }
708                 return nil, "", err
709         }
710
711         return os.NewFile(uintptr(fd), ""), tmpBase, nil
712 }