diff options
Diffstat (limited to 'internal')
73 files changed, 1197 insertions, 551 deletions
diff --git a/internal/configfile/config_file.go b/internal/configfile/config_file.go index 995a0c8..28a1ca5 100644 --- a/internal/configfile/config_file.go +++ b/internal/configfile/config_file.go @@ -5,7 +5,6 @@ package configfile import ( "encoding/json" "fmt" - "io/ioutil" "syscall" "os" @@ -183,12 +182,12 @@ func Load(filename string) (*ConfFile, error) { cf.filename = filename // Read from disk - js, err := ioutil.ReadFile(filename) + js, err := os.ReadFile(filename) if err != nil { return nil, err } if len(js) == 0 { - return nil, fmt.Errorf("Config file is empty") + return nil, fmt.Errorf("config file is empty") } // Unmarshal diff --git a/internal/configfile/scrypt.go b/internal/configfile/scrypt.go index 0ce8777..f6201ba 100644 --- a/internal/configfile/scrypt.go +++ b/internal/configfile/scrypt.go @@ -87,19 +87,19 @@ func (s *ScryptKDF) LogN() int { func (s *ScryptKDF) validateParams() error { minN := 1 << scryptMinLogN if s.N < minN { - return fmt.Errorf("Fatal: scryptn below 10 is too low to make sense") + return fmt.Errorf("fatal: scryptn below 10 is too low to make sense") } if s.R < scryptMinR { - return fmt.Errorf("Fatal: scrypt parameter R below minimum: value=%d, min=%d", s.R, scryptMinR) + return fmt.Errorf("fatal: scrypt parameter R below minimum: value=%d, min=%d", s.R, scryptMinR) } if s.P < scryptMinP { - return fmt.Errorf("Fatal: scrypt parameter P below minimum: value=%d, min=%d", s.P, scryptMinP) + return fmt.Errorf("fatal: scrypt parameter P below minimum: value=%d, min=%d", s.P, scryptMinP) } if len(s.Salt) < scryptMinSaltLen { - return fmt.Errorf("Fatal: scrypt salt length below minimum: value=%d, min=%d", len(s.Salt), scryptMinSaltLen) + return fmt.Errorf("fatal: scrypt salt length below minimum: value=%d, min=%d", len(s.Salt), scryptMinSaltLen) } if s.KeyLen < cryptocore.KeyLen { - return fmt.Errorf("Fatal: scrypt parameter KeyLen below minimum: value=%d, min=%d", s.KeyLen, cryptocore.KeyLen) + return fmt.Errorf("fatal: scrypt parameter KeyLen below minimum: value=%d, min=%d", s.KeyLen, cryptocore.KeyLen) } return nil } diff --git a/internal/configfile/validate.go b/internal/configfile/validate.go index ab8917d..5428a7b 100644 --- a/internal/configfile/validate.go +++ b/internal/configfile/validate.go @@ -9,7 +9,7 @@ import ( // Validate that the combination of settings makes sense and is supported func (cf *ConfFile) Validate() error { if cf.Version != contentenc.CurrentVersion { - return fmt.Errorf("Unsupported on-disk format %d", cf.Version) + return fmt.Errorf("unsupported on-disk format %d", cf.Version) } // scrypt params ok? if err := cf.ScryptObject.validateParams(); err != nil { @@ -18,13 +18,13 @@ func (cf *ConfFile) Validate() error { // All feature flags that are in the config file are known? for _, flag := range cf.FeatureFlags { if !isFeatureFlagKnown(flag) { - return fmt.Errorf("Unknown feature flag %q", flag) + return fmt.Errorf("unknown feature flag %q", flag) } } // File content encryption { if cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) && cf.IsFeatureFlagSet(FlagAESSIV) { - return fmt.Errorf("Can't have both XChaCha20Poly1305 and AESSIV feature flags") + return fmt.Errorf("can't have both XChaCha20Poly1305 and AESSIV feature flags") } if cf.IsFeatureFlagSet(FlagAESSIV) && !cf.IsFeatureFlagSet(FlagGCMIV128) { diff --git a/internal/contentenc/bpool.go b/internal/contentenc/bpool.go index c4517d3..c62170d 100644 --- a/internal/contentenc/bpool.go +++ b/internal/contentenc/bpool.go @@ -26,6 +26,7 @@ func (b *bPool) Put(s []byte) { if len(s) != b.sliceLen { log.Panicf("wrong len=%d, want=%d", len(s), b.sliceLen) } + //lint:ignore SA6002 We intentionally pass slice by value to avoid allocation overhead in this specific use case b.Pool.Put(s) } diff --git a/internal/contentenc/content.go b/internal/contentenc/content.go index 3005bf5..5bf0b3c 100644 --- a/internal/contentenc/content.go +++ b/internal/contentenc/content.go @@ -151,7 +151,7 @@ func (be *ContentEnc) DecryptBlock(ciphertext []byte, blockNo uint64, fileID []b if len(ciphertext) < be.cryptoCore.IVLen { tlog.Warn.Printf("DecryptBlock: Block is too short: %d bytes", len(ciphertext)) - return nil, errors.New("Block is too short") + return nil, errors.New("block is too short") } // Extract nonce diff --git a/internal/cryptocore/hkdf.go b/internal/cryptocore/hkdf.go index b56f507..369616a 100644 --- a/internal/cryptocore/hkdf.go +++ b/internal/cryptocore/hkdf.go @@ -1,10 +1,9 @@ package cryptocore import ( + "crypto/hkdf" "crypto/sha256" "log" - - "golang.org/x/crypto/hkdf" ) const ( @@ -19,12 +18,10 @@ const ( // hkdfDerive derives "outLen" bytes from "masterkey" and "info" using // HKDF-SHA256 (RFC 5869). // It returns the derived bytes or panics. -func hkdfDerive(masterkey []byte, info string, outLen int) (out []byte) { - h := hkdf.New(sha256.New, masterkey, nil, []byte(info)) - out = make([]byte, outLen) - n, err := h.Read(out) - if n != outLen || err != nil { - log.Panicf("hkdfDerive: hkdf read failed, got %d bytes, error: %v", n, err) +func hkdfDerive(masterkey []byte, info string, outLen int) []byte { + key, err := hkdf.Key(sha256.New, masterkey, nil, info, outLen) + if err != nil { + log.Panicf("hkdfDerive: hkdf failed with error: %v", err) } - return out + return key } diff --git a/internal/cryptocore/nonce.go b/internal/cryptocore/nonce.go index 9df094c..c800807 100644 --- a/internal/cryptocore/nonce.go +++ b/internal/cryptocore/nonce.go @@ -11,6 +11,8 @@ func RandBytes(n int) []byte { b := make([]byte, n) _, err := rand.Read(b) if err != nil { + // crypto/rand.Read() is documented to never return an + // error, so this should never happen. Still, better safe than sorry. log.Panic("Failed to read random bytes: " + err.Error()) } return b diff --git a/internal/ctlsocksrv/ctlsock_serve.go b/internal/ctlsocksrv/ctlsock_serve.go index 85f5b65..25d1e44 100644 --- a/internal/ctlsocksrv/ctlsock_serve.go +++ b/internal/ctlsocksrv/ctlsock_serve.go @@ -101,7 +101,7 @@ func (ch *ctlSockHandler) handleRequest(in *ctlsock.RequestStruct, conn *net.Uni } // Neither encryption nor encryption has been requested, makes no sense if in.DecryptPath == "" && in.EncryptPath == "" { - err = errors.New("Empty input") + err = errors.New("empty input") sendResponse(conn, err, "", "") return } @@ -118,7 +118,7 @@ func (ch *ctlSockHandler) handleRequest(in *ctlsock.RequestStruct, conn *net.Uni } // Error out if the canonical path is now empty if clean == "" { - err = errors.New("Empty input after canonicalization") + err = errors.New("empty input after canonicalization") sendResponse(conn, err, "", warnText) return } diff --git a/internal/exitcodes/exitcodes.go b/internal/exitcodes/exitcodes.go index 508ba38..881afda 100644 --- a/internal/exitcodes/exitcodes.go +++ b/internal/exitcodes/exitcodes.go @@ -3,7 +3,7 @@ package exitcodes import ( - "fmt" + "errors" "os" ) @@ -83,7 +83,7 @@ type Err struct { // NewErr returns an error containing "msg" and the exit code "code". func NewErr(msg string, code int) Err { return Err{ - error: fmt.Errorf(msg), + error: errors.New(msg), code: code, } } diff --git a/internal/fusefrontend/args.go b/internal/fusefrontend/args.go index 64a5923..ec3d1c2 100644 --- a/internal/fusefrontend/args.go +++ b/internal/fusefrontend/args.go @@ -51,4 +51,6 @@ type Args struct { OneFileSystem bool // DeterministicNames disables gocryptfs.diriv files DeterministicNames bool + // NoXattr disables extended attribute operations + NoXattr bool } diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go index 0e25de3..64c6ca0 100644 --- a/internal/fusefrontend/file.go +++ b/internal/fusefrontend/file.go @@ -37,8 +37,6 @@ type File struct { // Every FUSE entrypoint should RLock(). The only user of Lock() is // Release(), which closes the fd and sets "released" to true. fdLock sync.RWMutex - // Content encryption helper - contentEnc *contentenc.ContentEnc // Device and inode number uniquely identify the backing file qIno inomap.QIno // Entry in the open file table @@ -50,6 +48,8 @@ type File struct { lastOpCount uint64 // Parent filesystem rootNode *RootNode + // If this open file is a directory, dirHandle will be set, otherwise it's nil. + dirHandle *DirHandle } // NewFile returns a new go-fuse File instance based on an already-open file @@ -71,7 +71,6 @@ func NewFile(fd int, cName string, rn *RootNode) (f *File, st *syscall.Stat_t, e f = &File{ fd: osFile, - contentEnc: rn.contentEnc, qIno: qi, fileTableEntry: e, rootNode: rn, @@ -116,7 +115,7 @@ func (f *File) createHeader() (fileID []byte, err error) { h := contentenc.RandomHeader() buf := h.Pack() // Prevent partially written (=corrupt) header by preallocating the space beforehand - if !f.rootNode.args.NoPrealloc && f.rootNode.quirks&syscallcompat.QuirkBrokenFalloc == 0 { + if !f.rootNode.args.NoPrealloc && f.rootNode.quirks&syscallcompat.QuirkBtrfsBrokenFalloc == 0 { err = syscallcompat.EnospcPrealloc(f.intFd(), 0, contentenc.HeaderLen) if err != nil { if !syscallcompat.IsENOSPC(err) { @@ -175,7 +174,7 @@ func (f *File) doRead(dst []byte, off uint64, length uint64) ([]byte, syscall.Er log.Panicf("fileID=%v", fileID) } // Read the backing ciphertext in one go - blocks := f.contentEnc.ExplodePlainRange(off, length) + blocks := f.rootNode.contentEnc.ExplodePlainRange(off, length) alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks) // f.fd.ReadAt takes an int64! if alignedOffset > math.MaxInt64 { @@ -204,10 +203,10 @@ func (f *File) doRead(dst []byte, off uint64, length uint64) ([]byte, syscall.Er tlog.Debug.Printf("ReadAt offset=%d bytes (%d blocks), want=%d, got=%d", alignedOffset, firstBlockNo, alignedLength, n) // Decrypt it - plaintext, err := f.contentEnc.DecryptBlocks(ciphertext, firstBlockNo, fileID) + plaintext, err := f.rootNode.contentEnc.DecryptBlocks(ciphertext, firstBlockNo, fileID) f.rootNode.contentEnc.CReqPool.Put(ciphertext) if err != nil { - corruptBlockNo := firstBlockNo + f.contentEnc.PlainOffToBlockNo(uint64(len(plaintext))) + corruptBlockNo := firstBlockNo + f.rootNode.contentEnc.PlainOffToBlockNo(uint64(len(plaintext))) tlog.Warn.Printf("doRead %d: corrupt block #%d: %v", f.qIno.Ino, corruptBlockNo, err) return nil, syscall.EIO } @@ -285,20 +284,20 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) { } // Handle payload data dataBuf := bytes.NewBuffer(data) - blocks := f.contentEnc.ExplodePlainRange(uint64(off), uint64(len(data))) + blocks := f.rootNode.contentEnc.ExplodePlainRange(uint64(off), uint64(len(data))) toEncrypt := make([][]byte, len(blocks)) for i, b := range blocks { blockData := dataBuf.Next(int(b.Length)) // Incomplete block -> Read-Modify-Write if b.IsPartial() { // Read - oldData, errno := f.doRead(nil, b.BlockPlainOff(), f.contentEnc.PlainBS()) + oldData, errno := f.doRead(nil, b.BlockPlainOff(), f.rootNode.contentEnc.PlainBS()) if errno != 0 { tlog.Warn.Printf("ino%d fh%d: RMW read failed: errno=%d", f.qIno.Ino, f.intFd(), errno) return 0, errno } // Modify - blockData = f.contentEnc.MergeBlocks(oldData, blockData, int(b.Skip)) + blockData = f.rootNode.contentEnc.MergeBlocks(oldData, blockData, int(b.Skip)) tlog.Debug.Printf("len(oldData)=%d len(blockData)=%d", len(oldData), len(blockData)) } tlog.Debug.Printf("ino%d: Writing %d bytes to block #%d", @@ -307,7 +306,7 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) { toEncrypt[i] = blockData } // Encrypt all blocks - ciphertext := f.contentEnc.EncryptBlocks(toEncrypt, blocks[0].BlockNo, f.fileTableEntry.ID) + ciphertext := f.rootNode.contentEnc.EncryptBlocks(toEncrypt, blocks[0].BlockNo, f.fileTableEntry.ID) // Preallocate so we cannot run out of space in the middle of the write. // This prevents partially written (=corrupt) blocks. var err error @@ -316,7 +315,7 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) { if cOff > math.MaxInt64 { return 0, syscall.EFBIG } - if !f.rootNode.args.NoPrealloc && f.rootNode.quirks&syscallcompat.QuirkBrokenFalloc == 0 { + if !f.rootNode.args.NoPrealloc && f.rootNode.quirks&syscallcompat.QuirkBtrfsBrokenFalloc == 0 { err = syscallcompat.EnospcPrealloc(f.intFd(), int64(cOff), int64(len(ciphertext))) if err != nil { if !syscallcompat.IsENOSPC(err) { @@ -437,7 +436,10 @@ func (f *File) Getattr(ctx context.Context, a *fuse.AttrOut) syscall.Errno { } f.rootNode.inoMap.TranslateStat(&st) a.FromStat(&st) - a.Size = f.contentEnc.CipherSizeToPlainSize(a.Size) + if a.IsRegular() { + a.Size = f.rootNode.contentEnc.CipherSizeToPlainSize(a.Size) + } + // TODO: Handle symlink size similar to node.translateSize() if f.rootNode.args.ForceOwner != nil { a.Owner = *f.rootNode.args.ForceOwner } diff --git a/internal/fusefrontend/file_allocate_truncate.go b/internal/fusefrontend/file_allocate_truncate.go index cae796e..a3decf9 100644 --- a/internal/fusefrontend/file_allocate_truncate.go +++ b/internal/fusefrontend/file_allocate_truncate.go @@ -54,7 +54,7 @@ func (f *File) Allocate(ctx context.Context, off uint64, sz uint64, mode uint32) f.fileTableEntry.ContentLock.Lock() defer f.fileTableEntry.ContentLock.Unlock() - blocks := f.contentEnc.ExplodePlainRange(off, sz) + blocks := f.rootNode.contentEnc.ExplodePlainRange(off, sz) firstBlock := blocks[0] lastBlock := blocks[len(blocks)-1] @@ -63,7 +63,7 @@ func (f *File) Allocate(ctx context.Context, off uint64, sz uint64, mode uint32) // the file. cipherOff := firstBlock.BlockCipherOff() cipherSz := lastBlock.BlockCipherOff() - cipherOff + - f.contentEnc.BlockOverhead() + lastBlock.Skip + lastBlock.Length + f.rootNode.contentEnc.BlockOverhead() + lastBlock.Skip + lastBlock.Length err := syscallcompat.Fallocate(f.intFd(), FALLOC_FL_KEEP_SIZE, int64(cipherOff), int64(cipherSz)) tlog.Debug.Printf("Allocate off=%d sz=%d mode=%x cipherOff=%d cipherSz=%d\n", off, sz, mode, cipherOff, cipherSz) @@ -113,8 +113,8 @@ func (f *File) truncate(newSize uint64) (errno syscall.Errno) { return fs.ToErrno(err) } - oldB := float32(oldSize) / float32(f.contentEnc.PlainBS()) - newB := float32(newSize) / float32(f.contentEnc.PlainBS()) + oldB := float32(oldSize) / float32(f.rootNode.contentEnc.PlainBS()) + newB := float32(newSize) / float32(f.rootNode.contentEnc.PlainBS()) tlog.Debug.Printf("ino%d: FUSE Truncate from %.2f to %.2f blocks (%d to %d bytes)", f.qIno.Ino, oldB, newB, oldSize, newSize) // File size stays the same - nothing to do @@ -127,9 +127,9 @@ func (f *File) truncate(newSize uint64) (errno syscall.Errno) { } // File shrinks - blockNo := f.contentEnc.PlainOffToBlockNo(newSize) - cipherOff := f.contentEnc.BlockNoToCipherOff(blockNo) - plainOff := f.contentEnc.BlockNoToPlainOff(blockNo) + blockNo := f.rootNode.contentEnc.PlainOffToBlockNo(newSize) + cipherOff := f.rootNode.contentEnc.BlockNoToCipherOff(blockNo) + plainOff := f.rootNode.contentEnc.BlockNoToPlainOff(blockNo) lastBlockLen := newSize - plainOff var data []byte if lastBlockLen > 0 { @@ -161,7 +161,7 @@ func (f *File) statPlainSize() (uint64, error) { return 0, err } cipherSz := uint64(fi.Size()) - plainSz := uint64(f.contentEnc.CipherSizeToPlainSize(cipherSz)) + plainSz := uint64(f.rootNode.contentEnc.CipherSizeToPlainSize(cipherSz)) return plainSz, nil } @@ -174,8 +174,8 @@ func (f *File) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) syscall.Er } newEOFOffset := newPlainSz - 1 if oldPlainSz > 0 { - n1 := f.contentEnc.PlainOffToBlockNo(oldPlainSz - 1) - n2 := f.contentEnc.PlainOffToBlockNo(newEOFOffset) + n1 := f.rootNode.contentEnc.PlainOffToBlockNo(oldPlainSz - 1) + n2 := f.rootNode.contentEnc.PlainOffToBlockNo(newEOFOffset) // The file is grown within one block, no need to pad anything. // Write a single zero to the last byte and let doWrite figure out the RMW. if n1 == n2 { @@ -194,7 +194,7 @@ func (f *File) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) syscall.Er } // The new size is block-aligned. In this case we can do everything ourselves // and avoid the call to doWrite. - if newPlainSz%f.contentEnc.PlainBS() == 0 { + if newPlainSz%f.rootNode.contentEnc.PlainBS() == 0 { // The file was empty, so it did not have a header. Create one. if oldPlainSz == 0 { id, err := f.createHeader() @@ -203,7 +203,7 @@ func (f *File) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) syscall.Er } f.fileTableEntry.ID = id } - cSz := int64(f.contentEnc.PlainSizeToCipherSize(newPlainSz)) + cSz := int64(f.rootNode.contentEnc.PlainSizeToCipherSize(newPlainSz)) err := syscall.Ftruncate(f.intFd(), cSz) if err != nil { tlog.Warn.Printf("Truncate: grow Ftruncate returned error: %v", err) diff --git a/internal/fusefrontend/file_dir_ops.go b/internal/fusefrontend/file_dir_ops.go new file mode 100644 index 0000000..b69e7bc --- /dev/null +++ b/internal/fusefrontend/file_dir_ops.go @@ -0,0 +1,177 @@ +package fusefrontend + +import ( + "context" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/rfjakob/gocryptfs/v2/internal/configfile" + "github.com/rfjakob/gocryptfs/v2/internal/nametransform" + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/v2/internal/tlog" +) + +func (n *Node) OpendirHandle(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + var fd int = -1 + var fdDup int = -1 + var file *File + var dirIV []byte + var ds fs.DirStream + rn := n.rootNode() + + dirfd, cName, errno := n.prepareAtSyscallMyself() + if errno != 0 { + return + } + defer syscall.Close(dirfd) + + // Open backing directory + fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) + if err != nil { + errno = fs.ToErrno(err) + return + } + + // NewLoopbackDirStreamFd gets its own fd to untangle Release vs Releasedir + fdDup, err = syscall.Dup(fd) + + if err != nil { + errno = fs.ToErrno(err) + goto err_out + } + + ds, errno = fs.NewLoopbackDirStreamFd(fdDup) + if errno != 0 { + goto err_out + } + + if !rn.args.PlaintextNames { + // Read the DirIV from disk + dirIV, err = rn.nameTransform.ReadDirIVAt(fd) + if err != nil { + tlog.Warn.Printf("OpendirHandle: could not read %s: %v", nametransform.DirIVFilename, err) + errno = syscall.EIO + goto err_out + } + } + + file, _, errno = NewFile(fd, cName, rn) + if errno != 0 { + goto err_out + } + + file.dirHandle = &DirHandle{ + ds: ds, + dirIV: dirIV, + isRootDir: n.IsRoot(), + } + + return file, fuseFlags, errno + +err_out: + if fd >= 0 { + syscall.Close(fd) + } + if fdDup >= 0 { + syscall.Close(fdDup) + } + if errno == 0 { + tlog.Warn.Printf("BUG: OpendirHandle: err_out called with errno == 0") + errno = syscall.EIO + } + return nil, 0, errno +} + +type DirHandle struct { + // Content of gocryptfs.diriv. nil if plaintextnames is used. + dirIV []byte + + isRootDir bool + + // fs.loopbackDirStream with a private dup of the file descriptor + ds fs.FileHandle +} + +var _ = (fs.FileReleasedirer)((*File)(nil)) + +func (f *File) Releasedir(ctx context.Context, flags uint32) { + // Does its own locking + f.dirHandle.ds.(fs.FileReleasedirer).Releasedir(ctx, flags) + // Does its own locking + f.Release(ctx) +} + +var _ = (fs.FileSeekdirer)((*File)(nil)) + +func (f *File) Seekdir(ctx context.Context, off uint64) syscall.Errno { + return f.dirHandle.ds.(fs.FileSeekdirer).Seekdir(ctx, off) +} + +var _ = (fs.FileFsyncdirer)((*File)(nil)) + +func (f *File) Fsyncdir(ctx context.Context, flags uint32) syscall.Errno { + return f.dirHandle.ds.(fs.FileFsyncdirer).Fsyncdir(ctx, flags) +} + +var _ = (fs.FileReaddirenter)((*File)(nil)) + +// This function is symlink-safe through use of openBackingDir() and +// ReadDirIVAt(). +func (f *File) Readdirent(ctx context.Context) (entry *fuse.DirEntry, errno syscall.Errno) { + f.fdLock.RLock() + defer f.fdLock.RUnlock() + + for { + entry, errno = f.dirHandle.ds.(fs.FileReaddirenter).Readdirent(ctx) + if errno != 0 || entry == nil { + return + } + + cName := entry.Name + if cName == "." || cName == ".." { + // We want these as-is + return + } + if f.dirHandle.isRootDir && cName == configfile.ConfDefaultName { + // silently ignore "gocryptfs.conf" in the top level dir + continue + } + if f.rootNode.args.PlaintextNames { + return + } + if !f.rootNode.args.DeterministicNames && cName == nametransform.DirIVFilename { + // silently ignore "gocryptfs.diriv" everywhere if dirIV is enabled + continue + } + // Handle long file name + isLong := nametransform.LongNameNone + if f.rootNode.args.LongNames { + isLong = nametransform.NameType(cName) + } + if isLong == nametransform.LongNameContent { + cNameLong, err := nametransform.ReadLongNameAt(f.intFd(), cName) + if err != nil { + tlog.Warn.Printf("Readdirent: incomplete entry %q: Could not read .name: %v", + cName, err) + f.rootNode.reportMitigatedCorruption(cName) + continue + } + cName = cNameLong + } else if isLong == nametransform.LongNameFilename { + // ignore "gocryptfs.longname.*.name" + continue + } + name, err := f.rootNode.nameTransform.DecryptName(cName, f.dirHandle.dirIV) + if err != nil { + tlog.Warn.Printf("Readdirent: could not decrypt entry %q: %v", + cName, err) + f.rootNode.reportMitigatedCorruption(cName) + continue + } + // Override the ciphertext name with the plaintext name but reuse the rest + // of the structure + entry.Name = name + return + } +} diff --git a/internal/fusefrontend/file_holes.go b/internal/fusefrontend/file_holes.go index f35fa70..fc58898 100644 --- a/internal/fusefrontend/file_holes.go +++ b/internal/fusefrontend/file_holes.go @@ -21,12 +21,12 @@ func (f *File) writePadHole(targetOff int64) syscall.Errno { tlog.Warn.Printf("checkAndPadHole: Fstat failed: %v", err) return fs.ToErrno(err) } - plainSize := f.contentEnc.CipherSizeToPlainSize(uint64(fi.Size())) + plainSize := f.rootNode.contentEnc.CipherSizeToPlainSize(uint64(fi.Size())) // Appending a single byte to the file (equivalent to writing to // offset=plainSize) would write to "nextBlock". - nextBlock := f.contentEnc.PlainOffToBlockNo(plainSize) + nextBlock := f.rootNode.contentEnc.PlainOffToBlockNo(plainSize) // targetBlock is the block the user wants to write to. - targetBlock := f.contentEnc.PlainOffToBlockNo(uint64(targetOff)) + targetBlock := f.rootNode.contentEnc.PlainOffToBlockNo(uint64(targetOff)) // The write goes into an existing block or (if the last block was full) // starts a new one directly after the last block. Nothing to do. if targetBlock <= nextBlock { @@ -45,12 +45,12 @@ func (f *File) writePadHole(targetOff int64) syscall.Errno { // Zero-pad the file of size plainSize to the next block boundary. This is a no-op // if the file is already block-aligned. func (f *File) zeroPad(plainSize uint64) syscall.Errno { - lastBlockLen := plainSize % f.contentEnc.PlainBS() + lastBlockLen := plainSize % f.rootNode.contentEnc.PlainBS() if lastBlockLen == 0 { // Already block-aligned return 0 } - missing := f.contentEnc.PlainBS() - lastBlockLen + missing := f.rootNode.contentEnc.PlainBS() - lastBlockLen pad := make([]byte, missing) tlog.Debug.Printf("zeroPad: Writing %d bytes\n", missing) _, errno := f.doWrite(pad, int64(plainSize)) diff --git a/internal/fusefrontend/node.go b/internal/fusefrontend/node.go index 687a386..95be48d 100644 --- a/internal/fusefrontend/node.go +++ b/internal/fusefrontend/node.go @@ -71,28 +71,54 @@ func (n *Node) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (ch func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) (errno syscall.Errno) { // If the kernel gives us a file handle, use it. if f != nil { - return f.(fs.FileGetattrer).Getattr(ctx, out) + if fga, ok := f.(fs.FileGetattrer); ok { + return fga.Getattr(ctx, out) + } } + rn := n.rootNode() + var st *syscall.Stat_t + var err error dirfd, cName, errno := n.prepareAtSyscallMyself() + // Hack for deleted fifos. As OPEN on a fifo does not reach + // the filesystem, we have no fd to access it. To make "cat" and git's + // t9300-fast-import.sh happy, we fake it as best as we can. + // https://github.com/rfjakob/gocryptfs/issues/929 + if errno == syscall.ENOENT && n.StableAttr().Mode == syscall.S_IFIFO { + out.Mode = syscall.S_IFIFO + out.Ino = n.StableAttr().Ino + // cat looks at this to determine the optimal io size. Seems to be always 4096 for fifos. + out.Blksize = 4096 + // We don't know what the owner was. Set it to nobody which seems safer + // than leaving it at 0 (root). + out.Owner.Gid = 65534 + out.Owner.Uid = 65534 + // All the other fields stay 0. This is what cat sees (strace -v log): + // + // fstat(1, {st_dev=makedev(0, 0x4d), st_ino=3838227, st_mode=S_IFIFO|000, st_nlink=0, + // st_uid=65534, st_gid=65534, st_blksize=4096, st_blocks=0, st_size=0, st_atime=0, + // st_atime_nsec=0, st_mtime=0, st_mtime_nsec=0, st_ctime=0, st_ctime_nsec=0}) = 0 + goto out + } if errno != 0 { return } + defer syscall.Close(dirfd) - st, err := syscallcompat.Fstatat2(dirfd, cName, unix.AT_SYMLINK_NOFOLLOW) + st, err = syscallcompat.Fstatat2(dirfd, cName, unix.AT_SYMLINK_NOFOLLOW) if err != nil { return fs.ToErrno(err) } // Fix inode number - rn := n.rootNode() rn.inoMap.TranslateStat(st) out.Attr.FromStat(st) // Translate ciphertext size in `out.Attr.Size` to plaintext size n.translateSize(dirfd, cName, &out.Attr) +out: if rn.args.ForceOwner != nil { out.Owner = *rn.args.ForceOwner } diff --git a/internal/fusefrontend/node_api_check.go b/internal/fusefrontend/node_api_check.go index 0f60c74..37d4293 100644 --- a/internal/fusefrontend/node_api_check.go +++ b/internal/fusefrontend/node_api_check.go @@ -7,7 +7,6 @@ import ( // Check that we have implemented the fs.Node* interfaces var _ = (fs.NodeGetattrer)((*Node)(nil)) var _ = (fs.NodeLookuper)((*Node)(nil)) -var _ = (fs.NodeReaddirer)((*Node)(nil)) var _ = (fs.NodeCreater)((*Node)(nil)) var _ = (fs.NodeMkdirer)((*Node)(nil)) var _ = (fs.NodeRmdirer)((*Node)(nil)) diff --git a/internal/fusefrontend/node_dir_ops.go b/internal/fusefrontend/node_dir_ops.go index 97e4caa..97327ce 100644 --- a/internal/fusefrontend/node_dir_ops.go +++ b/internal/fusefrontend/node_dir_ops.go @@ -12,7 +12,6 @@ import ( "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" - "github.com/rfjakob/gocryptfs/v2/internal/configfile" "github.com/rfjakob/gocryptfs/v2/internal/cryptocore" "github.com/rfjakob/gocryptfs/v2/internal/nametransform" "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" @@ -137,12 +136,6 @@ func (n *Node) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.En } defer syscall.Close(fd) - err = syscall.Fstat(fd, &st) - if err != nil { - tlog.Warn.Printf("Mkdir %q: Fstat failed: %v", cName, err) - return nil, fs.ToErrno(err) - } - // Fix permissions if origMode != mode { // Preserve SGID bit if it was set due to inheritance. @@ -151,97 +144,17 @@ func (n *Node) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.En if err != nil { tlog.Warn.Printf("Mkdir %q: Fchmod %#o -> %#o failed: %v", cName, mode, origMode, err) } - } - // Create child node & return - ch := n.newChild(ctx, &st, out) - return ch, 0 -} - -// Readdir - FUSE call. -// -// This function is symlink-safe through use of openBackingDir() and -// ReadDirIVAt(). -func (n *Node) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { - parentDirFd, cDirName, errno := n.prepareAtSyscallMyself() - if errno != 0 { - return nil, errno - } - defer syscall.Close(parentDirFd) - - // Read ciphertext directory - fd, err := syscallcompat.Openat(parentDirFd, cDirName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) - if err != nil { - return nil, fs.ToErrno(err) - } - defer syscall.Close(fd) - cipherEntries, specialEntries, err := syscallcompat.GetdentsSpecial(fd) + err = syscall.Fstat(fd, &st) if err != nil { + tlog.Warn.Printf("Mkdir %q: Fstat failed: %v", cName, err) return nil, fs.ToErrno(err) } - // Get DirIV (stays nil if PlaintextNames is used) - var cachedIV []byte - rn := n.rootNode() - if !rn.args.PlaintextNames { - // Read the DirIV from disk - cachedIV, err = rn.nameTransform.ReadDirIVAt(fd) - if err != nil { - tlog.Warn.Printf("OpenDir %q: could not read %s: %v", cDirName, nametransform.DirIVFilename, err) - return nil, syscall.EIO - } - } - // Decrypted directory entries - var plain []fuse.DirEntry - // Add "." and ".." - plain = append(plain, specialEntries...) - // Filter and decrypt filenames - for i := range cipherEntries { - cName := cipherEntries[i].Name - if n.IsRoot() && cName == configfile.ConfDefaultName { - // silently ignore "gocryptfs.conf" in the top level dir - continue - } - if rn.args.PlaintextNames { - plain = append(plain, cipherEntries[i]) - continue - } - if !rn.args.DeterministicNames && cName == nametransform.DirIVFilename { - // silently ignore "gocryptfs.diriv" everywhere if dirIV is enabled - continue - } - // Handle long file name - isLong := nametransform.LongNameNone - if rn.args.LongNames { - isLong = nametransform.NameType(cName) - } - if isLong == nametransform.LongNameContent { - cNameLong, err := nametransform.ReadLongNameAt(fd, cName) - if err != nil { - tlog.Warn.Printf("OpenDir %q: invalid entry %q: Could not read .name: %v", - cDirName, cName, err) - rn.reportMitigatedCorruption(cName) - continue - } - cName = cNameLong - } else if isLong == nametransform.LongNameFilename { - // ignore "gocryptfs.longname.*.name" - continue - } - name, err := rn.nameTransform.DecryptName(cName, cachedIV) - if err != nil { - tlog.Warn.Printf("OpenDir %q: invalid entry %q: %v", - cDirName, cName, err) - rn.reportMitigatedCorruption(cName) - continue - } - // Override the ciphertext name with the plaintext name but reuse the rest - // of the structure - cipherEntries[i].Name = name - plain = append(plain, cipherEntries[i]) - } - return fs.NewListDirStream(plain), 0 + // Create child node & return + ch := n.newChild(ctx, &st, out) + return ch, 0 } // Rmdir - FUSE call. diff --git a/internal/fusefrontend/node_helpers.go b/internal/fusefrontend/node_helpers.go index f5dfeb6..e8fca80 100644 --- a/internal/fusefrontend/node_helpers.go +++ b/internal/fusefrontend/node_helpers.go @@ -2,7 +2,6 @@ package fusefrontend import ( "context" - "sync/atomic" "syscall" "github.com/hanwen/go-fuse/v2/fs" @@ -91,12 +90,12 @@ func (n *Node) newChild(ctx context.Context, st *syscall.Stat_t, out *fuse.Entry if rn.args.SharedStorage || rn.quirks&syscallcompat.QuirkDuplicateIno1 != 0 { // Make each directory entry a unique node by using a unique generation // value - see the comment at RootNode.gen for details. - gen = atomic.AddUint64(&rn.gen, 1) + gen = rn.gen.Add(1) } // Create child node id := fs.StableAttr{ - Mode: uint32(st.Mode), + Mode: uint32(st.Mode), // go-fuse masks this with syscall.S_IFMT Gen: gen, Ino: st.Ino, } diff --git a/internal/fusefrontend/node_open_create.go b/internal/fusefrontend/node_open_create.go index 9598559..622d5dc 100644 --- a/internal/fusefrontend/node_open_create.go +++ b/internal/fusefrontend/node_open_create.go @@ -2,6 +2,7 @@ package fusefrontend import ( "context" + "os" "syscall" "github.com/hanwen/go-fuse/v2/fs" @@ -12,6 +13,30 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/tlog" ) +// mangleOpenCreateFlags is used by Create() and Open() to convert the open flags the user +// wants to the flags we internally use to open the backing file using Openat(). +// The returned flags always contain O_NOFOLLOW/O_SYMLINK. +func mangleOpenCreateFlags(flags uint32) (newFlags int) { + newFlags = int(flags) + // Convert WRONLY to RDWR. We always need read access to do read-modify-write cycles. + if (newFlags & syscall.O_ACCMODE) == syscall.O_WRONLY { + newFlags = newFlags ^ os.O_WRONLY | os.O_RDWR + } + // We also cannot open the file in append mode, we need to seek back for RMW + newFlags = newFlags &^ os.O_APPEND + // O_DIRECT accesses must be aligned in both offset and length. Due to our + // crypto header, alignment will be off, even if userspace makes aligned + // accesses. Running xfstests generic/013 on ext4 used to trigger lots of + // EINVAL errors due to missing alignment. Just fall back to buffered IO. + newFlags = newFlags &^ syscallcompat.O_DIRECT + // Create and Open are two separate FUSE operations, so O_CREAT should usually not + // be part of the Open() flags. Create() will add O_CREAT back itself. + newFlags = newFlags &^ syscall.O_CREAT + // We always want O_NOFOLLOW/O_SYMLINK to be safe against symlink races + newFlags |= syscallcompat.OpenatFlagNofollowSymlink + return newFlags +} + // Open - FUSE call. Open already-existing file. // // Symlink-safe through Openat(). @@ -23,7 +48,7 @@ func (n *Node) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFl defer syscall.Close(dirfd) rn := n.rootNode() - newFlags := rn.mangleOpenFlags(flags) + newFlags := mangleOpenCreateFlags(flags) // Taking this lock makes sure we don't race openWriteOnlyFile() rn.openWriteOnlyLock.RLock() defer rn.openWriteOnlyLock.RUnlock() @@ -71,7 +96,7 @@ func (n *Node) Create(ctx context.Context, name string, flags uint32, mode uint3 if !rn.args.PreserveOwner { ctx = nil } - newFlags := rn.mangleOpenFlags(flags) + newFlags := mangleOpenCreateFlags(flags) // Handle long file name ctx2 := toFuseCtx(ctx) if !rn.args.PlaintextNames && nametransform.IsLongContent(cName) { diff --git a/internal/fusefrontend/node_prepare_syscall.go b/internal/fusefrontend/node_prepare_syscall.go index 2a4d6ab..9021350 100644 --- a/internal/fusefrontend/node_prepare_syscall.go +++ b/internal/fusefrontend/node_prepare_syscall.go @@ -1,7 +1,6 @@ package fusefrontend import ( - "sync/atomic" "syscall" "github.com/rfjakob/gocryptfs/v2/internal/tlog" @@ -24,7 +23,7 @@ func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno sy // All filesystem operations go through here, so this is a good place // to reset the idle marker. - atomic.StoreUint32(&rn.IsIdle, 0) + rn.IsIdle.Store(false) if n.IsRoot() && rn.isFiltered(child) { return -1, "", syscall.EPERM diff --git a/internal/fusefrontend/node_xattr.go b/internal/fusefrontend/node_xattr.go index 44bc502..1470a2a 100644 --- a/internal/fusefrontend/node_xattr.go +++ b/internal/fusefrontend/node_xattr.go @@ -12,9 +12,6 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/tlog" ) -// -1 as uint32 -const minus1 = ^uint32(0) - // We store encrypted xattrs under this prefix plus the base64-encoded // encrypted original name. var xattrStorePrefix = "user.gocryptfs." @@ -35,6 +32,10 @@ func isAcl(attr string) bool { // This function is symlink-safe through Fgetxattr. func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, syscall.Errno) { rn := n.rootNode() + // If -noxattr is enabled, return ENOATTR for all getxattr calls + if rn.args.NoXattr { + return 0, noSuchAttributeError + } // If we are not mounted with -suid, reading the capability xattr does not // make a lot of sense, so reject the request and gain a massive speedup. // See https://github.com/rfjakob/gocryptfs/issues/515 . @@ -50,13 +51,13 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, var errno syscall.Errno data, errno = n.getXAttr(attr) if errno != 0 { - return minus1, errno + return 0, errno } } else { // encrypted user xattr cAttr, err := rn.encryptXattrName(attr) if err != nil { - return minus1, syscall.EIO + return 0, syscall.EIO } cData, errno := n.getXAttr(cAttr) if errno != 0 { @@ -65,15 +66,11 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, data, err = rn.decryptXattrValue(cData) if err != nil { tlog.Warn.Printf("GetXAttr: %v", err) - return minus1, syscall.EIO + return 0, syscall.EIO } } - // Caller passes size zero to find out how large their buffer should be - if len(dest) == 0 { - return uint32(len(data)), 0 - } if len(dest) < len(data) { - return minus1, syscall.ERANGE + return uint32(len(data)), syscall.ERANGE } l := copy(dest, data) return uint32(l), 0 @@ -84,6 +81,10 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, // This function is symlink-safe through Fsetxattr. func (n *Node) Setxattr(ctx context.Context, attr string, data []byte, flags uint32) syscall.Errno { rn := n.rootNode() + // If -noxattr is enabled, fail all setxattr calls + if rn.args.NoXattr { + return syscall.EOPNOTSUPP + } flags = uint32(filterXattrSetFlags(int(flags))) // ACLs are passed through without encryption @@ -109,6 +110,10 @@ func (n *Node) Setxattr(ctx context.Context, attr string, data []byte, flags uin // This function is symlink-safe through Fremovexattr. func (n *Node) Removexattr(ctx context.Context, attr string) syscall.Errno { rn := n.rootNode() + // If -noxattr is enabled, fail all removexattr calls + if rn.args.NoXattr { + return syscall.EOPNOTSUPP + } // ACLs are passed through without encryption if isAcl(attr) { @@ -126,11 +131,15 @@ func (n *Node) Removexattr(ctx context.Context, attr string) syscall.Errno { // // This function is symlink-safe through Flistxattr. func (n *Node) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) { + rn := n.rootNode() + // If -noxattr is enabled, return zero results for listxattr + if rn.args.NoXattr { + return 0, 0 + } cNames, errno := n.listXAttr() if errno != 0 { return 0, errno } - rn := n.rootNode() var buf bytes.Buffer for _, curName := range cNames { // ACLs are passed through without encryption @@ -155,12 +164,8 @@ func (n *Node) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errn } buf.WriteString(name + "\000") } - // Caller passes size zero to find out how large their buffer should be - if len(dest) == 0 { - return uint32(buf.Len()), 0 - } if buf.Len() > len(dest) { - return minus1, syscall.ERANGE + return uint32(buf.Len()), syscall.ERANGE } return uint32(copy(dest, buf.Bytes())), 0 } diff --git a/internal/fusefrontend/node_xattr_darwin.go b/internal/fusefrontend/node_xattr_darwin.go index a539847..1d25f3d 100644 --- a/internal/fusefrontend/node_xattr_darwin.go +++ b/internal/fusefrontend/node_xattr_darwin.go @@ -11,6 +11,9 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" ) +// On Darwin, ENOATTR is returned when an attribute is not found. +const noSuchAttributeError = syscall.ENOATTR + // On Darwin we have to unset XATTR_NOSECURITY 0x0008 func filterXattrSetFlags(flags int) int { // See https://opensource.apple.com/source/xnu/xnu-1504.15.3/bsd/sys/xattr.h.auto.html @@ -26,8 +29,8 @@ func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) { } defer syscall.Close(dirfd) - // O_NONBLOCK to not block on FIFOs. - fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + // O_NONBLOCK to not block on FIFOs, O_SYMLINK to open the symlink itself (if it is one). + fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) if err != nil { return nil, fs.ToErrno(err) } @@ -49,10 +52,10 @@ func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags defer syscall.Close(dirfd) // O_NONBLOCK to not block on FIFOs. - fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) // Directories cannot be opened read-write. Retry. if err == syscall.EISDIR { - fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) } if err != nil { fs.ToErrno(err) @@ -71,10 +74,10 @@ func (n *Node) removeXAttr(cAttr string) (errno syscall.Errno) { defer syscall.Close(dirfd) // O_NONBLOCK to not block on FIFOs. - fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) // Directories cannot be opened read-write. Retry. if err == syscall.EISDIR { - fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) } if err != nil { return fs.ToErrno(err) @@ -93,7 +96,7 @@ func (n *Node) listXAttr() (out []string, errno syscall.Errno) { defer syscall.Close(dirfd) // O_NONBLOCK to not block on FIFOs. - fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) if err != nil { return nil, fs.ToErrno(err) } diff --git a/internal/fusefrontend/node_xattr_linux.go b/internal/fusefrontend/node_xattr_linux.go index 4a356a5..9964212 100644 --- a/internal/fusefrontend/node_xattr_linux.go +++ b/internal/fusefrontend/node_xattr_linux.go @@ -12,6 +12,9 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" ) +// On Linux, ENODATA is returned when an attribute is not found. +const noSuchAttributeError = syscall.ENODATA + func filterXattrSetFlags(flags int) int { return flags } diff --git a/internal/fusefrontend/prepare_syscall_test.go b/internal/fusefrontend/prepare_syscall_test.go index acddaf3..e2c7d08 100644 --- a/internal/fusefrontend/prepare_syscall_test.go +++ b/internal/fusefrontend/prepare_syscall_test.go @@ -1,6 +1,7 @@ package fusefrontend import ( + "context" "strings" "syscall" "testing" @@ -22,13 +23,13 @@ func TestPrepareAtSyscall(t *testing.T) { rn := newTestFS(args) out := &fuse.EntryOut{} - child, errno := rn.Mkdir(nil, "dir1", 0700, out) + child, errno := rn.Mkdir(context.TODO(), "dir1", 0700, out) if errno != 0 { t.Fatal(errno) } rn.AddChild("dir1", child, false) dir1 := toNode(child.Operations()) - _, errno = dir1.Mkdir(nil, "dir2", 0700, out) + _, errno = dir1.Mkdir(context.TODO(), "dir2", 0700, out) if errno != 0 { t.Fatal(errno) } @@ -43,7 +44,7 @@ func TestPrepareAtSyscall(t *testing.T) { syscall.Close(dirfd) // Again, but populate the cache for "" by looking up a non-existing file - rn.Lookup(nil, "xyz1234", &fuse.EntryOut{}) + rn.Lookup(context.TODO(), "xyz1234", &fuse.EntryOut{}) dirfd, cName, errno = rn.prepareAtSyscallMyself() if errno != 0 { t.Fatal(errno) @@ -89,7 +90,7 @@ func TestPrepareAtSyscall(t *testing.T) { syscall.Close(dirfd) n255 := strings.Repeat("n", 255) - dir1.Mkdir(nil, n255, 0700, out) + dir1.Mkdir(context.TODO(), n255, 0700, out) dirfd, cName, errno = dir1.prepareAtSyscall(n255) if errno != 0 { t.Fatal(errno) @@ -116,13 +117,13 @@ func TestPrepareAtSyscallPlaintextnames(t *testing.T) { rn := newTestFS(args) out := &fuse.EntryOut{} - child, errno := rn.Mkdir(nil, "dir1", 0700, out) + child, errno := rn.Mkdir(context.TODO(), "dir1", 0700, out) if errno != 0 { t.Fatal(errno) } rn.AddChild("dir1", child, false) dir1 := toNode(child.Operations()) - _, errno = dir1.Mkdir(nil, "dir2", 0700, out) + _, errno = dir1.Mkdir(context.TODO(), "dir2", 0700, out) if errno != 0 { t.Fatal(errno) } diff --git a/internal/fusefrontend/root_node.go b/internal/fusefrontend/root_node.go index ac814ad..38d070d 100644 --- a/internal/fusefrontend/root_node.go +++ b/internal/fusefrontend/root_node.go @@ -1,9 +1,9 @@ package fusefrontend import ( - "os" "strings" "sync" + "sync/atomic" "syscall" "time" @@ -44,7 +44,7 @@ type RootNode struct { // (uint32 so that it can be reset with CompareAndSwapUint32). // When -idle was used when mounting, idleMonitor() sets it to 1 // periodically. - IsIdle uint32 + IsIdle atomic.Bool // dirCache caches directory fds dirCache dirCache // inoMap translates inode numbers from different devices to unique inode @@ -55,7 +55,7 @@ type RootNode struct { // This makes each directory entry unique (even hard links), // makes go-fuse hand out separate FUSE Node IDs for each, and prevents // bizarre problems when inode numbers are reused behind our back. - gen uint64 + gen atomic.Uint64 // quirks is a bitmap that enables workaround for quirks in the filesystem // backing the cipherdir quirks uint64 @@ -90,6 +90,12 @@ func NewRootNode(args Args, c *contentenc.ContentEnc, n *nametransform.NameTrans dirCache: dirCache{ivLen: ivLen}, quirks: syscallcompat.DetectQuirks(args.Cipherdir), } + // Suppress the message if the user has already specified -noprealloc + if rn.quirks&syscallcompat.QuirkBtrfsBrokenFalloc != 0 && !args.NoPrealloc { + syscallcompat.LogQuirk("Btrfs detected, forcing -noprealloc. " + + "Use \"chattr +C\" on the backing directory to enable NOCOW and allow preallocation. " + + "See https://github.com/rfjakob/gocryptfs/issues/395 for details.") + } if statErr == nil { rn.inoMap.TranslateStat(&st) rn.rootIno = st.Ino @@ -103,30 +109,6 @@ func (rn *RootNode) AfterUnmount() { rn.dirCache.stats() } -// mangleOpenFlags is used by Create() and Open() to convert the open flags the user -// wants to the flags we internally use to open the backing file. -// The returned flags always contain O_NOFOLLOW. -func (rn *RootNode) mangleOpenFlags(flags uint32) (newFlags int) { - newFlags = int(flags) - // Convert WRONLY to RDWR. We always need read access to do read-modify-write cycles. - if (newFlags & syscall.O_ACCMODE) == syscall.O_WRONLY { - newFlags = newFlags ^ os.O_WRONLY | os.O_RDWR - } - // We also cannot open the file in append mode, we need to seek back for RMW - newFlags = newFlags &^ os.O_APPEND - // O_DIRECT accesses must be aligned in both offset and length. Due to our - // crypto header, alignment will be off, even if userspace makes aligned - // accesses. Running xfstests generic/013 on ext4 used to trigger lots of - // EINVAL errors due to missing alignment. Just fall back to buffered IO. - newFlags = newFlags &^ syscallcompat.O_DIRECT - // Create and Open are two separate FUSE operations, so O_CREAT should not - // be part of the open flags. - newFlags = newFlags &^ syscall.O_CREAT - // We always want O_NOFOLLOW to be safe against symlink races - newFlags |= syscall.O_NOFOLLOW - return newFlags -} - // reportMitigatedCorruption is used to report a corruption that was transparently // mitigated and did not return an error to the user. Pass the name of the corrupt // item (filename for OpenDir(), xattr name for ListXAttr() etc). diff --git a/internal/fusefrontend/xattr_unit_test.go b/internal/fusefrontend/xattr_unit_test.go index 86c87a7..7d8e32e 100644 --- a/internal/fusefrontend/xattr_unit_test.go +++ b/internal/fusefrontend/xattr_unit_test.go @@ -21,10 +21,10 @@ func newTestFS(args Args) *RootNode { cEnc := contentenc.New(cCore, contentenc.DefaultBS) n := nametransform.New(cCore.EMECipher, true, 0, true, nil, false) rn := NewRootNode(args, cEnc, n) - oneSec := time.Second + oneSecond := time.Second options := &fs.Options{ - EntryTimeout: &oneSec, - AttrTimeout: &oneSec, + EntryTimeout: &oneSecond, + AttrTimeout: &oneSecond, } fs.NewNodeFS(rn, options) return rn diff --git a/internal/fusefrontend_reverse/excluder.go b/internal/fusefrontend_reverse/excluder.go index 0faadfa..1cb4b80 100644 --- a/internal/fusefrontend_reverse/excluder.go +++ b/internal/fusefrontend_reverse/excluder.go @@ -1,7 +1,6 @@ package fusefrontend_reverse import ( - "io/ioutil" "log" "os" "strings" @@ -50,7 +49,7 @@ func getExclusionPatterns(args fusefrontend.Args) []string { // getLines reads a file and splits it into lines func getLines(file string) ([]string, error) { - buffer, err := ioutil.ReadFile(file) + buffer, err := os.ReadFile(file) if err != nil { return nil, err } diff --git a/internal/fusefrontend_reverse/excluder_test.go b/internal/fusefrontend_reverse/excluder_test.go index bb041ce..b44ddce 100644 --- a/internal/fusefrontend_reverse/excluder_test.go +++ b/internal/fusefrontend_reverse/excluder_test.go @@ -1,7 +1,6 @@ package fusefrontend_reverse import ( - "io/ioutil" "os" "reflect" "testing" @@ -23,7 +22,7 @@ func TestShouldPrefixExcludeValuesWithSlash(t *testing.T) { } func TestShouldReadExcludePatternsFromFiles(t *testing.T) { - tmpfile1, err := ioutil.TempFile("", "excludetest") + tmpfile1, err := os.CreateTemp("", "excludetest") if err != nil { t.Fatal(err) } @@ -31,7 +30,7 @@ func TestShouldReadExcludePatternsFromFiles(t *testing.T) { defer os.Remove(exclude1) defer tmpfile1.Close() - tmpfile2, err := ioutil.TempFile("", "excludetest") + tmpfile2, err := os.CreateTemp("", "excludetest") if err != nil { t.Fatal(err) } diff --git a/internal/fusefrontend_reverse/node_api_check.go b/internal/fusefrontend_reverse/node_api_check.go index f8ec9ce..eb608f9 100644 --- a/internal/fusefrontend_reverse/node_api_check.go +++ b/internal/fusefrontend_reverse/node_api_check.go @@ -11,14 +11,8 @@ var _ = (fs.NodeReaddirer)((*Node)(nil)) var _ = (fs.NodeReadlinker)((*Node)(nil)) var _ = (fs.NodeOpener)((*Node)(nil)) var _ = (fs.NodeStatfser)((*Node)(nil)) - -/* -TODO but low prio. reverse mode in gocryptfs v1 did not have xattr support -either. - var _ = (fs.NodeGetxattrer)((*Node)(nil)) var _ = (fs.NodeListxattrer)((*Node)(nil)) -*/ /* Not needed var _ = (fs.NodeOpendirer)((*Node)(nil)) diff --git a/internal/fusefrontend_reverse/node_helpers.go b/internal/fusefrontend_reverse/node_helpers.go index 30361bc..f733689 100644 --- a/internal/fusefrontend_reverse/node_helpers.go +++ b/internal/fusefrontend_reverse/node_helpers.go @@ -24,7 +24,6 @@ const ( // * base64(192 bytes) = 256 bytes (over 255!) // But the PKCS#7 padding is at least one byte. This means we can only use // 175 bytes for the file name. - shortNameMax = 175 ) // translateSize translates the ciphertext size in `out` into plaintext size. @@ -135,7 +134,7 @@ func (n *Node) lookupLongnameName(ctx context.Context, nameFile string, out *fus if errno != 0 { return } - if rn.isExcludedPlain(filepath.Join(d.cPath, pName)) { + if rn.isExcludedPlain(filepath.Join(d.pPath, pName)) { errno = syscall.EPERM return } @@ -175,7 +174,7 @@ func (n *Node) lookupDiriv(ctx context.Context, out *fuse.EntryOut) (ch *fs.Inod errno = fs.ToErrno(err) return } - content := pathiv.Derive(d.cPath, pathiv.PurposeDirIV) + content := rn.deriveDirIV(d.cPath) var vf *VirtualMemNode vf, errno = n.newVirtualMemNode(content, st, inoTagDirIV) if errno != 0 { diff --git a/internal/fusefrontend_reverse/node_xattr.go b/internal/fusefrontend_reverse/node_xattr.go new file mode 100644 index 0000000..b940339 --- /dev/null +++ b/internal/fusefrontend_reverse/node_xattr.go @@ -0,0 +1,89 @@ +// Package fusefrontend_reverse interfaces directly with the go-fuse library. +package fusefrontend_reverse + +import ( + "bytes" + "context" + "syscall" + + "github.com/rfjakob/gocryptfs/v2/internal/pathiv" +) + +// We store encrypted xattrs under this prefix plus the base64-encoded +// encrypted original name. +var xattrStorePrefix = "user.gocryptfs." + +// isAcl returns true if the attribute name is for storing ACLs +// +// ACLs are passed through without encryption +func isAcl(attr string) bool { + return attr == "system.posix_acl_access" || attr == "system.posix_acl_default" +} + +// GetXAttr - FUSE call. Reads the value of extended attribute "attr". +// +// This function is symlink-safe through Fgetxattr. +func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, syscall.Errno) { + rn := n.rootNode() + // If -noxattr is enabled, return ENOATTR for all getxattr calls + if rn.args.NoXattr { + return 0, noSuchAttributeError + } + var data []byte + // ACLs are passed through without encryption + if isAcl(attr) { + var errno syscall.Errno + data, errno = n.getXAttr(attr) + if errno != 0 { + return 0, errno + } + } else { + pAttr, err := rn.decryptXattrName(attr) + if err != nil { + return 0, noSuchAttributeError + } + pData, errno := n.getXAttr(pAttr) + if errno != 0 { + return 0, errno + } + nonce := pathiv.Derive(n.Path()+"\000"+attr, pathiv.PurposeXattrIV) + data = rn.encryptXattrValue(pData, nonce) + } + if len(dest) < len(data) { + return uint32(len(data)), syscall.ERANGE + } + l := copy(dest, data) + return uint32(l), 0 +} + +// ListXAttr - FUSE call. Lists extended attributes on the file at "relPath". +// +// This function is symlink-safe through Flistxattr. +func (n *Node) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) { + rn := n.rootNode() + // If -noxattr is enabled, return zero results for listxattr + if rn.args.NoXattr { + return 0, 0 + } + pNames, errno := n.listXAttr() + if errno != 0 { + return 0, errno + } + var buf bytes.Buffer + for _, pName := range pNames { + // ACLs are passed through without encryption + if isAcl(pName) { + buf.WriteString(pName + "\000") + continue + } + cName, err := rn.encryptXattrName(pName) + if err != nil { + continue + } + buf.WriteString(cName + "\000") + } + if buf.Len() > len(dest) { + return uint32(buf.Len()), syscall.ERANGE + } + return uint32(copy(dest, buf.Bytes())), 0 +} diff --git a/internal/fusefrontend_reverse/node_xattr_darwin.go b/internal/fusefrontend_reverse/node_xattr_darwin.go new file mode 100644 index 0000000..6816a18 --- /dev/null +++ b/internal/fusefrontend_reverse/node_xattr_darwin.go @@ -0,0 +1,55 @@ +package fusefrontend_reverse + +import ( + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" +) + +// On Darwin, ENOATTR is returned when an attribute is not found. +const noSuchAttributeError = syscall.ENOATTR + +func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) { + d, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(d.dirfd) + + // O_NONBLOCK to not block on FIFOs. + fd, err := syscallcompat.Openat(d.dirfd, d.pName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + if err != nil { + return nil, fs.ToErrno(err) + } + defer syscall.Close(fd) + + cData, err := syscallcompat.Fgetxattr(fd, cAttr) + if err != nil { + return nil, fs.ToErrno(err) + } + + return cData, 0 +} + +func (n *Node) listXAttr() (out []string, errno syscall.Errno) { + d, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(d.dirfd) + + // O_NONBLOCK to not block on FIFOs. + fd, err := syscallcompat.Openat(d.dirfd, d.pName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + if err != nil { + return nil, fs.ToErrno(err) + } + defer syscall.Close(fd) + + pNames, err := syscallcompat.Flistxattr(fd) + if err != nil { + return nil, fs.ToErrno(err) + } + return pNames, 0 +} diff --git a/internal/fusefrontend_reverse/node_xattr_linux.go b/internal/fusefrontend_reverse/node_xattr_linux.go new file mode 100644 index 0000000..3c574f5 --- /dev/null +++ b/internal/fusefrontend_reverse/node_xattr_linux.go @@ -0,0 +1,43 @@ +package fusefrontend_reverse + +import ( + "fmt" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" +) + +// On Linux, ENODATA is returned when an attribute is not found. +const noSuchAttributeError = syscall.ENODATA + +func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) { + d, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(d.dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", d.dirfd, d.pName) + pData, err := syscallcompat.Lgetxattr(procPath, cAttr) + if err != nil { + return nil, fs.ToErrno(err) + } + return pData, 0 +} + +func (n *Node) listXAttr() (out []string, errno syscall.Errno) { + d, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(d.dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", d.dirfd, d.pName) + pNames, err := syscallcompat.Llistxattr(procPath) + if err != nil { + return nil, fs.ToErrno(err) + } + return pNames, 0 +} diff --git a/internal/fusefrontend_reverse/root_node.go b/internal/fusefrontend_reverse/root_node.go index cb04151..1a668af 100644 --- a/internal/fusefrontend_reverse/root_node.go +++ b/internal/fusefrontend_reverse/root_node.go @@ -13,6 +13,7 @@ import ( "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" + "github.com/rfjakob/gocryptfs/v2/internal/configfile" "github.com/rfjakob/gocryptfs/v2/internal/contentenc" "github.com/rfjakob/gocryptfs/v2/internal/exitcodes" "github.com/rfjakob/gocryptfs/v2/internal/fusefrontend" @@ -21,7 +22,7 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" "github.com/rfjakob/gocryptfs/v2/internal/tlog" - "github.com/sabhiram/go-gitignore" + ignore "github.com/sabhiram/go-gitignore" ) // RootNode is the root directory in a `gocryptfs -reverse` mount @@ -50,7 +51,7 @@ type RootNode struct { // makes go-fuse hand out separate FUSE Node IDs for each, and prevents // bizarre problems when inode numbers are reused behind our back, // like this one: https://github.com/rfjakob/gocryptfs/issues/802 - gen uint64 + gen atomic.Uint64 // rootIno is the inode number that we report for the root node on mount rootIno uint64 } @@ -136,7 +137,8 @@ func (rn *RootNode) findLongnameParent(fd int, diriv []byte, longname string) (p // excluded (used when -exclude is passed by the user). func (rn *RootNode) isExcludedPlain(pPath string) bool { // root dir can't be excluded - if pPath == "" { + // Don't exclude gocryptfs.conf too + if pPath == "" || pPath == configfile.ConfReverseName { return false } return rn.excluder != nil && rn.excluder.MatchesPath(pPath) @@ -175,10 +177,45 @@ func (rn *RootNode) uniqueStableAttr(mode uint32, ino uint64) fs.StableAttr { Ino: ino, // Make each directory entry a unique node by using a unique generation // value. Also see the comment at RootNode.gen for details. - Gen: atomic.AddUint64(&rn.gen, 1), + Gen: rn.gen.Add(1), } } func (rn *RootNode) RootIno() uint64 { return rn.rootIno } + +// encryptXattrValue encrypts the xattr value "data". +// The data is encrypted like a file content block, but without binding it to +// a file location (block number and file id are set to zero). +// Special case: an empty value is encrypted to an empty value. +func (rn *RootNode) encryptXattrValue(data []byte, nonce []byte) (cData []byte) { + if len(data) == 0 { + return []byte{} + } + return rn.contentEnc.EncryptBlockNonce(data, 0, nil, nonce) +} + +// encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf" +func (rn *RootNode) encryptXattrName(attr string) (string, error) { + // xattr names are encrypted like file names, but with a fixed IV. + cAttr, err := rn.nameTransform.EncryptXattrName(attr) + if err != nil { + return "", err + } + return xattrStorePrefix + cAttr, nil +} + +func (rn *RootNode) decryptXattrName(cAttr string) (attr string, err error) { + // Reject anything that does not start with "user.gocryptfs." + if !strings.HasPrefix(cAttr, xattrStorePrefix) { + return "", syscall.EINVAL + } + // Strip "user.gocryptfs." prefix + cAttr = cAttr[len(xattrStorePrefix):] + attr, err = rn.nameTransform.DecryptXattrName(cAttr) + if err != nil { + return "", err + } + return attr, nil +} diff --git a/internal/fusefrontend_reverse/virtualnode.go b/internal/fusefrontend_reverse/virtualnode.go index 732564a..95e71ab 100644 --- a/internal/fusefrontend_reverse/virtualnode.go +++ b/internal/fusefrontend_reverse/virtualnode.go @@ -100,6 +100,15 @@ func (n *Node) newVirtualMemNode(content []byte, parentStat *syscall.Stat_t, ino st.Nlink = 1 var a fuse.Attr a.FromStat(st) + // With inode number reuse and hard links, we could have returned + // wrong data for gocryptfs.diriv and gocryptfs.xyz.longname files, respectively + // (https://github.com/rfjakob/gocryptfs/issues/802). + // + // Now that this is fixed, ensure that rsync and similar tools pick up the new + // correct files by advancing mtime and ctime by 10 seconds, which should be more + // than any filesytems' timestamp granularity (FAT32 has 2 seconds). + a.Mtime += 10 + a.Ctime += 10 if rn.args.ForceOwner != nil { a.Owner = *rn.args.ForceOwner } diff --git a/internal/inomap/inomap.go b/internal/inomap/inomap.go index b4dbf27..5749202 100644 --- a/internal/inomap/inomap.go +++ b/internal/inomap/inomap.go @@ -45,7 +45,7 @@ type InoMap struct { // spill is used once the namespaces map is full spillMap map[QIno]uint64 // spillNext is the next free inode number in the spill map - spillNext uint64 + spillNext atomic.Uint64 } // New returns a new InoMap. @@ -57,8 +57,9 @@ func New(rootDev uint64) *InoMap { namespaceMap: make(map[namespaceData]uint16), namespaceNext: 0, spillMap: make(map[QIno]uint64), - spillNext: spillSpaceStart, } + m.spillNext.Store(spillSpaceStart) + if rootDev > 0 { // Reserve namespace 0 for rootDev m.namespaceMap[namespaceData{rootDev, 0}] = 0 @@ -74,10 +75,10 @@ var spillWarn sync.Once // Reverse mode NextSpillIno() for gocryptfs.longname.*.name files where a stable // mapping is not needed. func (m *InoMap) NextSpillIno() (out uint64) { - if m.spillNext == math.MaxUint64 { - log.Panicf("spillMap overflow: spillNext = 0x%x", m.spillNext) + if m.spillNext.Load() == math.MaxUint64 { + log.Panicf("spillMap overflow: spillNext = 0x%x", m.spillNext.Load()) } - return atomic.AddUint64(&m.spillNext, 1) - 1 + return m.spillNext.Add(1) - 1 } func (m *InoMap) spill(in QIno) (out uint64) { diff --git a/internal/nametransform/diriv.go b/internal/nametransform/diriv.go index 7929c40..5dd4940 100644 --- a/internal/nametransform/diriv.go +++ b/internal/nametransform/diriv.go @@ -67,7 +67,7 @@ func fdReadDirIV(fd *os.File) (iv []byte, err error) { func WriteDirIVAt(dirfd int) error { iv := cryptocore.RandBytes(DirIVLen) // 0400 permissions: gocryptfs.diriv should never be modified after creation. - // Don't use "ioutil.WriteFile", it causes trouble on NFS: + // Don't use "os.WriteFile", it causes trouble on NFS: // https://github.com/rfjakob/gocryptfs/commit/7d38f80a78644c8ec4900cc990bfb894387112ed fd, err := syscallcompat.Openat(dirfd, DirIVFilename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, dirivPerms) if err != nil { diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index 3313a7c..0389c95 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -7,9 +7,12 @@ import ( "errors" "math" "path/filepath" + "runtime" "strings" "syscall" + "golang.org/x/text/unicode/norm" + "github.com/rfjakob/eme" "github.com/rfjakob/gocryptfs/v2/internal/tlog" @@ -32,6 +35,12 @@ type NameTransform struct { // Patterns to bypass decryption badnamePatterns []string deterministicNames bool + // Convert filenames to NFC before encrypting, + // and to NFD when decrypting. + // For MacOS compatibility. + // Automatically enabled on MacOS, off otherwise, + // except in tests (see nfc_test.go). + nfd2nfc bool } // New returns a new NameTransform instance. @@ -55,34 +64,44 @@ func New(e *eme.EMECipher, longNames bool, longNameMax uint8, raw64 bool, badnam effectiveLongNameMax = int(longNameMax) } } + nfd2nfc := runtime.GOOS == "darwin" + if nfd2nfc { + tlog.Info.Printf("Running on MacOS, enabling Unicode normalization") + } return &NameTransform{ emeCipher: e, longNameMax: effectiveLongNameMax, B64: b64, badnamePatterns: badname, deterministicNames: deterministicNames, + nfd2nfc: nfd2nfc, } } // DecryptName calls decryptName to try and decrypt a base64-encoded encrypted // filename "cipherName", and failing that checks if it can be bypassed -func (n *NameTransform) DecryptName(cipherName string, iv []byte) (string, error) { - res, err := n.decryptName(cipherName, iv) +func (n *NameTransform) DecryptName(cipherName string, iv []byte) (plainName string, err error) { + plainName, err = n.decryptName(cipherName, iv) if err != nil && n.HaveBadnamePatterns() { - res, err = n.decryptBadname(cipherName, iv) + plainName, err = n.decryptBadname(cipherName, iv) } if err != nil { return "", err } - if err := IsValidName(res); err != nil { + if err := IsValidName(plainName); err != nil { tlog.Warn.Printf("DecryptName %q: invalid name after decryption: %v", cipherName, err) return "", syscall.EBADMSG } - return res, err + if n.nfd2nfc { + // MacOS expects file names in NFD form. Present them as NFD. + // They are converted back to NFC in EncryptName. + plainName = norm.NFD.String(plainName) + } + return plainName, err } -// decryptName decrypts a base64-encoded encrypted filename "cipherName" using the -// initialization vector "iv". +// decryptName decrypts a base64-encoded encrypted file- or xattr-name "cipherName" +// using the initialization vector "iv". func (n *NameTransform) decryptName(cipherName string, iv []byte) (string, error) { // From https://pkg.go.dev/encoding/base64#Encoding.Strict : // > Note that the input is still malleable, as new line characters @@ -126,6 +145,16 @@ func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 s tlog.Warn.Printf("EncryptName %q: invalid plainName: %v", plainName, err) return "", syscall.EBADMSG } + if n.nfd2nfc { + // MacOS GUI apps expect Unicode in NFD form. + // But MacOS CLI apps, Linux and Windows use NFC form. + // We normalize to NFC for two reasons: + // 1) Make sharing gocryptfs filesystems from MacOS to other systems + // less painful + // 2) Enable DecryptName to normalize to NFD, which works for both + // GUI and CLI on MacOS. + plainName = norm.NFC.String(plainName) + } return n.encryptName(plainName, iv), nil } diff --git a/internal/nametransform/nfc_test.go b/internal/nametransform/nfc_test.go new file mode 100644 index 0000000..aad1d7d --- /dev/null +++ b/internal/nametransform/nfc_test.go @@ -0,0 +1,29 @@ +package nametransform + +import ( + "strconv" + "testing" + + "golang.org/x/text/unicode/norm" +) + +func TestNFD2NFC(t *testing.T) { + n := newLognamesTestInstance(NameMax) + n.nfd2nfc = true + iv := make([]byte, DirIVLen) + srcNFC := "Österreich Café" + srcNFD := norm.NFD.String(srcNFC) + + // cipherName should get normalized to NFC + cipherName, _ := n.EncryptName(srcNFD, iv) + // Decrypt without changing normalization + decryptedRaw, _ := n.decryptName(cipherName, iv) + if srcNFC != decryptedRaw { + t.Errorf("want %s have %s", strconv.QuoteToASCII(srcNFC), strconv.QuoteToASCII(decryptedRaw)) + } + // Decrypt with normalizing to NFD + decrypted, _ := n.DecryptName(cipherName, iv) + if srcNFD != decrypted { + t.Errorf("want %s have %s", strconv.QuoteToASCII(srcNFD), strconv.QuoteToASCII(decrypted)) + } +} diff --git a/internal/nametransform/pad16.go b/internal/nametransform/pad16.go index 833be0e..2c2466a 100644 --- a/internal/nametransform/pad16.go +++ b/internal/nametransform/pad16.go @@ -32,10 +32,10 @@ func pad16(orig []byte) (padded []byte) { func unPad16(padded []byte) ([]byte, error) { oldLen := len(padded) if oldLen == 0 { - return nil, errors.New("Empty input") + return nil, errors.New("empty input") } if oldLen%aes.BlockSize != 0 { - return nil, errors.New("Unaligned size") + return nil, errors.New("unaligned size") } // The last byte is always a padding byte padByte := padded[oldLen-1] @@ -43,20 +43,20 @@ func unPad16(padded []byte) ([]byte, error) { padLen := int(padByte) // Padding must be at least 1 byte if padLen == 0 { - return nil, errors.New("Padding cannot be zero-length") + return nil, errors.New("padding cannot be zero-length") } // Padding more than 16 bytes make no sense if padLen > aes.BlockSize { - return nil, fmt.Errorf("Padding too long, padLen=%d > 16", padLen) + return nil, fmt.Errorf("padding too long, padLen=%d > 16", padLen) } // Padding cannot be as long as (or longer than) the whole string, if padLen >= oldLen { - return nil, fmt.Errorf("Padding too long, oldLen=%d >= padLen=%d", oldLen, padLen) + return nil, fmt.Errorf("padding too long, oldLen=%d >= padLen=%d", oldLen, padLen) } // All padding bytes must be identical for i := oldLen - padLen; i < oldLen; i++ { if padded[i] != padByte { - return nil, fmt.Errorf("Padding byte at i=%d is invalid", i) + return nil, fmt.Errorf("padding byte at i=%d is invalid", i) } } newLen := oldLen - padLen diff --git a/internal/openfiletable/open_file_table.go b/internal/openfiletable/open_file_table.go index ce8df76..420d070 100644 --- a/internal/openfiletable/open_file_table.go +++ b/internal/openfiletable/open_file_table.go @@ -29,7 +29,7 @@ type table struct { // The variable is accessed without holding any locks so atomic operations // must be used. It must be the first element of the struct to guarantee // 64-bit alignment. - writeOpCount uint64 + writeOpCount atomic.Uint64 // Protects map access sync.Mutex // Table entries @@ -85,13 +85,13 @@ type countingMutex struct { func (c *countingMutex) Lock() { c.RWMutex.Lock() - atomic.AddUint64(&t.writeOpCount, 1) + t.writeOpCount.Add(1) } // WriteOpCount returns the write lock counter value. This value is incremented // each time writeLock.Lock() on a file table entry is called. func WriteOpCount() uint64 { - return atomic.LoadUint64(&t.writeOpCount) + return t.writeOpCount.Load() } // CountOpenFiles returns how many entries are currently in the table diff --git a/internal/pathiv/pathiv.go b/internal/pathiv/pathiv.go index 48f8426..11a7189 100644 --- a/internal/pathiv/pathiv.go +++ b/internal/pathiv/pathiv.go @@ -20,6 +20,8 @@ const ( PurposeSymlinkIV Purpose = "SYMLINKIV" // PurposeBlock0IV means the value will be used as the IV of ciphertext block #0. PurposeBlock0IV Purpose = "BLOCK0IV" + // PurposeXattrIV means the value will be used as a xattr IV + PurposeXattrIV Purpose = "XATTRIV" ) // Derive derives an IV from an encrypted path by hashing it with sha256 diff --git a/internal/readpassword/read.go b/internal/readpassword/read.go index 582a104..9193ae8 100644 --- a/internal/readpassword/read.go +++ b/internal/readpassword/read.go @@ -58,7 +58,7 @@ func Twice(extpass []string, passfile []string) ([]byte, error) { return nil, err } if !bytes.Equal(p1, p2) { - return nil, fmt.Errorf("Passwords do not match") + return nil, fmt.Errorf("passwords do not match") } // Wipe the password duplicate from memory for i := range p2 { @@ -71,15 +71,15 @@ func Twice(extpass []string, passfile []string) ([]byte, error) { // Exits on read error or empty result. func readPasswordTerminal(prompt string) ([]byte, error) { fd := int(os.Stdin.Fd()) - fmt.Fprintf(os.Stderr, prompt) + fmt.Fprint(os.Stderr, prompt) // term.ReadPassword removes the trailing newline p, err := term.ReadPassword(fd) if err != nil { - return nil, fmt.Errorf("Could not read password from terminal: %v\n", err) + return nil, fmt.Errorf("could not read password from terminal: %v", err) } fmt.Fprintf(os.Stderr, "\n") if len(p) == 0 { - return nil, fmt.Errorf("Password is empty") + return nil, fmt.Errorf("password is empty") } return p, nil } @@ -100,7 +100,7 @@ func readPasswordStdin(prompt string) ([]byte, error) { return nil, err } if len(p) == 0 { - return nil, fmt.Errorf("Got empty %s from stdin", prompt) + return nil, fmt.Errorf("got empty %s from stdin", prompt) } return p, nil } diff --git a/internal/siv_aead/correctness_test.go b/internal/siv_aead/correctness_test.go index 0653e26..7be97bc 100644 --- a/internal/siv_aead/correctness_test.go +++ b/internal/siv_aead/correctness_test.go @@ -50,7 +50,7 @@ func TestK32(t *testing.T) { expectedResult, _ := hex.DecodeString( "02020202020202020202020202020202ad7a4010649a84d8c1dd5f752e935eed57d45b8b10008f3834") if !bytes.Equal(aResult, expectedResult) { - t.Errorf(hex.EncodeToString(aResult)) + t.Error(hex.EncodeToString(aResult)) } // Verify overhead overhead := len(aResult) - len(plaintext) - len(nonce) @@ -108,7 +108,7 @@ func TestK64(t *testing.T) { expectedResult, _ := hex.DecodeString( "02020202020202020202020202020202317b316f67c3ad336c01c9a01b4c5e552ba89e966bc4c1ade1") if !bytes.Equal(aResult, expectedResult) { - t.Errorf(hex.EncodeToString(aResult)) + t.Error(hex.EncodeToString(aResult)) } // Verify overhead overhead := len(aResult) - len(plaintext) - len(nonce) diff --git a/internal/speed/cpuinfo.go b/internal/speed/cpuinfo.go index df3177d..636a4f0 100644 --- a/internal/speed/cpuinfo.go +++ b/internal/speed/cpuinfo.go @@ -1,7 +1,7 @@ package speed import ( - "io/ioutil" + "io" "os" "runtime" "strings" @@ -33,7 +33,7 @@ func cpuModelName() string { if err != nil { return "" } - content, err := ioutil.ReadAll(f) + content, err := io.ReadAll(f) if err != nil { return "" } diff --git a/internal/stupidgcm/chacha.go b/internal/stupidgcm/chacha.go index de0c2e8..c500ea5 100644 --- a/internal/stupidgcm/chacha.go +++ b/internal/stupidgcm/chacha.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl package stupidgcm diff --git a/internal/stupidgcm/chacha_test.go b/internal/stupidgcm/chacha_test.go index 542ff15..356c813 100644 --- a/internal/stupidgcm/chacha_test.go +++ b/internal/stupidgcm/chacha_test.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl package stupidgcm diff --git a/internal/stupidgcm/common.go b/internal/stupidgcm/common.go index d88dc62..03698b9 100644 --- a/internal/stupidgcm/common.go +++ b/internal/stupidgcm/common.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl package stupidgcm diff --git a/internal/stupidgcm/common_test.go b/internal/stupidgcm/common_test.go index 7f38e90..633f279 100644 --- a/internal/stupidgcm/common_test.go +++ b/internal/stupidgcm/common_test.go @@ -1,5 +1,4 @@ //go:build cgo && !without_openssl -// +build cgo,!without_openssl package stupidgcm diff --git a/internal/stupidgcm/gcm.go b/internal/stupidgcm/gcm.go index 2e5aac4..274d3df 100644 --- a/internal/stupidgcm/gcm.go +++ b/internal/stupidgcm/gcm.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl package stupidgcm diff --git a/internal/stupidgcm/gcm_test.go b/internal/stupidgcm/gcm_test.go index c730a87..041dcab 100644 --- a/internal/stupidgcm/gcm_test.go +++ b/internal/stupidgcm/gcm_test.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl // We compare against Go's built-in GCM implementation. Since stupidgcm only // supports 128-bit IVs and Go only supports that from 1.5 onward, we cannot diff --git a/internal/stupidgcm/locking.go b/internal/stupidgcm/locking.go index 04cf232..00cc361 100644 --- a/internal/stupidgcm/locking.go +++ b/internal/stupidgcm/locking.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl package stupidgcm diff --git a/internal/stupidgcm/openssl.go b/internal/stupidgcm/openssl.go index 8c950f8..3a72f9c 100644 --- a/internal/stupidgcm/openssl.go +++ b/internal/stupidgcm/openssl.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl package stupidgcm diff --git a/internal/stupidgcm/openssl_aead.c b/internal/stupidgcm/openssl_aead.c index e02466f..a7f4f59 100644 --- a/internal/stupidgcm/openssl_aead.c +++ b/internal/stupidgcm/openssl_aead.c @@ -1,4 +1,4 @@ -// +build !without_openssl +//go:build cgo && !without_openssl #include "openssl_aead.h" #include <openssl/evp.h> diff --git a/internal/stupidgcm/without_openssl.go b/internal/stupidgcm/without_openssl.go index fcef793..c59ebe6 100644 --- a/internal/stupidgcm/without_openssl.go +++ b/internal/stupidgcm/without_openssl.go @@ -1,5 +1,4 @@ -//go:build without_openssl -// +build without_openssl +//go:build !cgo || without_openssl package stupidgcm diff --git a/internal/stupidgcm/xchacha.go b/internal/stupidgcm/xchacha.go index 3c121ba..cfd69d7 100644 --- a/internal/stupidgcm/xchacha.go +++ b/internal/stupidgcm/xchacha.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style @@ -7,7 +6,7 @@ // // Copied from // https://github.com/golang/crypto/blob/32db794688a5a24a23a43f2a984cecd5b3d8da58/chacha20poly1305/xchacha20poly1305.go -// and adapted for stupidgcm by @rfjakob. +// and adapted to use OpenSSL ChaCha20 (see chacha.go) instaed of stdlib by @rfjakob. package stupidgcm diff --git a/internal/stupidgcm/xchacha_test.go b/internal/stupidgcm/xchacha_test.go index 676a023..a448f20 100644 --- a/internal/stupidgcm/xchacha_test.go +++ b/internal/stupidgcm/xchacha_test.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl package stupidgcm diff --git a/internal/syscallcompat/asuser.go b/internal/syscallcompat/asuser.go new file mode 100644 index 0000000..0c083ec --- /dev/null +++ b/internal/syscallcompat/asuser.go @@ -0,0 +1,59 @@ +package syscallcompat + +import ( + "golang.org/x/sys/unix" + + "github.com/hanwen/go-fuse/v2/fuse" +) + +// OpenatUser runs the Openat syscall in the context of a different user. +// It switches the current thread to the new user, performs the syscall, +// and switches back. +// +// If `context` is nil, this function behaves like ordinary Openat (no +// user switching). +func OpenatUser(dirfd int, path string, flags int, mode uint32, context *fuse.Context) (fd int, err error) { + f := func() (int, error) { + return Openat(dirfd, path, flags, mode) + } + return asUser(f, context) +} + +// MknodatUser runs the Mknodat syscall in the context of a different user. +// If `context` is nil, this function behaves like ordinary Mknodat. +// +// See OpenatUser() for how this works. +func MknodatUser(dirfd int, path string, mode uint32, dev int, context *fuse.Context) (err error) { + f := func() (int, error) { + err := Mknodat(dirfd, path, mode, dev) + return -1, err + } + _, err = asUser(f, context) + return err +} + +// SymlinkatUser runs the Symlinkat syscall in the context of a different user. +// If `context` is nil, this function behaves like ordinary Symlinkat. +// +// See OpenatUser() for how this works. +func SymlinkatUser(oldpath string, newdirfd int, newpath string, context *fuse.Context) (err error) { + f := func() (int, error) { + err := unix.Symlinkat(oldpath, newdirfd, newpath) + return -1, err + } + _, err = asUser(f, context) + return err +} + +// MkdiratUser runs the Mkdirat syscall in the context of a different user. +// If `context` is nil, this function behaves like ordinary Mkdirat. +// +// See OpenatUser() for how this works. +func MkdiratUser(dirfd int, path string, mode uint32, context *fuse.Context) (err error) { + f := func() (int, error) { + err := unix.Mkdirat(dirfd, path, mode) + return -1, err + } + _, err = asUser(f, context) + return err +} diff --git a/internal/syscallcompat/asuser_darwin.go b/internal/syscallcompat/asuser_darwin.go new file mode 100644 index 0000000..5da2782 --- /dev/null +++ b/internal/syscallcompat/asuser_darwin.go @@ -0,0 +1,46 @@ +package syscallcompat + +import ( + "runtime" + "syscall" + + "github.com/hanwen/go-fuse/v2/fuse" +) + +// asUser runs `f()` under the effective uid, gid, groups specified +// in `context`. +// +// If `context` is nil, `f()` is executed directly without switching user id. +func asUser(f func() (int, error), context *fuse.Context) (int, error) { + if context == nil { + return f() + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + err := pthread_setugid_np(context.Owner.Uid, context.Owner.Gid) + if err != nil { + return -1, err + } + + const ( + // KAUTH_UID_NONE and KAUTH_GID_NONE are special values to + // revert permissions to the process credentials. + KAUTH_UID_NONE = ^uint32(0) - 100 + KAUTH_GID_NONE = ^uint32(0) - 100 + ) + + defer pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE) + + return f() +} + +// Unfortunately pthread_setugid_np does not have a syscall wrapper yet. +func pthread_setugid_np(uid uint32, gid uint32) (err error) { + _, _, e1 := syscall.RawSyscall(syscall.SYS_SETTID, uintptr(uid), uintptr(gid), 0) + if e1 != 0 { + err = e1 + } + return +} diff --git a/internal/syscallcompat/asuser_linux.go b/internal/syscallcompat/asuser_linux.go new file mode 100644 index 0000000..39e3ff2 --- /dev/null +++ b/internal/syscallcompat/asuser_linux.go @@ -0,0 +1,80 @@ +package syscallcompat + +import ( + "fmt" + "os" + "runtime" + "strconv" + "strings" + + "github.com/hanwen/go-fuse/v2/fuse" +) + +// asUser runs `f()` under the effective uid, gid, groups specified +// in `context`. +// +// If `context` is nil, `f()` is executed directly without switching user id. +func asUser(f func() (int, error), context *fuse.Context) (int, error) { + if context == nil { + return f() + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + // Since go1.16beta1 (commit d1b1145cace8b968307f9311ff611e4bb810710c , + // https://go-review.googlesource.com/c/go/+/210639 ) + // syscall.{Setgroups,Setregid,Setreuid} affects all threads, which + // is exactly what we not want. + // + // And unix.{Setgroups,Setregid,Setreuid} also changed to this behavoir in + // v0.1.0 (commit d0df966e6959f00dc1c74363e537872647352d51 , + // https://go-review.googlesource.com/c/sys/+/428174 ), so we use + // our own syscall wrappers. + + err := Setgroups(getSupplementaryGroups(context.Pid)) + if err != nil { + return -1, err + } + defer SetgroupsPanic(nil) + + err = Setregid(-1, int(context.Owner.Gid)) + if err != nil { + return -1, err + } + defer SetregidPanic(-1, 0) + + err = Setreuid(-1, int(context.Owner.Uid)) + if err != nil { + return -1, err + } + defer SetreuidPanic(-1, 0) + + return f() +} + +func getSupplementaryGroups(pid uint32) (gids []int) { + procPath := fmt.Sprintf("/proc/%d/task/%d/status", pid, pid) + blob, err := os.ReadFile(procPath) + if err != nil { + return nil + } + + lines := strings.Split(string(blob), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "Groups:") { + f := strings.Fields(line[7:]) + gids = make([]int, len(f)) + for i := range gids { + val, err := strconv.ParseInt(f[i], 10, 32) + if err != nil { + return nil + } + gids[i] = int(val) + } + return gids + } + } + + return nil +} diff --git a/internal/syscallcompat/getdents_test.go b/internal/syscallcompat/getdents_test.go index eb670d6..c9e6a99 100644 --- a/internal/syscallcompat/getdents_test.go +++ b/internal/syscallcompat/getdents_test.go @@ -4,7 +4,6 @@ package syscallcompat import ( - "io/ioutil" "os" "runtime" "strings" @@ -49,13 +48,13 @@ func testGetdents(t *testing.T) { getdentsUnderTest = emulateGetdents } // Fill a directory with filenames of length 1 ... 255 - testDir, err := ioutil.TempDir(tmpDir, "TestGetdents") + testDir, err := os.MkdirTemp(tmpDir, "TestGetdents") if err != nil { t.Fatal(err) } for i := 1; i <= unix.NAME_MAX; i++ { n := strings.Repeat("x", i) - err = ioutil.WriteFile(testDir+"/"+n, nil, 0600) + err = os.WriteFile(testDir+"/"+n, nil, 0600) if err != nil { t.Fatal(err) } diff --git a/internal/syscallcompat/main_test.go b/internal/syscallcompat/main_test.go index ddf6bc4..7183f5a 100644 --- a/internal/syscallcompat/main_test.go +++ b/internal/syscallcompat/main_test.go @@ -2,7 +2,6 @@ package syscallcompat import ( "fmt" - "io/ioutil" "os" "testing" ) @@ -23,7 +22,7 @@ func TestMain(m *testing.M) { fmt.Println(err) os.Exit(1) } - tmpDir, err = ioutil.TempDir(parent, "syscallcompat") + tmpDir, err = os.MkdirTemp(parent, "syscallcompat") if err != nil { fmt.Println(err) os.Exit(1) diff --git a/internal/syscallcompat/quirks.go b/internal/syscallcompat/quirks.go index 858f16d..36bcb9f 100644 --- a/internal/syscallcompat/quirks.go +++ b/internal/syscallcompat/quirks.go @@ -5,18 +5,17 @@ import ( ) const ( - // QuirkBrokenFalloc means the falloc is broken. + // QuirkBtrfsBrokenFalloc means the falloc is broken. // Preallocation on Btrfs is broken ( https://github.com/rfjakob/gocryptfs/issues/395 ) // and slow ( https://github.com/rfjakob/gocryptfs/issues/63 ). - QuirkBrokenFalloc = uint64(1 << iota) + QuirkBtrfsBrokenFalloc = uint64(1 << iota) // QuirkDuplicateIno1 means that we have duplicate inode numbers. // On MacOS ExFAT, all empty files share inode number 1: // https://github.com/rfjakob/gocryptfs/issues/585 QuirkDuplicateIno1 - // QuirkNoUserXattr means that user.* xattrs are not supported - QuirkNoUserXattr ) -func logQuirk(s string) { - tlog.Info.Printf(tlog.ColorYellow + "DetectQuirks: " + s + tlog.ColorReset) +// LogQuirk prints a yellow message about a detected quirk. +func LogQuirk(s string) { + tlog.Info.Println(tlog.ColorYellow + "DetectQuirks: " + s + tlog.ColorReset) } diff --git a/internal/syscallcompat/quirks_darwin.go b/internal/syscallcompat/quirks_darwin.go index 4adeea1..c4d5006 100644 --- a/internal/syscallcompat/quirks_darwin.go +++ b/internal/syscallcompat/quirks_darwin.go @@ -33,7 +33,7 @@ func DetectQuirks(cipherdir string) (q uint64) { // On MacOS ExFAT, all empty files share inode number 1: // https://github.com/rfjakob/gocryptfs/issues/585 if fstypename == FstypenameExfat { - logQuirk("ExFAT detected, disabling hard links. See https://github.com/rfjakob/gocryptfs/issues/585 for why.") + LogQuirk("ExFAT detected, disabling hard links. See https://github.com/rfjakob/gocryptfs/issues/585 for why.") q |= QuirkDuplicateIno1 } diff --git a/internal/syscallcompat/quirks_linux.go b/internal/syscallcompat/quirks_linux.go index 5ef2d8a..35f754d 100644 --- a/internal/syscallcompat/quirks_linux.go +++ b/internal/syscallcompat/quirks_linux.go @@ -1,11 +1,38 @@ package syscallcompat import ( + "syscall" + "golang.org/x/sys/unix" "github.com/rfjakob/gocryptfs/v2/internal/tlog" ) +// FS_NOCOW_FL is the flag set by "chattr +C" to disable copy-on-write on +// btrfs. Not exported by golang.org/x/sys/unix, value from linux/fs.h. +const FS_NOCOW_FL = 0x00800000 + +// dirHasNoCow checks whether the directory at the given path has the +// NOCOW (No Copy-on-Write) attribute set (i.e. "chattr +C"). +// When a directory has this attribute, files created within it inherit +// NOCOW, which makes fallocate work correctly on btrfs because writes +// go in-place rather than through COW. +func dirHasNoCow(path string) bool { + fd, err := syscall.Open(path, syscall.O_RDONLY|syscall.O_DIRECTORY, 0) + if err != nil { + tlog.Debug.Printf("dirHasNoCow: Open %q failed: %v", path, err) + return false + } + defer syscall.Close(fd) + + flags, err := unix.IoctlGetInt(fd, unix.FS_IOC_GETFLAGS) + if err != nil { + tlog.Debug.Printf("dirHasNoCow: FS_IOC_GETFLAGS on %q failed: %v", path, err) + return false + } + return flags&FS_NOCOW_FL != 0 +} + // DetectQuirks decides if there are known quirks on the backing filesystem // that need to be workarounded. // @@ -21,14 +48,19 @@ func DetectQuirks(cipherdir string) (q uint64) { // Preallocation on Btrfs is broken ( https://github.com/rfjakob/gocryptfs/issues/395 ) // and slow ( https://github.com/rfjakob/gocryptfs/issues/63 ). // + // The root cause is that btrfs COW allocates new blocks on write even for + // preallocated extents, defeating the purpose of fallocate. However, if the + // backing directory has the NOCOW attribute (chattr +C), writes go in-place + // and fallocate works correctly. + // // Cast to uint32 avoids compile error on arm: "constant 2435016766 overflows int32" if uint32(st.Type) == unix.BTRFS_SUPER_MAGIC { - logQuirk("Btrfs detected, forcing -noprealloc. See https://github.com/rfjakob/gocryptfs/issues/395 for why.") - q |= QuirkBrokenFalloc - } - - if uint32(st.Type) == unix.TMPFS_MAGIC { - logQuirk("tmpfs detected, no extended attributes except acls will work.") + if dirHasNoCow(cipherdir) { + tlog.Debug.Printf("DetectQuirks: Btrfs detected but cipherdir has NOCOW attribute (chattr +C), fallocate should work correctly") + } else { + // LogQuirk is called in fusefrontend/root_node.go + q |= QuirkBtrfsBrokenFalloc + } } return q diff --git a/internal/syscallcompat/rename_exchange_test.go b/internal/syscallcompat/rename_exchange_test.go new file mode 100644 index 0000000..97a95f8 --- /dev/null +++ b/internal/syscallcompat/rename_exchange_test.go @@ -0,0 +1,58 @@ +package syscallcompat + +import ( + "os" + "path/filepath" + "testing" + + "golang.org/x/sys/unix" +) + +func TestRenameExchange(t *testing.T) { + // Create a temporary directory for testing + tmpDir, err := os.MkdirTemp("", "renameat2_test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Test basic exchange functionality + file1 := filepath.Join(tmpDir, "file1.txt") + file2 := filepath.Join(tmpDir, "file2.txt") + + content1 := []byte("content of file 1") + content2 := []byte("content of file 2") + + if err := os.WriteFile(file1, content1, 0644); err != nil { + t.Fatalf("Failed to create file1: %v", err) + } + + if err := os.WriteFile(file2, content2, 0644); err != nil { + t.Fatalf("Failed to create file2: %v", err) + } + + // Test RENAME_EXCHANGE - this is the core functionality for issue #914 + err = Renameat2(unix.AT_FDCWD, file1, unix.AT_FDCWD, file2, RENAME_EXCHANGE) + if err != nil { + t.Fatalf("RENAME_EXCHANGE failed: %v", err) + } + + // Verify that the files have been swapped + newContent1, err := os.ReadFile(file1) + if err != nil { + t.Fatalf("Failed to read file1 after exchange: %v", err) + } + + newContent2, err := os.ReadFile(file2) + if err != nil { + t.Fatalf("Failed to read file2 after exchange: %v", err) + } + + if string(newContent1) != string(content2) { + t.Errorf("file1 content after exchange. Expected: %s, Got: %s", content2, newContent1) + } + + if string(newContent2) != string(content1) { + t.Errorf("file2 content after exchange. Expected: %s, Got: %s", content1, newContent2) + } +} diff --git a/internal/syscallcompat/sys_common.go b/internal/syscallcompat/sys_common.go index 1aa6a6e..3cb9ffa 100644 --- a/internal/syscallcompat/sys_common.go +++ b/internal/syscallcompat/sys_common.go @@ -54,10 +54,10 @@ func Openat(dirfd int, path string, flags int, mode uint32) (fd int, err error) flags |= syscall.O_EXCL } } else { - // If O_CREAT is not used, we should use O_NOFOLLOW - if flags&syscall.O_NOFOLLOW == 0 { - tlog.Warn.Printf("Openat: O_NOFOLLOW missing: flags = %#x", flags) - flags |= syscall.O_NOFOLLOW + // If O_CREAT is not used, we should use O_NOFOLLOW or O_SYMLINK + if flags&(unix.O_NOFOLLOW|OpenatFlagNofollowSymlink) == 0 { + tlog.Warn.Printf("Openat: O_NOFOLLOW/O_SYMLINK missing: flags = %#x", flags) + flags |= unix.O_NOFOLLOW } } @@ -112,10 +112,10 @@ const XATTR_SIZE_MAX = 65536 // Make the buffer 1kB bigger so we can detect overflows. Unfortunately, // slices larger than 64kB are always allocated on the heap. -const XATTR_BUFSZ = XATTR_SIZE_MAX + 1024 +const GETXATTR_BUFSZ_BIG = XATTR_SIZE_MAX + 1024 // We try with a small buffer first - this one can be allocated on the stack. -const XATTR_BUFSZ_SMALL = 500 +const GETXATTR_BUFSZ_SMALL = 500 // Fgetxattr is a wrapper around unix.Fgetxattr that handles the buffer sizing. func Fgetxattr(fd int, attr string) (val []byte, err error) { @@ -135,7 +135,7 @@ func Lgetxattr(path string, attr string) (val []byte, err error) { func getxattrSmartBuf(fn func(buf []byte) (int, error)) ([]byte, error) { // Fastpaths. Important for security.capabilities, which gets queried a lot. - buf := make([]byte, XATTR_BUFSZ_SMALL) + buf := make([]byte, GETXATTR_BUFSZ_SMALL) sz, err := fn(buf) // Non-existing xattr if err == unix.ENODATA { @@ -159,7 +159,7 @@ func getxattrSmartBuf(fn func(buf []byte) (int, error)) ([]byte, error) { // We choose the simple approach of buffer that is bigger than the limit on // Linux, and return an error for everything that is bigger (which can // only happen on MacOS). - buf = make([]byte, XATTR_BUFSZ) + buf = make([]byte, GETXATTR_BUFSZ_BIG) sz, err = fn(buf) if err == syscall.ERANGE { // Do NOT return ERANGE - the user might retry ad inifinitum! @@ -182,42 +182,44 @@ out: // Flistxattr is a wrapper for unix.Flistxattr that handles buffer sizing and // parsing the returned blob to a string slice. func Flistxattr(fd int) (attrs []string, err error) { - // See the buffer sizing comments in getxattrSmartBuf. - // TODO: smarter buffer sizing? - buf := make([]byte, XATTR_BUFSZ) - sz, err := unix.Flistxattr(fd, buf) - if err == syscall.ERANGE { - // Do NOT return ERANGE - the user might retry ad inifinitum! - return nil, syscall.EOVERFLOW + listxattrSyscall := func(buf []byte) (int, error) { + return unix.Flistxattr(fd, buf) } - if err != nil { - return nil, err - } - if sz >= XATTR_SIZE_MAX { - return nil, syscall.EOVERFLOW - } - attrs = parseListxattrBlob(buf[:sz]) - return attrs, nil + return listxattrSmartBuf(listxattrSyscall) } // Llistxattr is a wrapper for unix.Llistxattr that handles buffer sizing and // parsing the returned blob to a string slice. func Llistxattr(path string) (attrs []string, err error) { - // TODO: smarter buffer sizing? - buf := make([]byte, XATTR_BUFSZ) - sz, err := unix.Llistxattr(path, buf) - if err == syscall.ERANGE { - // Do NOT return ERANGE - the user might retry ad inifinitum! - return nil, syscall.EOVERFLOW + listxattrSyscall := func(buf []byte) (int, error) { + return unix.Llistxattr(path, buf) } - if err != nil { - return nil, err - } - if sz >= XATTR_SIZE_MAX { - return nil, syscall.EOVERFLOW + return listxattrSmartBuf(listxattrSyscall) +} + +// listxattrSmartBuf handles smart buffer sizing for Flistxattr and Llistxattr +func listxattrSmartBuf(listxattrSyscall func([]byte) (int, error)) ([]string, error) { + const LISTXATTR_BUFSZ_SMALL = 100 + + // Blindly try with the small buffer first + buf := make([]byte, LISTXATTR_BUFSZ_SMALL) + sz, err := listxattrSyscall(buf) + if err == syscall.ERANGE { + // Did not fit. Find the actual size + sz, err = listxattrSyscall(nil) + if err != nil { + return nil, err + } + // ...and allocate the buffer to fit + buf = make([]byte, sz) + sz, err = listxattrSyscall(buf) + if err != nil { + // When an xattr got added between the size probe and here, + // we could fail with ERANGE. This is ok as the caller will retry. + return nil, err + } } - attrs = parseListxattrBlob(buf[:sz]) - return attrs, nil + return parseListxattrBlob(buf[:sz]), nil } func parseListxattrBlob(buf []byte) (attrs []string) { diff --git a/internal/syscallcompat/sys_darwin.go b/internal/syscallcompat/sys_darwin.go index 06f09f0..ef19f24 100644 --- a/internal/syscallcompat/sys_darwin.go +++ b/internal/syscallcompat/sys_darwin.go @@ -3,7 +3,6 @@ package syscallcompat import ( "log" "path/filepath" - "runtime" "syscall" "time" "unsafe" @@ -21,26 +20,18 @@ const ( // O_PATH is only defined on Linux O_PATH = 0 + // Same meaning, different name + RENAME_NOREPLACE = unix.RENAME_EXCL + RENAME_EXCHANGE = unix.RENAME_SWAP + // Only exists on Linux. Define here to fix build failure, even though - // we will never see the flags. - RENAME_NOREPLACE = 1 - RENAME_EXCHANGE = 2 - RENAME_WHITEOUT = 4 - - // KAUTH_UID_NONE and KAUTH_GID_NONE are special values to - // revert permissions to the process credentials. - KAUTH_UID_NONE = ^uint32(0) - 100 - KAUTH_GID_NONE = ^uint32(0) - 100 -) + // we will never see this flag. + RENAME_WHITEOUT = 1 << 30 -// Unfortunately pthread_setugid_np does not have a syscall wrapper yet. -func pthread_setugid_np(uid uint32, gid uint32) (err error) { - _, _, e1 := syscall.RawSyscall(syscall.SYS_SETTID, uintptr(uid), uintptr(gid), 0) - if e1 != 0 { - err = e1 - } - return -} + // On Darwin we use O_SYMLINK which allows opening a symlink itself. + // On Linux, we only have O_NOFOLLOW. + OpenatFlagNofollowSymlink = unix.O_SYMLINK +) // Unfortunately fsetattrlist does not have a syscall wrapper yet. func fsetattrlist(fd int, list unsafe.Pointer, buf unsafe.Pointer, size uintptr, options int) (err error) { @@ -84,74 +75,14 @@ func Dup3(oldfd int, newfd int, flags int) (err error) { //// Emulated Syscalls (see emulate.go) //////////////// //////////////////////////////////////////////////////// -func OpenatUser(dirfd int, path string, flags int, mode uint32, context *fuse.Context) (fd int, err error) { - if context != nil { - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - err = pthread_setugid_np(context.Owner.Uid, context.Owner.Gid) - if err != nil { - return -1, err - } - defer pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE) - } - - return Openat(dirfd, path, flags, mode) -} - func Mknodat(dirfd int, path string, mode uint32, dev int) (err error) { return emulateMknodat(dirfd, path, mode, dev) } -func MknodatUser(dirfd int, path string, mode uint32, dev int, context *fuse.Context) (err error) { - if context != nil { - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - err = pthread_setugid_np(context.Owner.Uid, context.Owner.Gid) - if err != nil { - return err - } - defer pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE) - } - - return Mknodat(dirfd, path, mode, dev) -} - func FchmodatNofollow(dirfd int, path string, mode uint32) (err error) { return unix.Fchmodat(dirfd, path, mode, unix.AT_SYMLINK_NOFOLLOW) } -func SymlinkatUser(oldpath string, newdirfd int, newpath string, context *fuse.Context) (err error) { - if context != nil { - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - err = pthread_setugid_np(context.Owner.Uid, context.Owner.Gid) - if err != nil { - return err - } - defer pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE) - } - - return unix.Symlinkat(oldpath, newdirfd, newpath) -} - -func MkdiratUser(dirfd int, path string, mode uint32, context *fuse.Context) (err error) { - if context != nil { - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - err = pthread_setugid_np(context.Owner.Uid, context.Owner.Gid) - if err != nil { - return err - } - defer pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE) - } - - return unix.Mkdirat(dirfd, path, mode) -} - type attrList struct { bitmapCount uint16 _ uint16 @@ -227,7 +158,12 @@ func GetdentsSpecial(fd int) (entries []fuse.DirEntry, entriesSpecial []fuse.Dir return emulateGetdents(fd) } -// Renameat2 does not exist on Darwin, so we call Renameat and ignore the flags. +// Renameat2 does not exist on Darwin, but RenameatxNp does. func Renameat2(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) (err error) { - return unix.Renameat(olddirfd, oldpath, newdirfd, newpath) + // If no flags are set, use tried and true renameat + if flags == 0 { + return unix.Renameat(olddirfd, oldpath, newdirfd, newpath) + } + // Let RenameatxNp handle everything else + return unix.RenameatxNp(olddirfd, oldpath, newdirfd, newpath, uint32(flags)) } diff --git a/internal/syscallcompat/sys_linux.go b/internal/syscallcompat/sys_linux.go index a64b27e..71478af 100644 --- a/internal/syscallcompat/sys_linux.go +++ b/internal/syscallcompat/sys_linux.go @@ -3,10 +3,6 @@ package syscallcompat import ( "fmt" - "io/ioutil" - "runtime" - "strconv" - "strings" "sync" "syscall" "time" @@ -32,6 +28,10 @@ const ( RENAME_NOREPLACE = unix.RENAME_NOREPLACE RENAME_WHITEOUT = unix.RENAME_WHITEOUT RENAME_EXCHANGE = unix.RENAME_EXCHANGE + + // On Darwin we use O_SYMLINK which allows opening a symlink itself. + // On Linux, we only have O_NOFOLLOW. + OpenatFlagNofollowSymlink = unix.O_NOFOLLOW ) var preallocWarn sync.Once @@ -67,104 +67,11 @@ func Fallocate(fd int, mode uint32, off int64, len int64) (err error) { return syscall.Fallocate(fd, mode, off, len) } -func getSupplementaryGroups(pid uint32) (gids []int) { - procPath := fmt.Sprintf("/proc/%d/task/%d/status", pid, pid) - blob, err := ioutil.ReadFile(procPath) - if err != nil { - return nil - } - - lines := strings.Split(string(blob), "\n") - for _, line := range lines { - if strings.HasPrefix(line, "Groups:") { - f := strings.Fields(line[7:]) - gids = make([]int, len(f)) - for i := range gids { - val, err := strconv.ParseInt(f[i], 10, 32) - if err != nil { - return nil - } - gids[i] = int(val) - } - return gids - } - } - - return nil -} - -// asUser runs `f()` under the effective uid, gid, groups specified -// in `context`. -// -// If `context` is nil, `f()` is executed directly without switching user id. -func asUser(f func() (int, error), context *fuse.Context) (int, error) { - if context == nil { - return f() - } - - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - // Since go1.16beta1 (commit d1b1145cace8b968307f9311ff611e4bb810710c , - // https://go-review.googlesource.com/c/go/+/210639 ) - // syscall.{Setgroups,Setregid,Setreuid} affects all threads, which - // is exactly what we not want. - // - // We now use unix.{Setgroups,Setregid,Setreuid} instead. - - err := unix.Setgroups(getSupplementaryGroups(context.Pid)) - if err != nil { - return -1, err - } - defer unix.Setgroups(nil) - - err = unix.Setregid(-1, int(context.Owner.Gid)) - if err != nil { - return -1, err - } - defer unix.Setregid(-1, 0) - - err = unix.Setreuid(-1, int(context.Owner.Uid)) - if err != nil { - return -1, err - } - defer unix.Setreuid(-1, 0) - - return f() -} - -// OpenatUser runs the Openat syscall in the context of a different user. -// -// It switches the current thread to the new user, performs the syscall, -// and switches back. -// -// If `context` is nil, this function behaves like ordinary Openat (no -// user switching). -func OpenatUser(dirfd int, path string, flags int, mode uint32, context *fuse.Context) (fd int, err error) { - f := func() (int, error) { - return Openat(dirfd, path, flags, mode) - } - return asUser(f, context) -} - // Mknodat wraps the Mknodat syscall. func Mknodat(dirfd int, path string, mode uint32, dev int) (err error) { return syscall.Mknodat(dirfd, path, mode, dev) } -// MknodatUser runs the Mknodat syscall in the context of a different user. -// If `context` is nil, this function behaves like ordinary Mknodat. -// -// See OpenatUser() for how this works. -func MknodatUser(dirfd int, path string, mode uint32, dev int, context *fuse.Context) (err error) { - f := func() (int, error) { - err := Mknodat(dirfd, path, mode, dev) - return -1, err - } - _, err = asUser(f, context) - return err -} - // Dup3 wraps the Dup3 syscall. We want to use Dup3 rather than Dup2 because Dup2 // is not implemented on arm64. func Dup3(oldfd int, newfd int, flags int) (err error) { @@ -205,32 +112,6 @@ func FchmodatNofollow(dirfd int, path string, mode uint32) (err error) { return syscall.Chmod(procPath, mode) } -// SymlinkatUser runs the Symlinkat syscall in the context of a different user. -// If `context` is nil, this function behaves like ordinary Symlinkat. -// -// See OpenatUser() for how this works. -func SymlinkatUser(oldpath string, newdirfd int, newpath string, context *fuse.Context) (err error) { - f := func() (int, error) { - err := unix.Symlinkat(oldpath, newdirfd, newpath) - return -1, err - } - _, err = asUser(f, context) - return err -} - -// MkdiratUser runs the Mkdirat syscall in the context of a different user. -// If `context` is nil, this function behaves like ordinary Mkdirat. -// -// See OpenatUser() for how this works. -func MkdiratUser(dirfd int, path string, mode uint32, context *fuse.Context) (err error) { - f := func() (int, error) { - err := unix.Mkdirat(dirfd, path, mode) - return -1, err - } - _, err = asUser(f, context) - return err -} - // LsetxattrUser runs the Lsetxattr syscall in the context of a different user. // This is useful when setting ACLs, as the result depends on the user running // the operation (see fuse-xfstests generic/375). @@ -247,8 +128,16 @@ func LsetxattrUser(path string, attr string, data []byte, flags int, context *fu func timesToTimespec(a *time.Time, m *time.Time) []unix.Timespec { ts := make([]unix.Timespec, 2) - ts[0] = unix.Timespec(fuse.UtimeToTimespec(a)) - ts[1] = unix.Timespec(fuse.UtimeToTimespec(m)) + if a == nil { + ts[0] = unix.Timespec{Nsec: unix.UTIME_OMIT} + } else { + ts[0], _ = unix.TimeToTimespec(*a) + } + if m == nil { + ts[1] = unix.Timespec{Nsec: unix.UTIME_OMIT} + } else { + ts[1], _ = unix.TimeToTimespec(*m) + } return ts } diff --git a/internal/syscallcompat/thread_credentials_linux.go b/internal/syscallcompat/thread_credentials_linux.go new file mode 100644 index 0000000..b5ec6cd --- /dev/null +++ b/internal/syscallcompat/thread_credentials_linux.go @@ -0,0 +1,61 @@ +//go:build linux + +// golang.org/x/sys/unix commit +// https://github.com/golang/sys/commit/d0df966e6959f00dc1c74363e537872647352d51 +// changed unix.Setreuid/unix.Setregid functions to affect the whole thread, which is +// what gocryptfs does NOT want (https://github.com/rfjakob/gocryptfs/issues/893). +// The functions Setreuid/Setegid are copy-pasted from one commit before +// (9e1f76180b77a12eb07c82eb8e1ea8a7f8d202e7). +// +// Looking at the diff at https://github.com/golang/sys/commit/d0df966e6959f00dc1c74363e537872647352d51 +// we see that only two architectures, 386 and arm, use SYS_SETREUID32/SYS_SETREGID32 +// (see "man 2 setreuid" for why). +// All the others architectures use SYS_SETREUID/SYS_SETREGID. +// +// As of golang.org/x/sys/unix v0.30.0, Setgroups/setgroups is still per-thread, but +// it is likely that this will change, too. Setgroups/setgroups are copy-pasted from +// v0.30.0. The SYS_SETGROUPS32/SYS_SETGROUPS split is the same as for Setreuid. +// +// Note: _Gid_t is always uint32 on linux, so we can directly use uint32 for setgroups. +package syscallcompat + +import ( + "log" +) + +// Setgroups is like setgroups(2) but affects only the current thread +func Setgroups(gids []int) (err error) { + if len(gids) == 0 { + return setgroups(0, nil) + } + + a := make([]uint32, len(gids)) + for i, v := range gids { + a[i] = uint32(v) + } + return setgroups(len(a), &a[0]) +} + +// SetgroupsPanic calls Setgroups and panics on error +func SetgroupsPanic(gids []int) { + err := Setgroups(gids) + if err != nil { + log.Panic(err) + } +} + +// SetregidPanic calls Setregid and panics on error +func SetregidPanic(rgid int, egid int) { + err := Setregid(rgid, egid) + if err != nil { + log.Panic(err) + } +} + +// SetreuidPanic calls Setreuid and panics on error +func SetreuidPanic(ruid int, euid int) { + err := Setreuid(ruid, euid) + if err != nil { + log.Panic(err) + } +} diff --git a/internal/syscallcompat/thread_credentials_linux_32.go b/internal/syscallcompat/thread_credentials_linux_32.go new file mode 100644 index 0000000..69fffca --- /dev/null +++ b/internal/syscallcompat/thread_credentials_linux_32.go @@ -0,0 +1,40 @@ +//go:build (linux && 386) || (linux && arm) + +// Linux on i386 and 32-bit ARM has SYS_SETREUID and friends returning 16-bit values. +// We need to use SYS_SETREUID32 instead. + +package syscallcompat + +import ( + "unsafe" + + "golang.org/x/sys/unix" +) + +// See thread_credentials_linux.go for docs + +// Setreuid is like setreuid(2) but affects only the current thread +func Setreuid(ruid int, euid int) (err error) { + _, _, e1 := unix.RawSyscall(unix.SYS_SETREUID32, uintptr(ruid), uintptr(euid), 0) + if e1 != 0 { + err = e1 + } + return +} + +// Setreuid is like setregid(2) but affects only the current thread +func Setregid(rgid int, egid int) (err error) { + _, _, e1 := unix.RawSyscall(unix.SYS_SETREGID32, uintptr(rgid), uintptr(egid), 0) + if e1 != 0 { + err = e1 + } + return +} + +func setgroups(n int, list *uint32) (err error) { + _, _, e1 := unix.RawSyscall(unix.SYS_SETGROUPS32, uintptr(n), uintptr(unsafe.Pointer(list)), 0) + if e1 != 0 { + err = e1 + } + return +} diff --git a/internal/syscallcompat/thread_credentials_linux_other.go b/internal/syscallcompat/thread_credentials_linux_other.go new file mode 100644 index 0000000..ab11c71 --- /dev/null +++ b/internal/syscallcompat/thread_credentials_linux_other.go @@ -0,0 +1,37 @@ +//go:build !((linux && 386) || (linux && arm)) + +package syscallcompat + +import ( + "unsafe" + + "golang.org/x/sys/unix" +) + +// See thread_credentials_linux.go for docs + +// Setreuid is like setreuid(2) but affects only the current thread +func Setreuid(ruid int, euid int) (err error) { + _, _, e1 := unix.RawSyscall(unix.SYS_SETREUID, uintptr(ruid), uintptr(euid), 0) + if e1 != 0 { + err = e1 + } + return +} + +// Setreuid is like setregid(2) but affects only the current thread +func Setregid(rgid int, egid int) (err error) { + _, _, e1 := unix.RawSyscall(unix.SYS_SETREGID, uintptr(rgid), uintptr(egid), 0) + if e1 != 0 { + err = e1 + } + return +} + +func setgroups(n int, list *uint32) (err error) { + _, _, e1 := unix.RawSyscall(unix.SYS_SETGROUPS, uintptr(n), uintptr(unsafe.Pointer(list)), 0) + if e1 != 0 { + err = e1 + } + return +} diff --git a/internal/tlog/log.go b/internal/tlog/log.go index 62d791d..02c760c 100644 --- a/internal/tlog/log.go +++ b/internal/tlog/log.go @@ -73,7 +73,7 @@ func (l *toggledLogger) Printf(format string, v ...interface{}) { return } msg := trimNewline(fmt.Sprintf(format, v...)) - l.Logger.Printf(l.prefix + msg + l.postfix) + l.Logger.Print(l.prefix + msg + l.postfix) if l.Wpanic { l.Logger.Panic(wpanicMsg + msg) } |
