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