v2api: delete (most) fusefrontend v1 files
All the functionality in these files has been reimplemented for the v2 api. Drop the old files.
This commit is contained in:
parent
81fb42b912
commit
777b95f82f
|
@ -1,482 +0,0 @@
|
||||||
package fusefrontend
|
|
||||||
|
|
||||||
// FUSE operations on file handles
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
|
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/contentenc"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/inomap"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/openfiletable"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/serialize_reads"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/stupidgcm"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ nodefs.File = &File{} // Verify that interface is implemented.
|
|
||||||
|
|
||||||
// File - based on loopbackFile in go-fuse/fuse/nodefs/files.go
|
|
||||||
type File struct {
|
|
||||||
fd *os.File
|
|
||||||
// Has Release() already been called on this file? This also means that the
|
|
||||||
// wlock entry has been freed, so let's not crash trying to access it.
|
|
||||||
// Due to concurrency, Release can overtake other operations. These will
|
|
||||||
// return EBADF in that case.
|
|
||||||
released bool
|
|
||||||
// fdLock prevents the fd to be closed while we are in the middle of
|
|
||||||
// an operation.
|
|
||||||
// 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
|
|
||||||
fileTableEntry *openfiletable.Entry
|
|
||||||
// Store where the last byte was written
|
|
||||||
lastWrittenOffset int64
|
|
||||||
// The opCount is used to judge whether "lastWrittenOffset" is still
|
|
||||||
// guaranteed to be correct.
|
|
||||||
lastOpCount uint64
|
|
||||||
// Parent filesystem
|
|
||||||
fs *FS
|
|
||||||
// We embed a nodefs.NewDefaultFile() that returns ENOSYS for every operation we
|
|
||||||
// have not implemented. This prevents build breakage when the go-fuse library
|
|
||||||
// adds new methods to the nodefs.File interface.
|
|
||||||
nodefs.File
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFile returns a new go-fuse File instance.
|
|
||||||
func NewFile(fd *os.File, fs *FS) (*File, fuse.Status) {
|
|
||||||
var st syscall.Stat_t
|
|
||||||
err := syscall.Fstat(int(fd.Fd()), &st)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("NewFile: Fstat on fd %d failed: %v\n", fd.Fd(), err)
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
qi := inomap.QInoFromStat(&st)
|
|
||||||
e := openfiletable.Register(qi)
|
|
||||||
|
|
||||||
return &File{
|
|
||||||
fd: fd,
|
|
||||||
contentEnc: fs.contentEnc,
|
|
||||||
qIno: qi,
|
|
||||||
fileTableEntry: e,
|
|
||||||
fs: fs,
|
|
||||||
File: nodefs.NewDefaultFile(),
|
|
||||||
}, fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// intFd - return the backing file descriptor as an integer.
|
|
||||||
func (f *File) intFd() int {
|
|
||||||
return int(f.fd.Fd())
|
|
||||||
}
|
|
||||||
|
|
||||||
// readFileID loads the file header from disk and extracts the file ID.
|
|
||||||
// Returns io.EOF if the file is empty.
|
|
||||||
func (f *File) readFileID() ([]byte, error) {
|
|
||||||
// We read +1 byte to determine if the file has actual content
|
|
||||||
// and not only the header. A header-only file will be considered empty.
|
|
||||||
// This makes File ID poisoning more difficult.
|
|
||||||
readLen := contentenc.HeaderLen + 1
|
|
||||||
buf := make([]byte, readLen)
|
|
||||||
n, err := f.fd.ReadAt(buf, 0)
|
|
||||||
if err != nil {
|
|
||||||
if err == io.EOF && n != 0 {
|
|
||||||
tlog.Warn.Printf("readFileID %d: incomplete file, got %d instead of %d bytes",
|
|
||||||
f.qIno.Ino, n, readLen)
|
|
||||||
f.fs.reportMitigatedCorruption(fmt.Sprint(f.qIno.Ino))
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buf = buf[:contentenc.HeaderLen]
|
|
||||||
h, err := contentenc.ParseHeader(buf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return h.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// createHeader creates a new random header and writes it to disk.
|
|
||||||
// Returns the new file ID.
|
|
||||||
// The caller must hold fileIDLock.Lock().
|
|
||||||
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.fs.args.NoPrealloc {
|
|
||||||
err = syscallcompat.EnospcPrealloc(f.intFd(), 0, contentenc.HeaderLen)
|
|
||||||
if err != nil {
|
|
||||||
if !syscallcompat.IsENOSPC(err) {
|
|
||||||
tlog.Warn.Printf("ino%d: createHeader: prealloc failed: %s\n", f.qIno.Ino, err.Error())
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Actually write header
|
|
||||||
_, err = f.fd.WriteAt(buf, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return h.ID, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// doRead - read "length" plaintext bytes from plaintext offset "off" and append
|
|
||||||
// to "dst".
|
|
||||||
// Arguments "length" and "off" do not have to be block-aligned.
|
|
||||||
//
|
|
||||||
// doRead reads the corresponding ciphertext blocks from disk, decrypts them and
|
|
||||||
// returns the requested part of the plaintext.
|
|
||||||
//
|
|
||||||
// Called by Read() for normal reading,
|
|
||||||
// by Write() and Truncate() via doWrite() for Read-Modify-Write.
|
|
||||||
func (f *File) doRead(dst []byte, off uint64, length uint64) ([]byte, fuse.Status) {
|
|
||||||
// Get the file ID, either from the open file table, or from disk.
|
|
||||||
var fileID []byte
|
|
||||||
f.fileTableEntry.IDLock.Lock()
|
|
||||||
if f.fileTableEntry.ID != nil {
|
|
||||||
// Use the cached value in the file table
|
|
||||||
fileID = f.fileTableEntry.ID
|
|
||||||
} else {
|
|
||||||
// Not cached, we have to read it from disk.
|
|
||||||
var err error
|
|
||||||
fileID, err = f.readFileID()
|
|
||||||
if err != nil {
|
|
||||||
f.fileTableEntry.IDLock.Unlock()
|
|
||||||
if err == io.EOF {
|
|
||||||
// Empty file
|
|
||||||
return nil, fuse.OK
|
|
||||||
}
|
|
||||||
buf := make([]byte, 100)
|
|
||||||
n, _ := f.fd.ReadAt(buf, 0)
|
|
||||||
buf = buf[:n]
|
|
||||||
hexdump := hex.EncodeToString(buf)
|
|
||||||
tlog.Warn.Printf("doRead %d: corrupt header: %v\nFile hexdump (%d bytes): %s",
|
|
||||||
f.qIno.Ino, err, n, hexdump)
|
|
||||||
return nil, fuse.EIO
|
|
||||||
}
|
|
||||||
// Save into the file table
|
|
||||||
f.fileTableEntry.ID = fileID
|
|
||||||
}
|
|
||||||
f.fileTableEntry.IDLock.Unlock()
|
|
||||||
if fileID == nil {
|
|
||||||
log.Panicf("fileID=%v", fileID)
|
|
||||||
}
|
|
||||||
// Read the backing ciphertext in one go
|
|
||||||
blocks := f.contentEnc.ExplodePlainRange(off, length)
|
|
||||||
alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks)
|
|
||||||
skip := blocks[0].Skip
|
|
||||||
tlog.Debug.Printf("doRead: off=%d len=%d -> off=%d len=%d skip=%d\n",
|
|
||||||
off, length, alignedOffset, alignedLength, skip)
|
|
||||||
|
|
||||||
ciphertext := f.fs.contentEnc.CReqPool.Get()
|
|
||||||
ciphertext = ciphertext[:int(alignedLength)]
|
|
||||||
n, err := f.fd.ReadAt(ciphertext, int64(alignedOffset))
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
tlog.Warn.Printf("read: ReadAt: %s", err.Error())
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// The ReadAt came back empty. We can skip all the decryption and return early.
|
|
||||||
if n == 0 {
|
|
||||||
f.fs.contentEnc.CReqPool.Put(ciphertext)
|
|
||||||
return dst, fuse.OK
|
|
||||||
}
|
|
||||||
// Truncate ciphertext buffer down to actually read bytes
|
|
||||||
ciphertext = ciphertext[0:n]
|
|
||||||
|
|
||||||
firstBlockNo := blocks[0].BlockNo
|
|
||||||
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)
|
|
||||||
f.fs.contentEnc.CReqPool.Put(ciphertext)
|
|
||||||
if err != nil {
|
|
||||||
if f.fs.args.ForceDecode && err == stupidgcm.ErrAuth {
|
|
||||||
// We do not have the information which block was corrupt here anymore,
|
|
||||||
// but DecryptBlocks() has already logged it anyway.
|
|
||||||
tlog.Warn.Printf("doRead %d: off=%d len=%d: returning corrupt data due to forcedecode",
|
|
||||||
f.qIno.Ino, off, length)
|
|
||||||
} else {
|
|
||||||
curruptBlockNo := firstBlockNo + f.contentEnc.PlainOffToBlockNo(uint64(len(plaintext)))
|
|
||||||
tlog.Warn.Printf("doRead %d: corrupt block #%d: %v", f.qIno.Ino, curruptBlockNo, err)
|
|
||||||
return nil, fuse.EIO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crop down to the relevant part
|
|
||||||
var out []byte
|
|
||||||
lenHave := len(plaintext)
|
|
||||||
lenWant := int(skip + length)
|
|
||||||
if lenHave > lenWant {
|
|
||||||
out = plaintext[skip:lenWant]
|
|
||||||
} else if lenHave > int(skip) {
|
|
||||||
out = plaintext[skip:lenHave]
|
|
||||||
}
|
|
||||||
// else: out stays empty, file was smaller than the requested offset
|
|
||||||
|
|
||||||
out = append(dst, out...)
|
|
||||||
f.fs.contentEnc.PReqPool.Put(plaintext)
|
|
||||||
|
|
||||||
return out, fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read - FUSE call
|
|
||||||
func (f *File) Read(buf []byte, off int64) (resultData fuse.ReadResult, code fuse.Status) {
|
|
||||||
if len(buf) > fuse.MAX_KERNEL_WRITE {
|
|
||||||
// This would crash us due to our fixed-size buffer pool
|
|
||||||
tlog.Warn.Printf("Read: rejecting oversized request with EMSGSIZE, len=%d", len(buf))
|
|
||||||
return nil, fuse.Status(syscall.EMSGSIZE)
|
|
||||||
}
|
|
||||||
f.fdLock.RLock()
|
|
||||||
defer f.fdLock.RUnlock()
|
|
||||||
|
|
||||||
f.fileTableEntry.ContentLock.RLock()
|
|
||||||
defer f.fileTableEntry.ContentLock.RUnlock()
|
|
||||||
|
|
||||||
tlog.Debug.Printf("ino%d: FUSE Read: offset=%d length=%d", f.qIno.Ino, off, len(buf))
|
|
||||||
if f.fs.args.SerializeReads {
|
|
||||||
serialize_reads.Wait(off, len(buf))
|
|
||||||
}
|
|
||||||
out, status := f.doRead(buf[:0], uint64(off), uint64(len(buf)))
|
|
||||||
if f.fs.args.SerializeReads {
|
|
||||||
serialize_reads.Done()
|
|
||||||
}
|
|
||||||
if status != fuse.OK {
|
|
||||||
return nil, status
|
|
||||||
}
|
|
||||||
tlog.Debug.Printf("ino%d: Read: status %v, returning %d bytes", f.qIno.Ino, status, len(out))
|
|
||||||
return fuse.ReadResultData(out), status
|
|
||||||
}
|
|
||||||
|
|
||||||
// doWrite - encrypt "data" and write it to plaintext offset "off"
|
|
||||||
//
|
|
||||||
// Arguments do not have to be block-aligned, read-modify-write is
|
|
||||||
// performed internally as necessary
|
|
||||||
//
|
|
||||||
// Called by Write() for normal writing,
|
|
||||||
// and by Truncate() to rewrite the last file block.
|
|
||||||
//
|
|
||||||
// Empty writes do nothing and are allowed.
|
|
||||||
func (f *File) doWrite(data []byte, off int64) (uint32, fuse.Status) {
|
|
||||||
fileWasEmpty := false
|
|
||||||
// Get the file ID, create a new one if it does not exist yet.
|
|
||||||
var fileID []byte
|
|
||||||
// The caller has exclusively locked ContentLock, which blocks all other
|
|
||||||
// readers and writers. No need to take IDLock.
|
|
||||||
if f.fileTableEntry.ID != nil {
|
|
||||||
fileID = f.fileTableEntry.ID
|
|
||||||
} else {
|
|
||||||
// If the file ID is not cached, read it from disk
|
|
||||||
var err error
|
|
||||||
fileID, err = f.readFileID()
|
|
||||||
// Write a new file header if the file is empty
|
|
||||||
if err == io.EOF {
|
|
||||||
fileID, err = f.createHeader()
|
|
||||||
fileWasEmpty = true
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return 0, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
f.fileTableEntry.ID = fileID
|
|
||||||
}
|
|
||||||
// Handle payload data
|
|
||||||
dataBuf := bytes.NewBuffer(data)
|
|
||||||
blocks := f.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, status := f.doRead(nil, b.BlockPlainOff(), f.contentEnc.PlainBS())
|
|
||||||
if status != fuse.OK {
|
|
||||||
tlog.Warn.Printf("ino%d fh%d: RMW read failed: %s", f.qIno.Ino, f.intFd(), status.String())
|
|
||||||
return 0, status
|
|
||||||
}
|
|
||||||
// Modify
|
|
||||||
blockData = f.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",
|
|
||||||
f.qIno.Ino, len(blockData), b.BlockNo)
|
|
||||||
// Write into the to-encrypt list
|
|
||||||
toEncrypt[i] = blockData
|
|
||||||
}
|
|
||||||
// Encrypt all blocks
|
|
||||||
ciphertext := f.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
|
|
||||||
cOff := int64(blocks[0].BlockCipherOff())
|
|
||||||
if !f.fs.args.NoPrealloc {
|
|
||||||
err = syscallcompat.EnospcPrealloc(f.intFd(), cOff, int64(len(ciphertext)))
|
|
||||||
if err != nil {
|
|
||||||
if !syscallcompat.IsENOSPC(err) {
|
|
||||||
tlog.Warn.Printf("ino%d fh%d: doWrite: prealloc failed: %v", f.qIno.Ino, f.intFd(), err)
|
|
||||||
}
|
|
||||||
if fileWasEmpty {
|
|
||||||
// Kill the file header again
|
|
||||||
f.fileTableEntry.ID = nil
|
|
||||||
err2 := syscall.Ftruncate(f.intFd(), 0)
|
|
||||||
if err2 != nil {
|
|
||||||
tlog.Warn.Printf("ino%d fh%d: doWrite: rollback failed: %v", f.qIno.Ino, f.intFd(), err2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Write
|
|
||||||
_, err = f.fd.WriteAt(ciphertext, cOff)
|
|
||||||
// Return memory to CReqPool
|
|
||||||
f.fs.contentEnc.CReqPool.Put(ciphertext)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("ino%d fh%d: doWrite: WriteAt off=%d len=%d failed: %v",
|
|
||||||
f.qIno.Ino, f.intFd(), cOff, len(ciphertext), err)
|
|
||||||
return 0, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
return uint32(len(data)), fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// isConsecutiveWrite returns true if the current write
|
|
||||||
// directly (in time and space) follows the last write.
|
|
||||||
// This is an optimisation for streaming writes on NFS where a
|
|
||||||
// Stat() call is very expensive.
|
|
||||||
// The caller must "wlock.lock(f.devIno.ino)" otherwise this check would be racy.
|
|
||||||
func (f *File) isConsecutiveWrite(off int64) bool {
|
|
||||||
opCount := openfiletable.WriteOpCount()
|
|
||||||
return opCount == f.lastOpCount+1 && off == f.lastWrittenOffset+1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write - FUSE call
|
|
||||||
//
|
|
||||||
// If the write creates a hole, pads the file to the next block boundary.
|
|
||||||
func (f *File) Write(data []byte, off int64) (uint32, fuse.Status) {
|
|
||||||
if len(data) > fuse.MAX_KERNEL_WRITE {
|
|
||||||
// This would crash us due to our fixed-size buffer pool
|
|
||||||
tlog.Warn.Printf("Write: rejecting oversized request with EMSGSIZE, len=%d", len(data))
|
|
||||||
return 0, fuse.Status(syscall.EMSGSIZE)
|
|
||||||
}
|
|
||||||
f.fdLock.RLock()
|
|
||||||
defer f.fdLock.RUnlock()
|
|
||||||
if f.released {
|
|
||||||
// The file descriptor has been closed concurrently
|
|
||||||
tlog.Warn.Printf("ino%d fh%d: Write on released file", f.qIno.Ino, f.intFd())
|
|
||||||
return 0, fuse.EBADF
|
|
||||||
}
|
|
||||||
f.fileTableEntry.ContentLock.Lock()
|
|
||||||
defer f.fileTableEntry.ContentLock.Unlock()
|
|
||||||
tlog.Debug.Printf("ino%d: FUSE Write: offset=%d length=%d", f.qIno.Ino, off, len(data))
|
|
||||||
// If the write creates a file hole, we have to zero-pad the last block.
|
|
||||||
// But if the write directly follows an earlier write, it cannot create a
|
|
||||||
// hole, and we can save one Stat() call.
|
|
||||||
if !f.isConsecutiveWrite(off) {
|
|
||||||
status := f.writePadHole(off)
|
|
||||||
if !status.Ok() {
|
|
||||||
return 0, status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
n, status := f.doWrite(data, off)
|
|
||||||
if status.Ok() {
|
|
||||||
f.lastOpCount = openfiletable.WriteOpCount()
|
|
||||||
f.lastWrittenOffset = off + int64(len(data)) - 1
|
|
||||||
}
|
|
||||||
return n, status
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release - FUSE call, close file
|
|
||||||
func (f *File) Release() {
|
|
||||||
f.fdLock.Lock()
|
|
||||||
if f.released {
|
|
||||||
log.Panicf("ino%d fh%d: double release", f.qIno.Ino, f.intFd())
|
|
||||||
}
|
|
||||||
f.released = true
|
|
||||||
openfiletable.Unregister(f.qIno)
|
|
||||||
f.fd.Close()
|
|
||||||
f.fdLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush - FUSE call
|
|
||||||
func (f *File) Flush() fuse.Status {
|
|
||||||
f.fdLock.RLock()
|
|
||||||
defer f.fdLock.RUnlock()
|
|
||||||
|
|
||||||
// Since Flush() may be called for each dup'd fd, we don't
|
|
||||||
// want to really close the file, we just want to flush. This
|
|
||||||
// is achieved by closing a dup'd fd.
|
|
||||||
newFd, err := syscall.Dup(f.intFd())
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
err = syscall.Close(newFd)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fsync FUSE call
|
|
||||||
func (f *File) Fsync(flags int) (code fuse.Status) {
|
|
||||||
f.fdLock.RLock()
|
|
||||||
defer f.fdLock.RUnlock()
|
|
||||||
|
|
||||||
return fuse.ToStatus(syscall.Fsync(f.intFd()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chmod FUSE call
|
|
||||||
func (f *File) Chmod(mode uint32) fuse.Status {
|
|
||||||
f.fdLock.RLock()
|
|
||||||
defer f.fdLock.RUnlock()
|
|
||||||
|
|
||||||
// os.File.Chmod goes through the "syscallMode" translation function that messes
|
|
||||||
// up the suid and sgid bits. So use syscall.Fchmod directly.
|
|
||||||
err := syscall.Fchmod(f.intFd(), mode)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chown FUSE call
|
|
||||||
func (f *File) Chown(uid uint32, gid uint32) fuse.Status {
|
|
||||||
f.fdLock.RLock()
|
|
||||||
defer f.fdLock.RUnlock()
|
|
||||||
|
|
||||||
return fuse.ToStatus(f.fd.Chown(int(uid), int(gid)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAttr FUSE call (like stat)
|
|
||||||
func (f *File) GetAttr(a *fuse.Attr) fuse.Status {
|
|
||||||
f.fdLock.RLock()
|
|
||||||
defer f.fdLock.RUnlock()
|
|
||||||
|
|
||||||
tlog.Debug.Printf("file.GetAttr()")
|
|
||||||
st := syscall.Stat_t{}
|
|
||||||
err := syscall.Fstat(f.intFd(), &st)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
f.fs.inoMap.TranslateStat(&st)
|
|
||||||
a.FromStat(&st)
|
|
||||||
a.Size = f.contentEnc.CipherSizeToPlainSize(a.Size)
|
|
||||||
if f.fs.args.ForceOwner != nil {
|
|
||||||
a.Owner = *f.fs.args.ForceOwner
|
|
||||||
}
|
|
||||||
|
|
||||||
return fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utimens FUSE call
|
|
||||||
func (f *File) Utimens(a *time.Time, m *time.Time) fuse.Status {
|
|
||||||
f.fdLock.RLock()
|
|
||||||
defer f.fdLock.RUnlock()
|
|
||||||
err := syscallcompat.FutimesNano(f.intFd(), a, m)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
|
@ -6,6 +6,7 @@ package fusefrontend
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fs"
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
|
@ -14,6 +15,15 @@ import (
|
||||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
"github.com/rfjakob/gocryptfs/internal/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FALLOC_DEFAULT is a "normal" fallocate operation
|
||||||
|
const FALLOC_DEFAULT = 0x00
|
||||||
|
|
||||||
|
// FALLOC_FL_KEEP_SIZE allocates disk space while not modifying the file size
|
||||||
|
const FALLOC_FL_KEEP_SIZE = 0x01
|
||||||
|
|
||||||
|
// Only warn once
|
||||||
|
var allocateWarnOnce sync.Once
|
||||||
|
|
||||||
// Allocate - FUSE call for fallocate(2)
|
// Allocate - FUSE call for fallocate(2)
|
||||||
//
|
//
|
||||||
// mode=FALLOC_FL_KEEP_SIZE is implemented directly.
|
// mode=FALLOC_FL_KEEP_SIZE is implemented directly.
|
||||||
|
|
|
@ -1,227 +0,0 @@
|
||||||
package fusefrontend
|
|
||||||
|
|
||||||
// FUSE operations Truncate and Allocate on file handles
|
|
||||||
// i.e. ftruncate and fallocate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FALLOC_DEFAULT is a "normal" fallocate operation
|
|
||||||
const FALLOC_DEFAULT = 0x00
|
|
||||||
|
|
||||||
// FALLOC_FL_KEEP_SIZE allocates disk space while not modifying the file size
|
|
||||||
const FALLOC_FL_KEEP_SIZE = 0x01
|
|
||||||
|
|
||||||
// Only warn once
|
|
||||||
var allocateWarnOnce sync.Once
|
|
||||||
|
|
||||||
// Allocate - FUSE call for fallocate(2)
|
|
||||||
//
|
|
||||||
// mode=FALLOC_FL_KEEP_SIZE is implemented directly.
|
|
||||||
//
|
|
||||||
// mode=FALLOC_DEFAULT is implemented as a two-step process:
|
|
||||||
//
|
|
||||||
// (1) Allocate the space using FALLOC_FL_KEEP_SIZE
|
|
||||||
// (2) Set the file size using ftruncate (via truncateGrowFile)
|
|
||||||
//
|
|
||||||
// This allows us to reuse the file grow mechanics from Truncate as they are
|
|
||||||
// complicated and hard to get right.
|
|
||||||
//
|
|
||||||
// Other modes (hole punching, zeroing) are not supported.
|
|
||||||
func (f *File) Allocate(off uint64, sz uint64, mode uint32) fuse.Status {
|
|
||||||
if mode != FALLOC_DEFAULT && mode != FALLOC_FL_KEEP_SIZE {
|
|
||||||
f := func() {
|
|
||||||
tlog.Info.Printf("fallocate: only mode 0 (default) and 1 (keep size) are supported")
|
|
||||||
}
|
|
||||||
allocateWarnOnce.Do(f)
|
|
||||||
return fuse.Status(syscall.EOPNOTSUPP)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.fdLock.RLock()
|
|
||||||
defer f.fdLock.RUnlock()
|
|
||||||
if f.released {
|
|
||||||
return fuse.EBADF
|
|
||||||
}
|
|
||||||
f.fileTableEntry.ContentLock.Lock()
|
|
||||||
defer f.fileTableEntry.ContentLock.Unlock()
|
|
||||||
|
|
||||||
blocks := f.contentEnc.ExplodePlainRange(off, sz)
|
|
||||||
firstBlock := blocks[0]
|
|
||||||
lastBlock := blocks[len(blocks)-1]
|
|
||||||
|
|
||||||
// Step (1): Allocate the space the user wants using FALLOC_FL_KEEP_SIZE.
|
|
||||||
// This will fill file holes and/or allocate additional space past the end of
|
|
||||||
// the file.
|
|
||||||
cipherOff := firstBlock.BlockCipherOff()
|
|
||||||
cipherSz := lastBlock.BlockCipherOff() - cipherOff +
|
|
||||||
f.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)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
if mode == FALLOC_FL_KEEP_SIZE {
|
|
||||||
// The user did not want to change the apparent size. We are done.
|
|
||||||
return fuse.OK
|
|
||||||
}
|
|
||||||
// Step (2): Grow the apparent file size
|
|
||||||
// We need the old file size to determine if we are growing the file at all.
|
|
||||||
newPlainSz := off + sz
|
|
||||||
oldPlainSz, err := f.statPlainSize()
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
if newPlainSz <= oldPlainSz {
|
|
||||||
// The new size is smaller (or equal). Fallocate with mode = 0 never
|
|
||||||
// truncates a file, so we are done.
|
|
||||||
return fuse.OK
|
|
||||||
}
|
|
||||||
// The file grows. The space has already been allocated in (1), so what is
|
|
||||||
// left to do is to pad the first and last block and call truncate.
|
|
||||||
// truncateGrowFile does just that.
|
|
||||||
return f.truncateGrowFile(oldPlainSz, newPlainSz)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate - FUSE call
|
|
||||||
func (f *File) Truncate(newSize uint64) fuse.Status {
|
|
||||||
f.fdLock.RLock()
|
|
||||||
defer f.fdLock.RUnlock()
|
|
||||||
if f.released {
|
|
||||||
// The file descriptor has been closed concurrently.
|
|
||||||
tlog.Warn.Printf("ino%d fh%d: Truncate on released file", f.qIno.Ino, f.intFd())
|
|
||||||
return fuse.EBADF
|
|
||||||
}
|
|
||||||
f.fileTableEntry.ContentLock.Lock()
|
|
||||||
defer f.fileTableEntry.ContentLock.Unlock()
|
|
||||||
var err error
|
|
||||||
// Common case first: Truncate to zero
|
|
||||||
if newSize == 0 {
|
|
||||||
err = syscall.Ftruncate(int(f.fd.Fd()), 0)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("ino%d fh%d: Ftruncate(fd, 0) returned error: %v", f.qIno.Ino, f.intFd(), err)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// Truncate to zero kills the file header
|
|
||||||
f.fileTableEntry.ID = nil
|
|
||||||
return fuse.OK
|
|
||||||
}
|
|
||||||
// We need the old file size to determine if we are growing or shrinking
|
|
||||||
// the file
|
|
||||||
oldSize, err := f.statPlainSize()
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
oldB := float32(oldSize) / float32(f.contentEnc.PlainBS())
|
|
||||||
newB := float32(newSize) / float32(f.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
|
|
||||||
if newSize == oldSize {
|
|
||||||
return fuse.OK
|
|
||||||
}
|
|
||||||
// File grows
|
|
||||||
if newSize > oldSize {
|
|
||||||
return f.truncateGrowFile(oldSize, newSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
// File shrinks
|
|
||||||
blockNo := f.contentEnc.PlainOffToBlockNo(newSize)
|
|
||||||
cipherOff := f.contentEnc.BlockNoToCipherOff(blockNo)
|
|
||||||
plainOff := f.contentEnc.BlockNoToPlainOff(blockNo)
|
|
||||||
lastBlockLen := newSize - plainOff
|
|
||||||
var data []byte
|
|
||||||
if lastBlockLen > 0 {
|
|
||||||
var status fuse.Status
|
|
||||||
data, status = f.doRead(nil, plainOff, lastBlockLen)
|
|
||||||
if status != fuse.OK {
|
|
||||||
tlog.Warn.Printf("Truncate: shrink doRead returned error: %v", err)
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Truncate down to the last complete block
|
|
||||||
err = syscall.Ftruncate(int(f.fd.Fd()), int64(cipherOff))
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("Truncate: shrink Ftruncate returned error: %v", err)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// Append partial block
|
|
||||||
if lastBlockLen > 0 {
|
|
||||||
_, status := f.doWrite(data, int64(plainOff))
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
return fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// statPlainSize stats the file and returns the plaintext size
|
|
||||||
func (f *File) statPlainSize() (uint64, error) {
|
|
||||||
fi, err := f.fd.Stat()
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("ino%d fh%d: statPlainSize: %v", f.qIno.Ino, f.intFd(), err)
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
cipherSz := uint64(fi.Size())
|
|
||||||
plainSz := uint64(f.contentEnc.CipherSizeToPlainSize(cipherSz))
|
|
||||||
return plainSz, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// truncateGrowFile extends a file using seeking or ftruncate performing RMW on
|
|
||||||
// the first and last block as necessary. New blocks in the middle become
|
|
||||||
// file holes unless they have been fallocate()'d beforehand.
|
|
||||||
func (f *File) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) fuse.Status {
|
|
||||||
if newPlainSz <= oldPlainSz {
|
|
||||||
log.Panicf("BUG: newSize=%d <= oldSize=%d", newPlainSz, oldPlainSz)
|
|
||||||
}
|
|
||||||
newEOFOffset := newPlainSz - 1
|
|
||||||
if oldPlainSz > 0 {
|
|
||||||
n1 := f.contentEnc.PlainOffToBlockNo(oldPlainSz - 1)
|
|
||||||
n2 := f.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 {
|
|
||||||
buf := make([]byte, 1)
|
|
||||||
_, status := f.doWrite(buf, int64(newEOFOffset))
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The truncate creates at least one new block.
|
|
||||||
//
|
|
||||||
// Make sure the old last block is padded to the block boundary. This call
|
|
||||||
// is a no-op if it is already block-aligned.
|
|
||||||
status := f.zeroPad(oldPlainSz)
|
|
||||||
if !status.Ok() {
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
// 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 {
|
|
||||||
// The file was empty, so it did not have a header. Create one.
|
|
||||||
if oldPlainSz == 0 {
|
|
||||||
id, err := f.createHeader()
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
f.fileTableEntry.ID = id
|
|
||||||
}
|
|
||||||
cSz := int64(f.contentEnc.PlainSizeToCipherSize(newPlainSz))
|
|
||||||
err := syscall.Ftruncate(f.intFd(), cSz)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("Truncate: grow Ftruncate returned error: %v", err)
|
|
||||||
}
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// The new size is NOT aligned, so we need to write a partial block.
|
|
||||||
// Write a single zero to the last byte and let doWrite figure it out.
|
|
||||||
buf := make([]byte, 1)
|
|
||||||
_, status = f.doWrite(buf, int64(newEOFOffset))
|
|
||||||
return status
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
package fusefrontend
|
|
||||||
|
|
||||||
// Helper functions for sparse files (files with holes)
|
|
||||||
|
|
||||||
import (
|
|
||||||
"runtime"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Will a write to plaintext offset "targetOff" create a file hole in the
|
|
||||||
// ciphertext? If yes, zero-pad the last ciphertext block.
|
|
||||||
func (f *File) writePadHole(targetOff int64) fuse.Status {
|
|
||||||
// Get the current file size.
|
|
||||||
fi, err := f.fd.Stat()
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("checkAndPadHole: Fstat failed: %v", err)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
plainSize := f.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)
|
|
||||||
// targetBlock is the block the user wants to write to.
|
|
||||||
targetBlock := f.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 {
|
|
||||||
return fuse.OK
|
|
||||||
}
|
|
||||||
// The write goes past the next block. nextBlock has
|
|
||||||
// to be zero-padded to the block boundary and (at least) nextBlock+1
|
|
||||||
// will contain a file hole in the ciphertext.
|
|
||||||
status := f.zeroPad(plainSize)
|
|
||||||
if status != fuse.OK {
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
return fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) fuse.Status {
|
|
||||||
lastBlockLen := plainSize % f.contentEnc.PlainBS()
|
|
||||||
if lastBlockLen == 0 {
|
|
||||||
// Already block-aligned
|
|
||||||
return fuse.OK
|
|
||||||
}
|
|
||||||
missing := f.contentEnc.PlainBS() - lastBlockLen
|
|
||||||
pad := make([]byte, missing)
|
|
||||||
tlog.Debug.Printf("zeroPad: Writing %d bytes\n", missing)
|
|
||||||
_, status := f.doWrite(pad, int64(plainSize))
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
|
|
||||||
// SeekData calls the lseek syscall with SEEK_DATA. It returns the offset of the
|
|
||||||
// next data bytes, skipping over file holes.
|
|
||||||
func (f *File) SeekData(oldOffset int64) (int64, error) {
|
|
||||||
if runtime.GOOS != "linux" {
|
|
||||||
// Does MacOS support something like this?
|
|
||||||
return 0, syscall.EOPNOTSUPP
|
|
||||||
}
|
|
||||||
const SEEK_DATA = 3
|
|
||||||
|
|
||||||
// Convert plaintext offset to ciphertext offset and round down to the
|
|
||||||
// start of the current block. File holes smaller than a full block will
|
|
||||||
// be ignored.
|
|
||||||
blockNo := f.contentEnc.PlainOffToBlockNo(uint64(oldOffset))
|
|
||||||
oldCipherOff := int64(f.contentEnc.BlockNoToCipherOff(blockNo))
|
|
||||||
|
|
||||||
// Determine the next data offset. If the old offset points to (or beyond)
|
|
||||||
// the end of the file, the Seek syscall fails with syscall.ENXIO.
|
|
||||||
newCipherOff, err := syscall.Seek(f.intFd(), oldCipherOff, SEEK_DATA)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert ciphertext offset back to plaintext offset. At this point,
|
|
||||||
// newCipherOff should always be >= contentenc.HeaderLen. Round down,
|
|
||||||
// but ensure that the result is never smaller than the initial offset
|
|
||||||
// (to avoid endless loops).
|
|
||||||
blockNo = f.contentEnc.CipherOffToBlockNo(uint64(newCipherOff))
|
|
||||||
newOffset := int64(f.contentEnc.BlockNoToPlainOff(blockNo))
|
|
||||||
if newOffset < oldOffset {
|
|
||||||
newOffset = oldOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
return newOffset, nil
|
|
||||||
}
|
|
|
@ -1,692 +0,0 @@
|
||||||
// Package fusefrontend interfaces directly with the go-fuse library.
|
|
||||||
package fusefrontend
|
|
||||||
|
|
||||||
// FUSE operations on paths
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
|
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/configfile"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/contentenc"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/inomap"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/serialize_reads"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FS implements the go-fuse virtual filesystem interface.
|
|
||||||
type FS struct {
|
|
||||||
// Embed pathfs.defaultFileSystem to avoid compile failure when the
|
|
||||||
// pathfs.FileSystem interface gets new functions. defaultFileSystem
|
|
||||||
// provides a no-op implementation for all functions.
|
|
||||||
pathfs.FileSystem
|
|
||||||
args Args // Stores configuration arguments
|
|
||||||
// dirIVLock: Lock()ed if any "gocryptfs.diriv" file is modified
|
|
||||||
// Readers must RLock() it to prevent them from seeing intermediate
|
|
||||||
// states
|
|
||||||
dirIVLock sync.RWMutex
|
|
||||||
// Filename encryption helper
|
|
||||||
nameTransform nametransform.NameTransformer
|
|
||||||
// Content encryption helper
|
|
||||||
contentEnc *contentenc.ContentEnc
|
|
||||||
// This lock is used by openWriteOnlyFile() to block concurrent opens while
|
|
||||||
// it relaxes the permissions on a file.
|
|
||||||
openWriteOnlyLock sync.RWMutex
|
|
||||||
// MitigatedCorruptions is used to report data corruption that is internally
|
|
||||||
// mitigated by ignoring the corrupt item. For example, when OpenDir() finds
|
|
||||||
// a corrupt filename, we still return the other valid filenames.
|
|
||||||
// The corruption is logged to syslog to inform the user, and in addition,
|
|
||||||
// the corrupt filename is logged to this channel via
|
|
||||||
// reportMitigatedCorruption().
|
|
||||||
// "gocryptfs -fsck" reads from the channel to also catch these transparently-
|
|
||||||
// mitigated corruptions.
|
|
||||||
MitigatedCorruptions chan string
|
|
||||||
// This flag is set to zero each time fs.isFiltered() is called
|
|
||||||
// (uint32 so that it can be reset with CompareAndSwapUint32).
|
|
||||||
// When -idle was used when mounting, idleMonitor() sets it to 1
|
|
||||||
// periodically.
|
|
||||||
IsIdle uint32
|
|
||||||
// dirCache caches directory fds
|
|
||||||
dirCache dirCacheStruct
|
|
||||||
// inoMap translates inode numbers from different devices to unique inode
|
|
||||||
// numbers.
|
|
||||||
inoMap *inomap.InoMap
|
|
||||||
}
|
|
||||||
|
|
||||||
//var _ pathfs.FileSystem = &FS{} // Verify that interface is implemented.
|
|
||||||
|
|
||||||
// NewFS returns a new encrypted FUSE overlay filesystem.
|
|
||||||
func NewFS(args Args, c *contentenc.ContentEnc, n nametransform.NameTransformer) *FS {
|
|
||||||
if args.SerializeReads {
|
|
||||||
serialize_reads.InitSerializer()
|
|
||||||
}
|
|
||||||
if len(args.Exclude) > 0 {
|
|
||||||
tlog.Warn.Printf("Forward mode does not support -exclude")
|
|
||||||
}
|
|
||||||
var st syscall.Stat_t
|
|
||||||
err := syscall.Stat(args.Cipherdir, &st)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("NewFS: could not stat cipherdir: %v", err)
|
|
||||||
st.Dev = 0
|
|
||||||
}
|
|
||||||
return &FS{
|
|
||||||
FileSystem: pathfs.NewDefaultFileSystem(),
|
|
||||||
args: args,
|
|
||||||
nameTransform: n,
|
|
||||||
contentEnc: c,
|
|
||||||
inoMap: inomap.New(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAttr implements pathfs.Filesystem.
|
|
||||||
//
|
|
||||||
// GetAttr is symlink-safe through use of openBackingDir() and Fstatat().
|
|
||||||
func (fs *FS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr, fuse.Status) {
|
|
||||||
tlog.Debug.Printf("FS.GetAttr(%q)", relPath)
|
|
||||||
if fs.isFiltered(relPath) {
|
|
||||||
return nil, fuse.EPERM
|
|
||||||
}
|
|
||||||
dirfd, cName, err := fs.openBackingDir(relPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
var st unix.Stat_t
|
|
||||||
err = syscallcompat.Fstatat(dirfd, cName, &st, unix.AT_SYMLINK_NOFOLLOW)
|
|
||||||
syscall.Close(dirfd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
a := &fuse.Attr{}
|
|
||||||
st2 := syscallcompat.Unix2syscall(st)
|
|
||||||
fs.inoMap.TranslateStat(&st2)
|
|
||||||
a.FromStat(&st2)
|
|
||||||
if a.IsRegular() {
|
|
||||||
a.Size = fs.contentEnc.CipherSizeToPlainSize(a.Size)
|
|
||||||
} else if a.IsSymlink() {
|
|
||||||
target, _ := fs.Readlink(relPath, context)
|
|
||||||
a.Size = uint64(len(target))
|
|
||||||
}
|
|
||||||
if fs.args.ForceOwner != nil {
|
|
||||||
a.Owner = *fs.args.ForceOwner
|
|
||||||
}
|
|
||||||
return a, fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (fs *FS) 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open - FUSE call. Open already-existing file.
|
|
||||||
//
|
|
||||||
// Symlink-safe through Openat().
|
|
||||||
func (fs *FS) Open(path string, flags uint32, context *fuse.Context) (fuseFile nodefs.File, status fuse.Status) {
|
|
||||||
if fs.isFiltered(path) {
|
|
||||||
return nil, fuse.EPERM
|
|
||||||
}
|
|
||||||
newFlags := fs.mangleOpenFlags(flags)
|
|
||||||
// Taking this lock makes sure we don't race openWriteOnlyFile()
|
|
||||||
fs.openWriteOnlyLock.RLock()
|
|
||||||
defer fs.openWriteOnlyLock.RUnlock()
|
|
||||||
// Symlink-safe open
|
|
||||||
dirfd, cName, err := fs.openBackingDir(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
fd, err := syscallcompat.Openat(dirfd, cName, newFlags, 0)
|
|
||||||
// Handle a few specific errors
|
|
||||||
if err != nil {
|
|
||||||
if err == syscall.EMFILE {
|
|
||||||
var lim syscall.Rlimit
|
|
||||||
syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim)
|
|
||||||
tlog.Warn.Printf("Open %q: too many open files. Current \"ulimit -n\": %d", cName, lim.Cur)
|
|
||||||
}
|
|
||||||
if err == syscall.EACCES && (int(flags)&syscall.O_ACCMODE) == syscall.O_WRONLY {
|
|
||||||
return fs.openWriteOnlyFile(dirfd, cName, newFlags)
|
|
||||||
}
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
f := os.NewFile(uintptr(fd), cName)
|
|
||||||
return NewFile(f, fs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// openBackingFile opens the ciphertext file that backs relative plaintext
|
|
||||||
// path "relPath". Always adds O_NOFOLLOW to the flags.
|
|
||||||
func (fs *FS) openBackingFile(relPath string, flags int) (fd int, err error) {
|
|
||||||
dirfd, cName, err := fs.openBackingDir(relPath)
|
|
||||||
if err != nil {
|
|
||||||
return -1, err
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
return syscallcompat.Openat(dirfd, cName, flags|syscall.O_NOFOLLOW, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Due to RMW, we always need read permissions on the backing file. This is a
|
|
||||||
// problem if the file permissions do not allow reading (i.e. 0200 permissions).
|
|
||||||
// This function works around that problem by chmod'ing the file, obtaining a fd,
|
|
||||||
// and chmod'ing it back.
|
|
||||||
func (fs *FS) openWriteOnlyFile(dirfd int, cName string, newFlags int) (*File, fuse.Status) {
|
|
||||||
woFd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NOFOLLOW, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(woFd)
|
|
||||||
var st syscall.Stat_t
|
|
||||||
err = syscall.Fstat(woFd, &st)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// The cast to uint32 fixes a build failure on Darwin, where st.Mode is uint16.
|
|
||||||
perms := uint32(st.Mode)
|
|
||||||
// Verify that we don't have read permissions
|
|
||||||
if perms&0400 != 0 {
|
|
||||||
tlog.Warn.Printf("openWriteOnlyFile: unexpected permissions %#o, returning EPERM", perms)
|
|
||||||
return nil, fuse.ToStatus(syscall.EPERM)
|
|
||||||
}
|
|
||||||
// Upgrade the lock to block other Open()s and downgrade again on return
|
|
||||||
fs.openWriteOnlyLock.RUnlock()
|
|
||||||
fs.openWriteOnlyLock.Lock()
|
|
||||||
defer func() {
|
|
||||||
fs.openWriteOnlyLock.Unlock()
|
|
||||||
fs.openWriteOnlyLock.RLock()
|
|
||||||
}()
|
|
||||||
// Relax permissions and revert on return
|
|
||||||
err = syscall.Fchmod(woFd, perms|0400)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("openWriteOnlyFile: changing permissions failed: %v", err)
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err2 := syscall.Fchmod(woFd, perms)
|
|
||||||
if err2 != nil {
|
|
||||||
tlog.Warn.Printf("openWriteOnlyFile: reverting permissions failed: %v", err2)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
rwFd, err := syscallcompat.Openat(dirfd, cName, newFlags, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
f := os.NewFile(uintptr(rwFd), cName)
|
|
||||||
return NewFile(f, fs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create - FUSE call. Creates a new file.
|
|
||||||
//
|
|
||||||
// Symlink-safe through the use of Openat().
|
|
||||||
func (fs *FS) Create(path string, flags uint32, mode uint32, context *fuse.Context) (nodefs.File, fuse.Status) {
|
|
||||||
if fs.isFiltered(path) {
|
|
||||||
return nil, fuse.EPERM
|
|
||||||
}
|
|
||||||
newFlags := fs.mangleOpenFlags(flags)
|
|
||||||
dirfd, cName, err := fs.openBackingDir(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
fd := -1
|
|
||||||
// Make sure context is nil if we don't want to preserve the owner
|
|
||||||
if !fs.args.PreserveOwner {
|
|
||||||
context = nil
|
|
||||||
}
|
|
||||||
// Handle long file name
|
|
||||||
if !fs.args.PlaintextNames && nametransform.IsLongContent(cName) {
|
|
||||||
// Create ".name"
|
|
||||||
err = fs.nameTransform.WriteLongNameAt(dirfd, cName, path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// Create content
|
|
||||||
fd, err = syscallcompat.OpenatUser(dirfd, cName, newFlags|syscall.O_CREAT|syscall.O_EXCL, mode, context)
|
|
||||||
if err != nil {
|
|
||||||
nametransform.DeleteLongNameAt(dirfd, cName)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Create content, normal (short) file name
|
|
||||||
fd, err = syscallcompat.OpenatUser(dirfd, cName, newFlags|syscall.O_CREAT|syscall.O_EXCL, mode, context)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
// xfstests generic/488 triggers this
|
|
||||||
if err == syscall.EMFILE {
|
|
||||||
var lim syscall.Rlimit
|
|
||||||
syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim)
|
|
||||||
tlog.Warn.Printf("Create %q: too many open files. Current \"ulimit -n\": %d", cName, lim.Cur)
|
|
||||||
}
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
f := os.NewFile(uintptr(fd), cName)
|
|
||||||
return NewFile(f, fs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chmod - FUSE call. Change permissions on "path".
|
|
||||||
//
|
|
||||||
// Symlink-safe through use of Fchmodat().
|
|
||||||
func (fs *FS) Chmod(path string, mode uint32, context *fuse.Context) (code fuse.Status) {
|
|
||||||
if fs.isFiltered(path) {
|
|
||||||
return fuse.EPERM
|
|
||||||
}
|
|
||||||
dirfd, cName, err := fs.openBackingDir(path)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
// os.Chmod goes through the "syscallMode" translation function that messes
|
|
||||||
// up the suid and sgid bits. So use a syscall directly.
|
|
||||||
err = syscallcompat.FchmodatNofollow(dirfd, cName, mode)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chown - FUSE call. Change the owner of "path".
|
|
||||||
//
|
|
||||||
// Symlink-safe through use of Fchownat().
|
|
||||||
func (fs *FS) Chown(path string, uid uint32, gid uint32, context *fuse.Context) (code fuse.Status) {
|
|
||||||
if fs.isFiltered(path) {
|
|
||||||
return fuse.EPERM
|
|
||||||
}
|
|
||||||
dirfd, cName, err := fs.openBackingDir(path)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
err = syscallcompat.Fchownat(dirfd, cName, int(uid), int(gid), unix.AT_SYMLINK_NOFOLLOW)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mknod - FUSE call. Create a device file.
|
|
||||||
//
|
|
||||||
// Symlink-safe through use of Mknodat().
|
|
||||||
func (fs *FS) Mknod(path string, mode uint32, dev uint32, context *fuse.Context) (code fuse.Status) {
|
|
||||||
if fs.isFiltered(path) {
|
|
||||||
return fuse.EPERM
|
|
||||||
}
|
|
||||||
dirfd, cName, err := fs.openBackingDir(path)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
// Make sure context is nil if we don't want to preserve the owner
|
|
||||||
if !fs.args.PreserveOwner {
|
|
||||||
context = nil
|
|
||||||
}
|
|
||||||
// Create ".name" file to store long file name (except in PlaintextNames mode)
|
|
||||||
if !fs.args.PlaintextNames && nametransform.IsLongContent(cName) {
|
|
||||||
err = fs.nameTransform.WriteLongNameAt(dirfd, cName, path)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// Create "gocryptfs.longfile." device node
|
|
||||||
err = syscallcompat.MknodatUser(dirfd, cName, mode, int(dev), context)
|
|
||||||
if err != nil {
|
|
||||||
nametransform.DeleteLongNameAt(dirfd, cName)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Create regular device node
|
|
||||||
err = syscallcompat.MknodatUser(dirfd, cName, mode, int(dev), context)
|
|
||||||
}
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate - FUSE call. Truncates a file.
|
|
||||||
//
|
|
||||||
// Support truncate(2) by opening the file and calling ftruncate(2)
|
|
||||||
// While the glibc "truncate" wrapper seems to always use ftruncate, fsstress from
|
|
||||||
// xfstests uses this a lot by calling "truncate64" directly.
|
|
||||||
//
|
|
||||||
// Symlink-safe by letting file.Truncate() do all the work.
|
|
||||||
func (fs *FS) Truncate(path string, offset uint64, context *fuse.Context) (code fuse.Status) {
|
|
||||||
file, code := fs.Open(path, uint32(os.O_RDWR), context)
|
|
||||||
if code != fuse.OK {
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
code = file.Truncate(offset)
|
|
||||||
file.Release()
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utimens - FUSE call. Set the timestamps on file "path".
|
|
||||||
//
|
|
||||||
// Symlink-safe through UtimesNanoAt.
|
|
||||||
func (fs *FS) Utimens(path string, a *time.Time, m *time.Time, context *fuse.Context) (code fuse.Status) {
|
|
||||||
if fs.isFiltered(path) {
|
|
||||||
return fuse.EPERM
|
|
||||||
}
|
|
||||||
dirfd, cName, err := fs.openBackingDir(path)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
err = syscallcompat.UtimesNanoAtNofollow(dirfd, cName, a, m)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StatFs - FUSE call. Returns information about the filesystem.
|
|
||||||
//
|
|
||||||
// Symlink-safe because the passed path is ignored.
|
|
||||||
func (fs *FS) StatFs(path string) *fuse.StatfsOut {
|
|
||||||
var st syscall.Statfs_t
|
|
||||||
err := syscall.Statfs(fs.args.Cipherdir, &st)
|
|
||||||
if err == nil {
|
|
||||||
var out fuse.StatfsOut
|
|
||||||
out.FromStatfsT(&st)
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// decryptSymlinkTarget: "cData64" is base64-decoded and decrypted
|
|
||||||
// like file contents (GCM).
|
|
||||||
// The empty string decrypts to the empty string.
|
|
||||||
//
|
|
||||||
// This function does not do any I/O and is hence symlink-safe.
|
|
||||||
func (fs *FS) decryptSymlinkTarget(cData64 string) (string, error) {
|
|
||||||
if cData64 == "" {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
cData, err := fs.nameTransform.B64DecodeString(cData64)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
data, err := fs.contentEnc.DecryptBlock([]byte(cData), 0, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(data), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Readlink - FUSE call.
|
|
||||||
//
|
|
||||||
// Symlink-safe through openBackingDir() + Readlinkat().
|
|
||||||
func (fs *FS) Readlink(relPath string, context *fuse.Context) (out string, status fuse.Status) {
|
|
||||||
dirfd, cName, err := fs.openBackingDir(relPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
cTarget, err := syscallcompat.Readlinkat(dirfd, cName)
|
|
||||||
if err != nil {
|
|
||||||
return "", fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
if fs.args.PlaintextNames {
|
|
||||||
return cTarget, fuse.OK
|
|
||||||
}
|
|
||||||
// Symlinks are encrypted like file contents (GCM) and base64-encoded
|
|
||||||
target, err := fs.decryptSymlinkTarget(cTarget)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("Readlink %q: decrypting target failed: %v", cName, err)
|
|
||||||
return "", fuse.EIO
|
|
||||||
}
|
|
||||||
return string(target), fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlink - FUSE call. Delete a file.
|
|
||||||
//
|
|
||||||
// Symlink-safe through use of Unlinkat().
|
|
||||||
func (fs *FS) Unlink(path string, context *fuse.Context) (code fuse.Status) {
|
|
||||||
if fs.isFiltered(path) {
|
|
||||||
return fuse.EPERM
|
|
||||||
}
|
|
||||||
dirfd, cName, err := fs.openBackingDir(path)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
// Delete content
|
|
||||||
err = syscallcompat.Unlinkat(dirfd, cName, 0)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// Delete ".name" file
|
|
||||||
if !fs.args.PlaintextNames && nametransform.IsLongContent(cName) {
|
|
||||||
err = nametransform.DeleteLongNameAt(dirfd, cName)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("Unlink: could not delete .name file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// encryptSymlinkTarget: "data" is encrypted like file contents (GCM)
|
|
||||||
// and base64-encoded.
|
|
||||||
// The empty string encrypts to the empty string.
|
|
||||||
//
|
|
||||||
// Symlink-safe because it does not do any I/O.
|
|
||||||
func (fs *FS) encryptSymlinkTarget(data string) (cData64 string) {
|
|
||||||
if data == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
cData := fs.contentEnc.EncryptBlock([]byte(data), 0, nil)
|
|
||||||
cData64 = fs.nameTransform.B64EncodeToString(cData)
|
|
||||||
return cData64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Symlink - FUSE call. Create a symlink.
|
|
||||||
//
|
|
||||||
// Symlink-safe through use of Symlinkat.
|
|
||||||
func (fs *FS) Symlink(target string, linkName string, context *fuse.Context) (code fuse.Status) {
|
|
||||||
tlog.Debug.Printf("Symlink(\"%s\", \"%s\")", target, linkName)
|
|
||||||
if fs.isFiltered(linkName) {
|
|
||||||
return fuse.EPERM
|
|
||||||
}
|
|
||||||
dirfd, cName, err := fs.openBackingDir(linkName)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
// Make sure context is nil if we don't want to preserve the owner
|
|
||||||
if !fs.args.PreserveOwner {
|
|
||||||
context = nil
|
|
||||||
}
|
|
||||||
cTarget := target
|
|
||||||
if !fs.args.PlaintextNames {
|
|
||||||
// Symlinks are encrypted like file contents (GCM) and base64-encoded
|
|
||||||
cTarget = fs.encryptSymlinkTarget(target)
|
|
||||||
}
|
|
||||||
// Create ".name" file to store long file name (except in PlaintextNames mode)
|
|
||||||
if !fs.args.PlaintextNames && nametransform.IsLongContent(cName) {
|
|
||||||
err = fs.nameTransform.WriteLongNameAt(dirfd, cName, linkName)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// Create "gocryptfs.longfile." symlink
|
|
||||||
err = syscallcompat.SymlinkatUser(cTarget, dirfd, cName, context)
|
|
||||||
if err != nil {
|
|
||||||
nametransform.DeleteLongNameAt(dirfd, cName)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Create symlink
|
|
||||||
err = syscallcompat.SymlinkatUser(cTarget, dirfd, cName, context)
|
|
||||||
}
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rename - FUSE call.
|
|
||||||
//
|
|
||||||
// Symlink-safe through Renameat().
|
|
||||||
func (fs *FS) Rename(oldPath string, newPath string, context *fuse.Context) (code fuse.Status) {
|
|
||||||
defer fs.dirCache.Clear()
|
|
||||||
if fs.isFiltered(newPath) {
|
|
||||||
return fuse.EPERM
|
|
||||||
}
|
|
||||||
oldDirfd, oldCName, err := fs.openBackingDir(oldPath)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(oldDirfd)
|
|
||||||
newDirfd, newCName, err := fs.openBackingDir(newPath)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(newDirfd)
|
|
||||||
// Easy case.
|
|
||||||
if fs.args.PlaintextNames {
|
|
||||||
return fuse.ToStatus(syscallcompat.Renameat(oldDirfd, oldCName, newDirfd, newCName))
|
|
||||||
}
|
|
||||||
// Long destination file name: create .name file
|
|
||||||
nameFileAlreadyThere := false
|
|
||||||
if nametransform.IsLongContent(newCName) {
|
|
||||||
err = fs.nameTransform.WriteLongNameAt(newDirfd, newCName, newPath)
|
|
||||||
// Failure to write the .name file is expected when the target path already
|
|
||||||
// exists. Since hashes are pretty unique, there is no need to modify the
|
|
||||||
// .name file in this case, and we ignore the error.
|
|
||||||
if err == syscall.EEXIST {
|
|
||||||
nameFileAlreadyThere = true
|
|
||||||
} else if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Actual rename
|
|
||||||
tlog.Debug.Printf("Renameat %d/%s -> %d/%s\n", oldDirfd, oldCName, newDirfd, newCName)
|
|
||||||
err = syscallcompat.Renameat(oldDirfd, oldCName, newDirfd, newCName)
|
|
||||||
if err == syscall.ENOTEMPTY || err == syscall.EEXIST {
|
|
||||||
// If an empty directory is overwritten we will always get an error as
|
|
||||||
// the "empty" directory will still contain gocryptfs.diriv.
|
|
||||||
// Interestingly, ext4 returns ENOTEMPTY while xfs returns EEXIST.
|
|
||||||
// We handle that by trying to fs.Rmdir() the target directory and trying
|
|
||||||
// again.
|
|
||||||
tlog.Debug.Printf("Rename: Handling ENOTEMPTY")
|
|
||||||
if fs.Rmdir(newPath, context) == fuse.OK {
|
|
||||||
err = syscallcompat.Renameat(oldDirfd, oldCName, newDirfd, newCName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if nametransform.IsLongContent(newCName) && nameFileAlreadyThere == false {
|
|
||||||
// Roll back .name creation unless the .name file was already there
|
|
||||||
nametransform.DeleteLongNameAt(newDirfd, newCName)
|
|
||||||
}
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
if nametransform.IsLongContent(oldCName) {
|
|
||||||
nametransform.DeleteLongNameAt(oldDirfd, oldCName)
|
|
||||||
}
|
|
||||||
return fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// Link - FUSE call. Creates a hard link at "newPath" pointing to file
|
|
||||||
// "oldPath".
|
|
||||||
//
|
|
||||||
// Symlink-safe through use of Linkat().
|
|
||||||
func (fs *FS) Link(oldPath string, newPath string, context *fuse.Context) (code fuse.Status) {
|
|
||||||
if fs.isFiltered(newPath) {
|
|
||||||
return fuse.EPERM
|
|
||||||
}
|
|
||||||
oldDirFd, cOldName, err := fs.openBackingDir(oldPath)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(oldDirFd)
|
|
||||||
newDirFd, cNewName, err := fs.openBackingDir(newPath)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(newDirFd)
|
|
||||||
// Handle long file name (except in PlaintextNames mode)
|
|
||||||
if !fs.args.PlaintextNames && nametransform.IsLongContent(cNewName) {
|
|
||||||
err = fs.nameTransform.WriteLongNameAt(newDirFd, cNewName, newPath)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// Create "gocryptfs.longfile." link
|
|
||||||
err = syscallcompat.Linkat(oldDirFd, cOldName, newDirFd, cNewName, 0)
|
|
||||||
if err != nil {
|
|
||||||
nametransform.DeleteLongNameAt(newDirFd, cNewName)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Create regular link
|
|
||||||
err = syscallcompat.Linkat(oldDirFd, cOldName, newDirFd, cNewName, 0)
|
|
||||||
}
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Access - FUSE call. Check if a file can be accessed in the specified mode(s)
|
|
||||||
// (read, write, execute).
|
|
||||||
//
|
|
||||||
// From https://github.com/libfuse/libfuse/blob/master/include/fuse.h :
|
|
||||||
//
|
|
||||||
// > Check file access permissions
|
|
||||||
// >
|
|
||||||
// > If the 'default_permissions' mount option is given, this method is not
|
|
||||||
// > called.
|
|
||||||
//
|
|
||||||
// We always enable default_permissions when -allow_other is passed, so there
|
|
||||||
// is no need for this function to check the uid in fuse.Context.
|
|
||||||
//
|
|
||||||
// Symlink-safe through use of faccessat.
|
|
||||||
func (fs *FS) Access(relPath string, mode uint32, context *fuse.Context) (code fuse.Status) {
|
|
||||||
if fs.isFiltered(relPath) {
|
|
||||||
return fuse.EPERM
|
|
||||||
}
|
|
||||||
dirfd, cName, err := fs.openBackingDir(relPath)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
err = syscallcompat.Faccessat(dirfd, cName, mode)
|
|
||||||
syscall.Close(dirfd)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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).
|
|
||||||
// See the MitigatedCorruptions channel for more info.
|
|
||||||
func (fs *FS) reportMitigatedCorruption(item string) {
|
|
||||||
if fs.MitigatedCorruptions == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case fs.MitigatedCorruptions <- item:
|
|
||||||
case <-time.After(1 * time.Second):
|
|
||||||
tlog.Warn.Printf("BUG: reportCorruptItem: timeout")
|
|
||||||
//debug.PrintStack()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// isFiltered - check if plaintext "path" should be forbidden
|
|
||||||
//
|
|
||||||
// Prevents name clashes with internal files when file names are not encrypted
|
|
||||||
func (fs *FS) isFiltered(path string) bool {
|
|
||||||
atomic.StoreUint32(&fs.IsIdle, 0)
|
|
||||||
|
|
||||||
if !fs.args.PlaintextNames {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// gocryptfs.conf in the root directory is forbidden
|
|
||||||
if path == configfile.ConfDefaultName {
|
|
||||||
tlog.Info.Printf("The name /%s is reserved when -plaintextnames is used\n",
|
|
||||||
configfile.ConfDefaultName)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// Note: gocryptfs.diriv is NOT forbidden because diriv and plaintextnames
|
|
||||||
// are exclusive
|
|
||||||
return false
|
|
||||||
}
|
|
|
@ -1,343 +0,0 @@
|
||||||
package fusefrontend
|
|
||||||
|
|
||||||
// Mkdir and Rmdir
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"runtime"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/configfile"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/cryptocore"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
const dsStoreName = ".DS_Store"
|
|
||||||
|
|
||||||
// mkdirWithIv - create a new directory and corresponding diriv file. dirfd
|
|
||||||
// should be a handle to the parent directory, cName is the name of the new
|
|
||||||
// directory and mode specifies the access permissions to use.
|
|
||||||
func (fs *FS) mkdirWithIv(dirfd int, cName string, mode uint32, context *fuse.Context) error {
|
|
||||||
// Between the creation of the directory and the creation of gocryptfs.diriv
|
|
||||||
// the directory is inconsistent. Take the lock to prevent other readers
|
|
||||||
// from seeing it.
|
|
||||||
fs.dirIVLock.Lock()
|
|
||||||
defer fs.dirIVLock.Unlock()
|
|
||||||
err := syscallcompat.MkdiratUser(dirfd, cName, mode, &context.Caller)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dirfd2, err := syscallcompat.Openat(dirfd, cName, syscall.O_DIRECTORY|syscall.O_NOFOLLOW|syscallcompat.O_PATH, 0)
|
|
||||||
if err == nil {
|
|
||||||
// Create gocryptfs.diriv
|
|
||||||
err = nametransform.WriteDirIVAt(dirfd2)
|
|
||||||
syscall.Close(dirfd2)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
// Delete inconsistent directory (missing gocryptfs.diriv!)
|
|
||||||
err2 := syscallcompat.Unlinkat(dirfd, cName, unix.AT_REMOVEDIR)
|
|
||||||
if err2 != nil {
|
|
||||||
tlog.Warn.Printf("mkdirWithIv: rollback failed: %v", err2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mkdir - FUSE call. Create a directory at "newPath" with permissions "mode".
|
|
||||||
//
|
|
||||||
// Symlink-safe through use of Mkdirat().
|
|
||||||
func (fs *FS) Mkdir(newPath string, mode uint32, context *fuse.Context) (code fuse.Status) {
|
|
||||||
if fs.isFiltered(newPath) {
|
|
||||||
return fuse.EPERM
|
|
||||||
}
|
|
||||||
dirfd, cName, err := fs.openBackingDir(newPath)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
// Make sure context is nil if we don't want to preserve the owner
|
|
||||||
if !fs.args.PreserveOwner {
|
|
||||||
context = nil
|
|
||||||
}
|
|
||||||
if fs.args.PlaintextNames {
|
|
||||||
err = syscallcompat.MkdiratUser(dirfd, cName, mode, &context.Caller)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need write and execute permissions to create gocryptfs.diriv.
|
|
||||||
// Also, we need read permissions to open the directory (to avoid
|
|
||||||
// race-conditions between getting and setting the mode).
|
|
||||||
origMode := mode
|
|
||||||
mode = mode | 0700
|
|
||||||
|
|
||||||
// Handle long file name
|
|
||||||
if nametransform.IsLongContent(cName) {
|
|
||||||
// Create ".name"
|
|
||||||
err = fs.nameTransform.WriteLongNameAt(dirfd, cName, newPath)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create directory
|
|
||||||
err = fs.mkdirWithIv(dirfd, cName, mode, context)
|
|
||||||
if err != nil {
|
|
||||||
nametransform.DeleteLongNameAt(dirfd, cName)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err = fs.mkdirWithIv(dirfd, cName, mode, context)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Set mode
|
|
||||||
if origMode != mode {
|
|
||||||
dirfd2, err := syscallcompat.Openat(dirfd, cName,
|
|
||||||
syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("Mkdir %q: Openat failed: %v", cName, err)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd2)
|
|
||||||
|
|
||||||
var st syscall.Stat_t
|
|
||||||
err = syscall.Fstat(dirfd2, &st)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("Mkdir %q: Fstat failed: %v", cName, err)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preserve SGID bit if it was set due to inheritance.
|
|
||||||
origMode = uint32(st.Mode&^0777) | origMode
|
|
||||||
err = syscall.Fchmod(dirfd2, origMode)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("Mkdir %q: Fchmod %#o -> %#o failed: %v", cName, mode, origMode, err)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// haveDsstore return true if one of the entries in "names" is ".DS_Store".
|
|
||||||
func haveDsstore(entries []fuse.DirEntry) bool {
|
|
||||||
for _, e := range entries {
|
|
||||||
if e.Name == dsStoreName {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rmdir - FUSE call.
|
|
||||||
//
|
|
||||||
// Symlink-safe through Unlinkat() + AT_REMOVEDIR.
|
|
||||||
func (fs *FS) Rmdir(relPath string, context *fuse.Context) (code fuse.Status) {
|
|
||||||
defer fs.dirCache.Clear()
|
|
||||||
parentDirFd, cName, err := fs.openBackingDir(relPath)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(parentDirFd)
|
|
||||||
if fs.args.PlaintextNames {
|
|
||||||
// Unlinkat with AT_REMOVEDIR is equivalent to Rmdir
|
|
||||||
err = unix.Unlinkat(parentDirFd, cName, unix.AT_REMOVEDIR)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// Unless we are running as root, we need read, write and execute permissions
|
|
||||||
// to handle gocryptfs.diriv.
|
|
||||||
permWorkaround := false
|
|
||||||
var origMode uint32
|
|
||||||
if !fs.args.PreserveOwner {
|
|
||||||
var st unix.Stat_t
|
|
||||||
err = syscallcompat.Fstatat(parentDirFd, cName, &st, unix.AT_SYMLINK_NOFOLLOW)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
if st.Mode&0700 != 0700 {
|
|
||||||
tlog.Debug.Printf("Rmdir: permWorkaround")
|
|
||||||
permWorkaround = true
|
|
||||||
// This cast is needed on Darwin, where st.Mode is uint16.
|
|
||||||
origMode = uint32(st.Mode)
|
|
||||||
err = syscallcompat.FchmodatNofollow(parentDirFd, cName, origMode|0700)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Debug.Printf("Rmdir: permWorkaround: chmod failed: %v", err)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dirfd, err := syscallcompat.Openat(parentDirFd, cName,
|
|
||||||
syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Debug.Printf("Rmdir: Open: %v", err)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
// Undo the chmod if removing the directory failed. This must run before
|
|
||||||
// closing dirfd, so defer it after (defer is LIFO).
|
|
||||||
if permWorkaround {
|
|
||||||
defer func() {
|
|
||||||
if code != fuse.OK {
|
|
||||||
err = unix.Fchmod(dirfd, origMode)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("Rmdir: permWorkaround: rollback failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
retry:
|
|
||||||
// Check directory contents
|
|
||||||
children, err := syscallcompat.Getdents(dirfd)
|
|
||||||
if err == io.EOF {
|
|
||||||
// The directory is empty
|
|
||||||
tlog.Warn.Printf("Rmdir: %q: %s is missing", cName, nametransform.DirIVFilename)
|
|
||||||
err = unix.Unlinkat(parentDirFd, cName, unix.AT_REMOVEDIR)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("Rmdir: Readdirnames: %v", err)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// MacOS sprinkles .DS_Store files everywhere. This is hard to avoid for
|
|
||||||
// users, so handle it transparently here.
|
|
||||||
if runtime.GOOS == "darwin" && len(children) <= 2 && haveDsstore(children) {
|
|
||||||
err = unix.Unlinkat(dirfd, dsStoreName, 0)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("Rmdir: failed to delete blocking file %q: %v", dsStoreName, err)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
tlog.Warn.Printf("Rmdir: had to delete blocking file %q", dsStoreName)
|
|
||||||
goto retry
|
|
||||||
}
|
|
||||||
// If the directory is not empty besides gocryptfs.diriv, do not even
|
|
||||||
// attempt the dance around gocryptfs.diriv.
|
|
||||||
if len(children) > 1 {
|
|
||||||
return fuse.ToStatus(syscall.ENOTEMPTY)
|
|
||||||
}
|
|
||||||
// Move "gocryptfs.diriv" to the parent dir as "gocryptfs.diriv.rmdir.XYZ"
|
|
||||||
tmpName := fmt.Sprintf("%s.rmdir.%d", nametransform.DirIVFilename, cryptocore.RandUint64())
|
|
||||||
tlog.Debug.Printf("Rmdir: Renaming %s to %s", nametransform.DirIVFilename, tmpName)
|
|
||||||
// The directory is in an inconsistent state between rename and rmdir.
|
|
||||||
// Protect against concurrent readers.
|
|
||||||
fs.dirIVLock.Lock()
|
|
||||||
defer fs.dirIVLock.Unlock()
|
|
||||||
err = syscallcompat.Renameat(dirfd, nametransform.DirIVFilename,
|
|
||||||
parentDirFd, tmpName)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("Rmdir: Renaming %s to %s failed: %v",
|
|
||||||
nametransform.DirIVFilename, tmpName, err)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// Actual Rmdir
|
|
||||||
err = syscallcompat.Unlinkat(parentDirFd, cName, unix.AT_REMOVEDIR)
|
|
||||||
if err != nil {
|
|
||||||
// This can happen if another file in the directory was created in the
|
|
||||||
// meantime, undo the rename
|
|
||||||
err2 := syscallcompat.Renameat(parentDirFd, tmpName,
|
|
||||||
dirfd, nametransform.DirIVFilename)
|
|
||||||
if err2 != nil {
|
|
||||||
tlog.Warn.Printf("Rmdir: Rename rollback failed: %v", err2)
|
|
||||||
}
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// Delete "gocryptfs.diriv.rmdir.XYZ"
|
|
||||||
err = syscallcompat.Unlinkat(parentDirFd, tmpName, 0)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("Rmdir: Could not clean up %s: %v", tmpName, err)
|
|
||||||
}
|
|
||||||
// Delete .name file
|
|
||||||
if nametransform.IsLongContent(cName) {
|
|
||||||
nametransform.DeleteLongNameAt(parentDirFd, cName)
|
|
||||||
}
|
|
||||||
return fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenDir - FUSE call
|
|
||||||
//
|
|
||||||
// This function is symlink-safe through use of openBackingDir() and
|
|
||||||
// ReadDirIVAt().
|
|
||||||
func (fs *FS) OpenDir(dirName string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) {
|
|
||||||
tlog.Debug.Printf("OpenDir(%s)", dirName)
|
|
||||||
parentDirFd, cDirName, err := fs.openBackingDir(dirName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(parentDirFd)
|
|
||||||
// Read ciphertext directory
|
|
||||||
var cipherEntries []fuse.DirEntry
|
|
||||||
var status fuse.Status
|
|
||||||
fd, err := syscallcompat.Openat(parentDirFd, cDirName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(fd)
|
|
||||||
cipherEntries, err = syscallcompat.Getdents(fd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// Get DirIV (stays nil if PlaintextNames is used)
|
|
||||||
var cachedIV []byte
|
|
||||||
if !fs.args.PlaintextNames {
|
|
||||||
// Read the DirIV from disk
|
|
||||||
cachedIV, err = nametransform.ReadDirIVAt(fd)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("OpenDir %q: could not read %s: %v", cDirName, nametransform.DirIVFilename, err)
|
|
||||||
return nil, fuse.EIO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Decrypted directory entries
|
|
||||||
var plain []fuse.DirEntry
|
|
||||||
// Filter and decrypt filenames
|
|
||||||
for i := range cipherEntries {
|
|
||||||
cName := cipherEntries[i].Name
|
|
||||||
if dirName == "" && cName == configfile.ConfDefaultName {
|
|
||||||
// silently ignore "gocryptfs.conf" in the top level dir
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if fs.args.PlaintextNames {
|
|
||||||
plain = append(plain, cipherEntries[i])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if cName == nametransform.DirIVFilename {
|
|
||||||
// silently ignore "gocryptfs.diriv" everywhere if dirIV is enabled
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Handle long file name
|
|
||||||
isLong := nametransform.LongNameNone
|
|
||||||
if fs.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)
|
|
||||||
fs.reportMitigatedCorruption(cName)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cName = cNameLong
|
|
||||||
} else if isLong == nametransform.LongNameFilename {
|
|
||||||
// ignore "gocryptfs.longname.*.name"
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name, err := fs.nameTransform.DecryptName(cName, cachedIV)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("OpenDir %q: invalid entry %q: %v",
|
|
||||||
cDirName, cName, err)
|
|
||||||
fs.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 plain, status
|
|
||||||
}
|
|
|
@ -20,6 +20,18 @@ import (
|
||||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
"github.com/rfjakob/gocryptfs/internal/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const dsStoreName = ".DS_Store"
|
||||||
|
|
||||||
|
// haveDsstore return true if one of the entries in "names" is ".DS_Store".
|
||||||
|
func haveDsstore(entries []fuse.DirEntry) bool {
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.Name == dsStoreName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// mkdirWithIv - create a new directory and corresponding diriv file. dirfd
|
// mkdirWithIv - create a new directory and corresponding diriv file. dirfd
|
||||||
// should be a handle to the parent directory, cName is the name of the new
|
// should be a handle to the parent directory, cName is the name of the new
|
||||||
// directory and mode specifies the access permissions to use.
|
// directory and mode specifies the access permissions to use.
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
package fusefrontend
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
|
||||||
)
|
|
||||||
|
|
||||||
// openBackingDir opens the parent ciphertext directory of plaintext path
|
|
||||||
// "relPath". It returns the dirfd (opened with O_PATH) and the encrypted
|
|
||||||
// basename.
|
|
||||||
//
|
|
||||||
// The caller should then use Openat(dirfd, cName, ...) and friends.
|
|
||||||
// For convenience, if relPath is "", cName is going to be ".".
|
|
||||||
//
|
|
||||||
// openBackingDir is secure against symlink races by using Openat and
|
|
||||||
// ReadDirIVAt.
|
|
||||||
func (fs *FS) openBackingDir(relPath string) (dirfd int, cName string, err error) {
|
|
||||||
dirRelPath := nametransform.Dir(relPath)
|
|
||||||
// With PlaintextNames, we don't need to read DirIVs. Easy.
|
|
||||||
if fs.args.PlaintextNames {
|
|
||||||
dirfd, err = syscallcompat.OpenDirNofollow(fs.args.Cipherdir, dirRelPath)
|
|
||||||
if err != nil {
|
|
||||||
return -1, "", err
|
|
||||||
}
|
|
||||||
// If relPath is empty, cName is ".".
|
|
||||||
cName = filepath.Base(relPath)
|
|
||||||
return dirfd, cName, nil
|
|
||||||
}
|
|
||||||
// Cache lookup
|
|
||||||
dirfd, iv := fs.dirCache.Lookup(dirRelPath)
|
|
||||||
if dirfd > 0 {
|
|
||||||
// If relPath is empty, cName is ".".
|
|
||||||
if relPath == "" {
|
|
||||||
return dirfd, ".", nil
|
|
||||||
}
|
|
||||||
name := filepath.Base(relPath)
|
|
||||||
cName, err = fs.nameTransform.EncryptAndHashName(name, iv)
|
|
||||||
if err != nil {
|
|
||||||
syscall.Close(dirfd)
|
|
||||||
return -1, "", err
|
|
||||||
}
|
|
||||||
return dirfd, cName, nil
|
|
||||||
}
|
|
||||||
// Open cipherdir (following symlinks)
|
|
||||||
dirfd, err = syscall.Open(fs.args.Cipherdir, syscall.O_DIRECTORY|syscallcompat.O_PATH, 0)
|
|
||||||
if err != nil {
|
|
||||||
return -1, "", err
|
|
||||||
}
|
|
||||||
// If relPath is empty, cName is ".".
|
|
||||||
if relPath == "" {
|
|
||||||
return dirfd, ".", nil
|
|
||||||
}
|
|
||||||
// Walk the directory tree
|
|
||||||
parts := strings.Split(relPath, "/")
|
|
||||||
for i, name := range parts {
|
|
||||||
iv, err := nametransform.ReadDirIVAt(dirfd)
|
|
||||||
if err != nil {
|
|
||||||
syscall.Close(dirfd)
|
|
||||||
return -1, "", err
|
|
||||||
}
|
|
||||||
cName, err = fs.nameTransform.EncryptAndHashName(name, iv)
|
|
||||||
if err != nil {
|
|
||||||
syscall.Close(dirfd)
|
|
||||||
return -1, "", err
|
|
||||||
}
|
|
||||||
// Last part? We are done.
|
|
||||||
if i == len(parts)-1 {
|
|
||||||
fs.dirCache.Store(dirRelPath, dirfd, iv)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// Not the last part? Descend into next directory.
|
|
||||||
dirfd2, err := syscallcompat.Openat(dirfd, cName, syscall.O_NOFOLLOW|syscall.O_DIRECTORY|syscallcompat.O_PATH, 0)
|
|
||||||
syscall.Close(dirfd)
|
|
||||||
if err != nil {
|
|
||||||
return -1, "", err
|
|
||||||
}
|
|
||||||
dirfd = dirfd2
|
|
||||||
}
|
|
||||||
return dirfd, cName, nil
|
|
||||||
}
|
|
|
@ -1,140 +0,0 @@
|
||||||
// Package fusefrontend interfaces directly with the go-fuse library.
|
|
||||||
package fusefrontend
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
const _EOPNOTSUPP = fuse.Status(syscall.EOPNOTSUPP)
|
|
||||||
|
|
||||||
// GetXAttr - FUSE call. Reads the value of extended attribute "attr".
|
|
||||||
//
|
|
||||||
// This function is symlink-safe through Fgetxattr.
|
|
||||||
func (fs *FS) GetXAttr(relPath string, attr string, context *fuse.Context) ([]byte, fuse.Status) {
|
|
||||||
if fs.isFiltered(relPath) {
|
|
||||||
return nil, fuse.EPERM
|
|
||||||
}
|
|
||||||
cAttr := fs.encryptXattrName(attr)
|
|
||||||
|
|
||||||
cData, status := fs.getXAttr(relPath, cAttr, context)
|
|
||||||
if !status.Ok() {
|
|
||||||
return nil, status
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := fs.decryptXattrValue(cData)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("GetXAttr: %v", err)
|
|
||||||
return nil, fuse.EIO
|
|
||||||
}
|
|
||||||
return data, fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetXAttr - FUSE call. Set extended attribute.
|
|
||||||
//
|
|
||||||
// This function is symlink-safe through Fsetxattr.
|
|
||||||
func (fs *FS) SetXAttr(relPath string, attr string, data []byte, flags int, context *fuse.Context) fuse.Status {
|
|
||||||
if fs.isFiltered(relPath) {
|
|
||||||
return fuse.EPERM
|
|
||||||
}
|
|
||||||
flags = filterXattrSetFlags(flags)
|
|
||||||
cAttr := fs.encryptXattrName(attr)
|
|
||||||
cData := fs.encryptXattrValue(data)
|
|
||||||
return fs.setXAttr(relPath, cAttr, cData, flags, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveXAttr - FUSE call.
|
|
||||||
//
|
|
||||||
// This function is symlink-safe through Fremovexattr.
|
|
||||||
func (fs *FS) RemoveXAttr(relPath string, attr string, context *fuse.Context) fuse.Status {
|
|
||||||
if fs.isFiltered(relPath) {
|
|
||||||
return fuse.EPERM
|
|
||||||
}
|
|
||||||
cAttr := fs.encryptXattrName(attr)
|
|
||||||
return fs.removeXAttr(relPath, cAttr, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListXAttr - FUSE call. Lists extended attributes on the file at "relPath".
|
|
||||||
//
|
|
||||||
// This function is symlink-safe through Flistxattr.
|
|
||||||
func (fs *FS) ListXAttr(relPath string, context *fuse.Context) ([]string, fuse.Status) {
|
|
||||||
if fs.isFiltered(relPath) {
|
|
||||||
return nil, fuse.EPERM
|
|
||||||
}
|
|
||||||
|
|
||||||
cNames, status := fs.listXAttr(relPath, context)
|
|
||||||
if !status.Ok() {
|
|
||||||
return nil, status
|
|
||||||
}
|
|
||||||
|
|
||||||
names := make([]string, 0, len(cNames))
|
|
||||||
for _, curName := range cNames {
|
|
||||||
if !strings.HasPrefix(curName, xattrStorePrefix) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name, err := fs.decryptXattrName(curName)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("ListXAttr: invalid xattr name %q: %v", curName, err)
|
|
||||||
fs.reportMitigatedCorruption(curName)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
return names, fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf"
|
|
||||||
func (fs *FS) encryptXattrName(attr string) (cAttr string) {
|
|
||||||
// xattr names are encrypted like file names, but with a fixed IV.
|
|
||||||
cAttr = xattrStorePrefix + fs.nameTransform.EncryptName(attr, xattrNameIV)
|
|
||||||
return cAttr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *FS) 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 = fs.nameTransform.DecryptName(cAttr, xattrNameIV)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return attr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (fs *FS) encryptXattrValue(data []byte) (cData []byte) {
|
|
||||||
if len(data) == 0 {
|
|
||||||
return []byte{}
|
|
||||||
}
|
|
||||||
return fs.contentEnc.EncryptBlock(data, 0, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// decryptXattrValue decrypts the xattr value "cData".
|
|
||||||
func (fs *FS) decryptXattrValue(cData []byte) (data []byte, err error) {
|
|
||||||
if len(cData) == 0 {
|
|
||||||
return []byte{}, nil
|
|
||||||
}
|
|
||||||
data, err1 := fs.contentEnc.DecryptBlock([]byte(cData), 0, nil)
|
|
||||||
if err1 == nil {
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
// This backward compatibility is needed to support old
|
|
||||||
// file systems having xattr values base64-encoded.
|
|
||||||
cData, err2 := fs.nameTransform.B64DecodeString(string(cData))
|
|
||||||
if err2 != nil {
|
|
||||||
// Looks like the value was not base64-encoded, but just corrupt.
|
|
||||||
// Return the original decryption error: err1
|
|
||||||
return nil, err1
|
|
||||||
}
|
|
||||||
return fs.contentEnc.DecryptBlock([]byte(cData), 0, nil)
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
// +build darwin
|
|
||||||
|
|
||||||
// Package fusefrontend interfaces directly with the go-fuse library.
|
|
||||||
package fusefrontend
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
|
||||||
)
|
|
||||||
|
|
||||||
// On Darwin it is needed 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
|
|
||||||
const XATTR_NOSECURITY = 0x0008
|
|
||||||
|
|
||||||
return flags &^ XATTR_NOSECURITY
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *FS) getXAttr(relPath string, cAttr string, context *fuse.Context) ([]byte, fuse.Status) {
|
|
||||||
// O_NONBLOCK to not block on FIFOs.
|
|
||||||
fd, err := fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_NONBLOCK)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(fd)
|
|
||||||
|
|
||||||
cData, err := syscallcompat.Fgetxattr(fd, cAttr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cData, fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *FS) setXAttr(relPath string, cAttr string, cData []byte, flags int, context *fuse.Context) fuse.Status {
|
|
||||||
// O_NONBLOCK to not block on FIFOs.
|
|
||||||
fd, err := fs.openBackingFile(relPath, syscall.O_WRONLY|syscall.O_NONBLOCK)
|
|
||||||
// Directories cannot be opened read-write. Retry.
|
|
||||||
if err == syscall.EISDIR {
|
|
||||||
fd, err = fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(fd)
|
|
||||||
|
|
||||||
err = unix.Fsetxattr(fd, cAttr, cData, flags)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *FS) removeXAttr(relPath string, cAttr string, context *fuse.Context) fuse.Status {
|
|
||||||
// O_NONBLOCK to not block on FIFOs.
|
|
||||||
fd, err := fs.openBackingFile(relPath, syscall.O_WRONLY|syscall.O_NONBLOCK)
|
|
||||||
// Directories cannot be opened read-write. Retry.
|
|
||||||
if err == syscall.EISDIR {
|
|
||||||
fd, err = fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(fd)
|
|
||||||
|
|
||||||
err = unix.Fremovexattr(fd, cAttr)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *FS) listXAttr(relPath string, context *fuse.Context) ([]string, fuse.Status) {
|
|
||||||
// O_NONBLOCK to not block on FIFOs.
|
|
||||||
fd, err := fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_NONBLOCK)
|
|
||||||
// On a symlink, openBackingFile fails with ELOOP. Let's pretend there
|
|
||||||
// can be no xattrs on symlinks, and always return an empty result.
|
|
||||||
if err == syscall.ELOOP {
|
|
||||||
return nil, fuse.OK
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(fd)
|
|
||||||
|
|
||||||
cNames, err := syscallcompat.Flistxattr(fd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
return cNames, fuse.OK
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
// +build linux
|
|
||||||
|
|
||||||
// Package fusefrontend interfaces directly with the go-fuse library.
|
|
||||||
package fusefrontend
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (fs *FS) getXAttr(relPath string, cAttr string, context *fuse.Context) ([]byte, fuse.Status) {
|
|
||||||
dirfd, cName, err := fs.openBackingDir(relPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
|
|
||||||
procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName)
|
|
||||||
cData, err := syscallcompat.Lgetxattr(procPath, cAttr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
return cData, fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *FS) setXAttr(relPath string, cAttr string, cData []byte, flags int, context *fuse.Context) fuse.Status {
|
|
||||||
dirfd, cName, err := fs.openBackingDir(relPath)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
|
|
||||||
procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName)
|
|
||||||
err = unix.Lsetxattr(procPath, cAttr, cData, flags)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *FS) removeXAttr(relPath string, cAttr string, context *fuse.Context) fuse.Status {
|
|
||||||
dirfd, cName, err := fs.openBackingDir(relPath)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
|
|
||||||
procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName)
|
|
||||||
err = unix.Lremovexattr(procPath, cAttr)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *FS) listXAttr(relPath string, context *fuse.Context) ([]string, fuse.Status) {
|
|
||||||
dirfd, cName, err := fs.openBackingDir(relPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
|
|
||||||
procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName)
|
|
||||||
cNames, err := syscallcompat.Llistxattr(procPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
return cNames, fuse.OK
|
|
||||||
}
|
|
Loading…
Reference in New Issue