Thread safety

This commit is contained in:
Matéo Duparc 2022-04-20 21:17:32 +02:00
parent b2ddf58e89
commit 985d852343
Signed by: hardcoresushi
GPG Key ID: AFE384344A45E13A
6 changed files with 139 additions and 64 deletions

View File

@ -3,4 +3,4 @@ libgocryptfs is a re-desing of the original [gocryptfs](https://github.com/rfjak
- Reduce attack surface by restricting volumes access to only one process rather than one user - Reduce attack surface by restricting volumes access to only one process rather than one user
## Warning ! ## Warning !
The only goal of this library is to be integrated in [DroidFS](https://forge.chapril.org/hardcoresushi/DroidFS). It's not actually ready for other usages. libgocryptfs doesn't implement all features provided by gocryptfs like symbolic links creation, thread-safety, reverse volume creation... Use it at your own risk ! The only goal of this library is to be integrated in [DroidFS](https://forge.chapril.org/hardcoresushi/DroidFS). It's not actually ready for other usages. libgocryptfs doesn't implement all features provided by gocryptfs like symbolic links, editing attributes, creating reverse volume... Use it at your own risk !

View File

@ -11,10 +11,11 @@ import (
//export gcf_get_attrs //export gcf_get_attrs
func gcf_get_attrs(sessionID int, relPath string) (uint64, int64, bool) { func gcf_get_attrs(sessionID int, relPath string) (uint64, int64, bool) {
volume, ok := OpenedVolumes[sessionID] value, ok := OpenedVolumes.Load(sessionID)
if !ok { if !ok {
return 0, 0, false return 0, 0, false
} }
volume := value.(*Volume)
dirfd, cName, err := volume.prepareAtSyscall(relPath) dirfd, cName, err := volume.prepareAtSyscall(relPath)
if err != nil { if err != nil {
return 0, 0, false return 0, 0, false
@ -35,10 +36,11 @@ func gcf_get_attrs(sessionID int, relPath string) (uint64, int64, bool) {
// libgocryptfs: using Renameat instead of Renameat2 to support older kernels // libgocryptfs: using Renameat instead of Renameat2 to support older kernels
//export gcf_rename //export gcf_rename
func gcf_rename(sessionID int, oldPath string, newPath string) bool { func gcf_rename(sessionID int, oldPath string, newPath string) bool {
volume, ok := OpenedVolumes[sessionID] value, ok := OpenedVolumes.Load(sessionID)
if !ok { if !ok {
return false return false
} }
volume := value.(*Volume)
dirfd, cName, err := volume.prepareAtSyscall(oldPath) dirfd, cName, err := volume.prepareAtSyscall(oldPath)
if err != nil { if err != nil {
return false return false

View File

@ -2,6 +2,7 @@ package main
import ( import (
"log" "log"
"sync"
"syscall" "syscall"
"time" "time"
) )
@ -40,6 +41,7 @@ func (e *dirCacheEntry) Clear() {
} }
type dirCache struct { type dirCache struct {
sync.Mutex
// Expected length of the stored IVs. Only used for sanity checks. // Expected length of the stored IVs. Only used for sanity checks.
// Usually set to 16, but 0 in plaintextnames mode. // Usually set to 16, but 0 in plaintextnames mode.
ivLen int ivLen int
@ -57,6 +59,8 @@ type dirCache struct {
// Clear clears the cache contents. // Clear clears the cache contents.
func (d *dirCache) Clear() { func (d *dirCache) Clear() {
d.Lock()
defer d.Unlock()
for i := range d.entries { for i := range d.entries {
d.entries[i].Clear() d.entries[i].Clear()
} }
@ -70,6 +74,8 @@ func (d *dirCache) Store(path string, fd int, iv []byte) {
if fd <= 0 || len(iv) != d.ivLen { if fd <= 0 || len(iv) != d.ivLen {
log.Panicf("Store sanity check failed: fd=%d len=%d", fd, len(iv)) log.Panicf("Store sanity check failed: fd=%d len=%d", fd, len(iv))
} }
d.Lock()
defer d.Unlock()
e := &d.entries[d.nextIndex] e := &d.entries[d.nextIndex]
// Round-robin works well enough // Round-robin works well enough
d.nextIndex = (d.nextIndex + 1) % dirCacheSize d.nextIndex = (d.nextIndex + 1) % dirCacheSize
@ -93,6 +99,8 @@ func (d *dirCache) Store(path string, fd int, iv []byte) {
// It returns (-1, nil) if not found. The fd is internally Dup()ed and the // It returns (-1, nil) if not found. The fd is internally Dup()ed and the
// caller must close it when done. // caller must close it when done.
func (d *dirCache) Lookup(path string) (fd int, iv []byte) { func (d *dirCache) Lookup(path string) (fd int, iv []byte) {
d.Lock()
defer d.Unlock()
if enableStats { if enableStats {
d.lookups++ d.lookups++
} }
@ -134,3 +142,13 @@ func (d *dirCache) expireThread() {
d.Clear() d.Clear()
} }
} }
func (d* dirCache) Delete(path string) {
for i := range d.entries {
e := &d.entries[i]
if e.path == path {
e.Clear()
break
}
}
}

View File

@ -17,10 +17,12 @@ import (
"libgocryptfs/v2/internal/syscallcompat" "libgocryptfs/v2/internal/syscallcompat"
) )
func mkdirWithIv(dirfd int, cName string, mode uint32) error { func (volume *Volume) mkdirWithIv(dirfd int, cName string, mode uint32) error {
// Between the creation of the directory and the creation of gocryptfs.diriv // Between the creation of the directory and the creation of gocryptfs.diriv
// the directory is inconsistent. Take the lock to prevent other readers // the directory is inconsistent. Take the lock to prevent other readers
// from seeing it. // from seeing it.
volume.dirIVLock.Lock()
defer volume.dirIVLock.Unlock()
err := unix.Mkdirat(dirfd, cName, mode) err := unix.Mkdirat(dirfd, cName, mode)
if err != nil { if err != nil {
return err return err
@ -40,10 +42,11 @@ func mkdirWithIv(dirfd int, cName string, mode uint32) error {
//export gcf_list_dir //export gcf_list_dir
func gcf_list_dir(sessionID int, dirName string) (*C.char, *C.int, C.int) { func gcf_list_dir(sessionID int, dirName string) (*C.char, *C.int, C.int) {
volume, ok := OpenedVolumes[sessionID] value, ok := OpenedVolumes.Load(sessionID)
if !ok { if !ok {
return nil, nil, 0 return nil, nil, 0
} }
volume := value.(*Volume)
parentDirFd, cDirName, err := volume.prepareAtSyscallMyself(dirName) parentDirFd, cDirName, err := volume.prepareAtSyscallMyself(dirName)
if err != nil { if err != nil {
return nil, nil, 0 return nil, nil, 0
@ -119,10 +122,11 @@ func gcf_list_dir(sessionID int, dirName string) (*C.char, *C.int, C.int) {
//export gcf_mkdir //export gcf_mkdir
func gcf_mkdir(sessionID int, path string, mode uint32) bool { func gcf_mkdir(sessionID int, path string, mode uint32) bool {
volume, ok := OpenedVolumes[sessionID] value, ok := OpenedVolumes.Load(sessionID)
if !ok { if !ok {
return false return false
} }
volume := value.(*Volume)
dirfd, cName, err := volume.prepareAtSyscall(path) dirfd, cName, err := volume.prepareAtSyscall(path)
if err != nil { if err != nil {
return false return false
@ -155,13 +159,13 @@ func gcf_mkdir(sessionID int, path string, mode uint32) bool {
} }
// Create directory // Create directory
err = mkdirWithIv(dirfd, cName, mode) err = volume.mkdirWithIv(dirfd, cName, mode)
if err != nil { if err != nil {
nametransform.DeleteLongNameAt(dirfd, cName) nametransform.DeleteLongNameAt(dirfd, cName)
return false return false
} }
} else { } else {
err = mkdirWithIv(dirfd, cName, mode) err = volume.mkdirWithIv(dirfd, cName, mode)
if err != nil { if err != nil {
return false return false
} }
@ -193,10 +197,11 @@ func gcf_mkdir(sessionID int, path string, mode uint32) bool {
//export gcf_rmdir //export gcf_rmdir
func gcf_rmdir(sessionID int, relPath string) bool { func gcf_rmdir(sessionID int, relPath string) bool {
volume, ok := OpenedVolumes[sessionID] value, ok := OpenedVolumes.Load(sessionID)
if !ok { if !ok {
return false return false
} }
volume := value.(*Volume)
parentDirFd, cName, err := volume.prepareAtSyscall(relPath) parentDirFd, cName, err := volume.prepareAtSyscall(relPath)
if err != nil { if err != nil {
return false return false
@ -229,6 +234,10 @@ func gcf_rmdir(sessionID int, relPath string) bool {
} }
// Move "gocryptfs.diriv" to the parent dir as "gocryptfs.diriv.rmdir.XYZ" // Move "gocryptfs.diriv" to the parent dir as "gocryptfs.diriv.rmdir.XYZ"
tmpName := fmt.Sprintf("%s.rmdir.%d", nametransform.DirIVFilename, cryptocore.RandUint64()) tmpName := fmt.Sprintf("%s.rmdir.%d", nametransform.DirIVFilename, cryptocore.RandUint64())
// The directory is in an inconsistent state between rename and rmdir.
// Protect against concurrent readers.
volume.dirIVLock.Lock()
defer volume.dirIVLock.Unlock()
err = syscallcompat.Renameat(dirfd, nametransform.DirIVFilename, parentDirFd, tmpName) err = syscallcompat.Renameat(dirfd, nametransform.DirIVFilename, parentDirFd, tmpName)
if err != nil { if err != nil {
return false return false
@ -247,5 +256,6 @@ func gcf_rmdir(sessionID int, relPath string) bool {
if nametransform.IsLongContent(cName) { if nametransform.IsLongContent(cName) {
nametransform.DeleteLongNameAt(parentDirFd, cName) nametransform.DeleteLongNameAt(parentDirFd, cName)
} }
volume.dirCache.Delete(relPath)
return true return true
} }

106
file.go
View File

@ -41,13 +41,16 @@ func (volume *Volume) registerFileHandle(fd int, cName, path string) int {
handleID := -1 handleID := -1
c := 0 c := 0
for handleID == -1 { for handleID == -1 {
_, ok := volume.file_handles[c] _, ok := volume.file_handles.Load(c)
if !ok { if !ok {
handleID = c handleID = c
} }
c++ c++
} }
volume.file_handles[handleID] = File{os.NewFile(uintptr(fd), cName), string([]byte(path[:]))} volume.file_handles.Store(handleID, &File {
fd: os.NewFile(uintptr(fd), cName),
path: string([]byte(path[:])),
})
return handleID return handleID
} }
@ -100,16 +103,17 @@ func createHeader(fd *os.File) (fileID []byte, err error) {
// Called by Read() for normal reading, // Called by Read() for normal reading,
// by Write() and Truncate() via doWrite() for Read-Modify-Write. // by Write() and Truncate() via doWrite() for Read-Modify-Write.
func (volume *Volume) doRead(handleID int, dst []byte, off uint64, length uint64) ([]byte, bool) { func (volume *Volume) doRead(handleID int, dst []byte, off uint64, length uint64) ([]byte, bool) {
f, ok := volume.file_handles[handleID] value, ok := volume.file_handles.Load(handleID)
if !ok { if !ok {
return nil, false return nil, false
} }
f := value.(*File)
fd := f.fd fd := f.fd
// Get the file ID, either from the open file table, or from disk. // Get the file ID, either from the open file table, or from disk.
var fileID []byte var fileID []byte
if volume.fileIDs[handleID] != nil { if f.ID != nil {
// Use the cached value in the file table // Use the cached value in the file table
fileID = volume.fileIDs[handleID] fileID = f.ID
} else { } else {
// Not cached, we have to read it from disk. // Not cached, we have to read it from disk.
var err error var err error
@ -118,9 +122,8 @@ func (volume *Volume) doRead(handleID int, dst []byte, off uint64, length uint64
return nil, false return nil, false
} }
// Save into the file table // Save into the file table
volume.fileIDs[handleID] = fileID f.ID = fileID
} }
// Read the backing ciphertext in one go // Read the backing ciphertext in one go
blocks := volume.contentEnc.ExplodePlainRange(off, length) blocks := volume.contentEnc.ExplodePlainRange(off, length)
alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks) alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks)
@ -180,19 +183,16 @@ func (volume *Volume) doRead(handleID int, dst []byte, off uint64, length uint64
// //
// Empty writes do nothing and are allowed. // Empty writes do nothing and are allowed.
func (volume *Volume) doWrite(handleID int, data []byte, off uint64) (uint32, bool) { func (volume *Volume) doWrite(handleID int, data []byte, off uint64) (uint32, bool) {
fileWasEmpty := false value, ok := volume.file_handles.Load(handleID)
// Get the file ID, create a new one if it does not exist yet.
var fileID []byte
f, ok := volume.file_handles[handleID]
if !ok { if !ok {
return 0, false return 0, false
} }
f := value.(*File)
fd := f.fd fd := f.fd
// The caller has exclusively locked ContentLock, which blocks all other fileWasEmpty := false
// readers and writers. No need to take IDLock. var fileID []byte
if volume.fileIDs[handleID] != nil { if f.ID != nil {
fileID = volume.fileIDs[handleID] fileID = f.ID
} else { } else {
// If the file ID is not cached, read it from disk // If the file ID is not cached, read it from disk
var err error var err error
@ -205,7 +205,7 @@ func (volume *Volume) doWrite(handleID int, data []byte, off uint64) (uint32, bo
if err != nil { if err != nil {
return 0, false return 0, false
} }
volume.fileIDs[handleID] = fileID f.ID = fileID
} }
// Handle payload data // Handle payload data
dataBuf := bytes.NewBuffer(data) dataBuf := bytes.NewBuffer(data)
@ -299,16 +299,21 @@ func (volume *Volume) truncateGrowFile(handleID int, oldPlainSz uint64, newPlain
// The new size is block-aligned. In this case we can do everything ourselves // The new size is block-aligned. In this case we can do everything ourselves
// and avoid the call to doWrite. // and avoid the call to doWrite.
if newPlainSz%volume.contentEnc.PlainBS() == 0 { if newPlainSz%volume.contentEnc.PlainBS() == 0 {
value, ok := volume.file_handles.Load(handleID)
if !ok {
return false
}
f := value.(*File)
// The file was empty, so it did not have a header. Create one. // The file was empty, so it did not have a header. Create one.
if oldPlainSz == 0 { if oldPlainSz == 0 {
id, err := createHeader(volume.file_handles[handleID].fd) id, err := createHeader(f.fd)
if err != nil { if err != nil {
return false return false
} }
volume.fileIDs[handleID] = id f.ID = id
} }
cSz := int64(volume.contentEnc.PlainSizeToCipherSize(newPlainSz)) cSz := int64(volume.contentEnc.PlainSizeToCipherSize(newPlainSz))
err := syscall.Ftruncate(int(volume.file_handles[handleID].fd.Fd()), cSz) err := syscall.Ftruncate(int(f.fd.Fd()), cSz)
return errToBool(err) return errToBool(err)
} }
// The new size is NOT aligned, so we need to write a partial block. // The new size is NOT aligned, so we need to write a partial block.
@ -319,7 +324,12 @@ func (volume *Volume) truncateGrowFile(handleID int, oldPlainSz uint64, newPlain
} }
func (volume *Volume) truncate(handleID int, newSize uint64) bool { func (volume *Volume) truncate(handleID int, newSize uint64) bool {
fileFD := int(volume.file_handles[handleID].fd.Fd()) value, ok := volume.file_handles.Load(handleID)
if !ok {
return false
}
f := value.(*File)
fileFD := int(f.fd.Fd())
var err error var err error
// Common case first: Truncate to zero // Common case first: Truncate to zero
if newSize == 0 { if newSize == 0 {
@ -328,7 +338,7 @@ func (volume *Volume) truncate(handleID int, newSize uint64) bool {
} }
// We need the old file size to determine if we are growing or shrinking // We need the old file size to determine if we are growing or shrinking
// the file // the file
oldSize, _, success := gcf_get_attrs(volume.volumeID, volume.file_handles[handleID].path) oldSize, _, success := gcf_get_attrs(volume.volumeID, f.path)
if !success { if !success {
return false return false
} }
@ -369,10 +379,11 @@ func (volume *Volume) truncate(handleID int, newSize uint64) bool {
//export gcf_open_read_mode //export gcf_open_read_mode
func gcf_open_read_mode(sessionID int, path string) int { func gcf_open_read_mode(sessionID int, path string) int {
volume, ok := OpenedVolumes[sessionID] value, ok := OpenedVolumes.Load(sessionID)
if !ok { if !ok {
return -1 return -1
} }
volume := value.(*Volume)
dirfd, cName, err := volume.prepareAtSyscallMyself(path) dirfd, cName, err := volume.prepareAtSyscallMyself(path)
if err != nil { if err != nil {
return -1 return -1
@ -389,10 +400,11 @@ func gcf_open_read_mode(sessionID int, path string) int {
//export gcf_open_write_mode //export gcf_open_write_mode
func gcf_open_write_mode(sessionID int, path string, mode uint32) int { func gcf_open_write_mode(sessionID int, path string, mode uint32) int {
volume, ok := OpenedVolumes[sessionID] value, ok := OpenedVolumes.Load(sessionID)
if !ok { if !ok {
return -1 return -1
} }
volume := value.(*Volume)
dirfd, cName, err := volume.prepareAtSyscall(path) dirfd, cName, err := volume.prepareAtSyscall(path)
if err != nil { if err != nil {
return -1 return -1
@ -425,10 +437,11 @@ func gcf_open_write_mode(sessionID int, path string, mode uint32) int {
//export gcf_truncate //export gcf_truncate
func gcf_truncate(sessionID int, handleID int, offset uint64) bool { func gcf_truncate(sessionID int, handleID int, offset uint64) bool {
volume, ok := OpenedVolumes[sessionID] value, ok := OpenedVolumes.Load(sessionID)
if !ok { if !ok {
return false return false
} }
volume := value.(*Volume)
return volume.truncate(handleID, offset) return volume.truncate(handleID, offset)
} }
@ -440,10 +453,21 @@ func gcf_read_file(sessionID, handleID int, offset uint64, dst_buff []byte) uint
return 0 return 0
} }
volume, ok := OpenedVolumes[sessionID] value, ok := OpenedVolumes.Load(sessionID)
if !ok { if !ok {
return 0 return 0
} }
volume := value.(*Volume)
value, ok = volume.file_handles.Load(handleID)
if !ok {
return 0
}
f := value.(*File)
f.fdLock.RLock()
defer f.fdLock.RUnlock()
f.contentLock.RLock()
defer f.contentLock.RUnlock()
out, success := volume.doRead(handleID, dst_buff[:0], offset, uint64(length)) out, success := volume.doRead(handleID, dst_buff[:0], offset, uint64(length))
if !success { if !success {
return 0 return 0
@ -460,37 +484,49 @@ func gcf_write_file(sessionID, handleID int, offset uint64, data []byte) uint32
return 0 return 0
} }
volume, ok := OpenedVolumes[sessionID] value, ok := OpenedVolumes.Load(sessionID)
if !ok { if !ok {
return 0 return 0
} }
volume := value.(*Volume)
value, ok = volume.file_handles.Load(handleID)
if !ok {
return 0
}
f := value.(*File)
f.fdLock.RLock()
defer f.fdLock.RUnlock()
f.contentLock.Lock()
defer f.contentLock.Unlock()
n, _ := volume.doWrite(handleID, data, offset) n, _ := volume.doWrite(handleID, data, offset)
return n return n
} }
//export gcf_close_file //export gcf_close_file
func gcf_close_file(sessionID, handleID int) { func gcf_close_file(sessionID, handleID int) {
volume, ok := OpenedVolumes[sessionID] value, ok := OpenedVolumes.Load(sessionID)
if !ok { if !ok {
return return
} }
f, ok := volume.file_handles[handleID] volume := value.(*Volume)
value, ok = volume.file_handles.Load(handleID)
if ok { if ok {
f := value.(*File)
f.fdLock.Lock()
f.fd.Close() f.fd.Close()
delete(volume.file_handles, handleID) volume.file_handles.Delete(handleID)
_, ok := volume.fileIDs[handleID] f.fdLock.Unlock()
if ok {
delete(volume.fileIDs, handleID)
}
} }
} }
//export gcf_remove_file //export gcf_remove_file
func gcf_remove_file(sessionID int, path string) bool { func gcf_remove_file(sessionID int, path string) bool {
volume, ok := OpenedVolumes[sessionID] value, ok := OpenedVolumes.Load(sessionID)
if !ok { if !ok {
return false return false
} }
volume := value.(*Volume)
dirfd, cName, err := volume.prepareAtSyscall(path) dirfd, cName, err := volume.prepareAtSyscall(path)
if err != nil { if err != nil {
return false return false

View File

@ -6,8 +6,9 @@ package main
import ( import (
"C" "C"
"os" "os"
"path/filepath" "sync"
"syscall" "syscall"
"path/filepath"
"libgocryptfs/v2/internal/configfile" "libgocryptfs/v2/internal/configfile"
"libgocryptfs/v2/internal/contentenc" "libgocryptfs/v2/internal/contentenc"
@ -18,23 +19,35 @@ import (
) )
type File struct { type File struct {
fd *os.File fd *os.File
path string path string
ID []byte
// 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
// ContentLock protects on-disk content from concurrent writes. Every writer
// must take this lock before modifying the file content.
contentLock sync.RWMutex
} }
type Volume struct { type Volume struct {
volumeID int volumeID int
rootCipherDir string rootCipherDir string
plainTextNames bool plainTextNames bool
// dirIVLock: Lock()ed if any "gocryptfs.diriv" file is modified
// Readers must RLock() it to prevent them from seeing intermediate
// states
dirIVLock sync.RWMutex
nameTransform *nametransform.NameTransform nameTransform *nametransform.NameTransform
cryptoCore *cryptocore.CryptoCore cryptoCore *cryptocore.CryptoCore
contentEnc *contentenc.ContentEnc contentEnc *contentenc.ContentEnc
dirCache dirCache dirCache dirCache
file_handles map[int]File file_handles sync.Map
fileIDs map[int][]byte
} }
var OpenedVolumes map[int]*Volume var OpenedVolumes sync.Map
func wipe(d []byte) { func wipe(d []byte) {
for i := range d { for i := range d {
@ -79,25 +92,19 @@ func registerNewVolume(rootCipherDir string, masterkey []byte, cf *configfile.Co
if newVolume.plainTextNames { if newVolume.plainTextNames {
ivLen = 0 ivLen = 0
} }
// New empty caches
newVolume.dirCache = dirCache{ivLen: ivLen} newVolume.dirCache = dirCache{ivLen: ivLen}
newVolume.file_handles = make(map[int]File)
newVolume.fileIDs = make(map[int][]byte)
//find unused volumeID //find unused volumeID
volumeID := -1 volumeID := -1
c := 0 c := 0
for volumeID == -1 { for volumeID == -1 {
_, ok := OpenedVolumes[c] _, ok := OpenedVolumes.Load(c)
if !ok { if !ok {
volumeID = c volumeID = c
} }
c++ c++
} }
if OpenedVolumes == nil { OpenedVolumes.Store(volumeID, &newVolume)
OpenedVolumes = make(map[int]*Volume)
}
OpenedVolumes[volumeID] = &newVolume
return volumeID return volumeID
} }
@ -117,21 +124,23 @@ func gcf_init(rootCipherDir string, password, givenScryptHash, returnedScryptHas
//export gcf_close //export gcf_close
func gcf_close(volumeID int) { func gcf_close(volumeID int) {
volume, ok := OpenedVolumes[volumeID] value, ok := OpenedVolumes.Load(volumeID)
if !ok { if !ok {
return return
} }
volume := value.(*Volume)
volume.cryptoCore.Wipe() volume.cryptoCore.Wipe()
for handleID := range volume.file_handles { volume.file_handles.Range(func (handleID, _ interface {}) bool {
gcf_close_file(volumeID, handleID) gcf_close_file(volumeID, handleID.(int))
} return true
})
volume.dirCache.Clear() volume.dirCache.Clear()
delete(OpenedVolumes, volumeID) OpenedVolumes.Delete(volumeID)
} }
//export gcf_is_closed //export gcf_is_closed
func gcf_is_closed(volumeID int) bool { func gcf_is_closed(volumeID int) bool {
_, ok := OpenedVolumes[volumeID] _, ok := OpenedVolumes.Load(volumeID)
return !ok return !ok
} }