/* FUSE: Filesystem in Userspace Copyright (C) 2001-2007 Miklos Szeredi This program can be distributed under the terms of the GNU LGPLv2. See the file COPYING.LIB */ #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include "syslog.hpp" #include "crc32b.h" #include "debug.hpp" #include "fatal.hpp" #include "fs_path.hpp" #include "kvec.h" #include "mutex.hpp" #include "node.hpp" #include "xalloc.hpp" #include "fuse_cfg.hpp" #include "fuse_req.hpp" #include "fuse_dirents.hpp" #include "fuse_i.hpp" #include "fuse_kernel.h" #include "fuse_lowlevel.h" #include "fuse_opt.h" #include "fuse_pollhandle.h" #include "fuse_msgbuf.hpp" #include "stat_utils.h" #include "maintenance_thread.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __GLIBC__ #include #endif #define FUSE_UNKNOWN_INO UINT64_MAX #define OFFSET_MAX 0x7fffffffffffffffLL #define PATH_BUFSIZE_HALF_MAX 0x80000000u #define PATH_BUFSIZE_MAX 0xffffffffu #define INFO_FILE_MAX_SIZE (1024 * 1024) #define NODE_TABLE_MIN_SIZE 8192 #define PARAM(inarg) ((void*)(((char*)(inarg)) + sizeof(*(inarg)))) struct lock_queue_element { struct lock_queue_element *next; pthread_cond_t cond; uint64_t nodeid1; const char *name1; // path1 / path2 are non-owning pointers to the caller's stack- // allocated fs::relpath objects (try_get_path / try_get_path2 take // a relpath* and fill it in place). Lifetime is bounded by the // calling FUSE op, which blocks on `cond` until the lock holder // signals -- the caller's stack frame is therefore guaranteed to // outlive the queue entry. Do not copy a lock_queue_element to a // longer-lived storage without reconsidering this invariant. fs::relpath *path1; node_t **wnode1; uint64_t nodeid2; const char *name2; fs::relpath *path2; node_t **wnode2; int err; bool done : 1; }; struct node_table { node_t **array; size_t use; size_t size; size_t split; }; struct list_head { struct list_head *next; struct list_head *prev; }; typedef struct nodeid_gen_t nodeid_gen_t; struct nodeid_gen_t { uint64_t nodeid; uint64_t generation; }; struct fuse { struct fuse_session *se; struct node_table name_table; struct node_table id_table; nodeid_gen_t nodeid_gen; unsigned int hidectr; mutex_t lock; fuse_operations ops; struct lock_queue_element *lockq; }; #define TREELOCK_WRITE -1 #define TREELOCK_WAIT_OFFSET INT_MIN struct fuse_dh { mutex_t lock; uint64_t fh; fuse_dirents_t d; }; static struct fuse f = {}; /* Why was the nodeid:generation logic simplified? nodeid is uint64_t: max value of 18446744073709551616 If nodes were created at a rate of 1048576 per second it would take over 500 thousand years to roll over. I'm fine with risking that. */ static uint64_t generate_nodeid(nodeid_gen_t *ng_) { ng_->nodeid++; return ng_->nodeid; } static void* fuse_hdr_arg(const struct fuse_in_header *hdr_) { return (void*)&hdr_[1]; } static size_t id_hash(uint64_t ino) { uint64_t hash = ((uint32_t)ino * 2654435761U) % f.id_table.size; uint64_t oldhash = hash % (f.id_table.size / 2); if(oldhash >= f.id_table.split) return oldhash; else return hash; } static node_t* get_node(const uint64_t nodeid) { size_t hash = id_hash(nodeid); node_t *node; for(node = f.id_table.array[hash]; node != NULL; node = node->id_next) if(node->nodeid == nodeid) return node; return NULL; } static uint32_t stat_crc32b(const struct stat *st_) { uint32_t crc; const auto mtimensec = ST_MTIM_NSEC(st_); const auto ctimensec = ST_CTIM_NSEC(st_); crc = crc32b_start(); /* st_dev is not exposed by FUSE attr replies, but the underlying value is still useful as a proxy for whether the backing stat source changed. */ crc = crc32b_continue(&st_->st_dev,sizeof(st_->st_dev),crc); crc = crc32b_continue(&st_->st_ino,sizeof(st_->st_ino),crc); crc = crc32b_continue(&st_->st_size,sizeof(st_->st_size),crc); crc = crc32b_continue(&st_->st_mtime,sizeof(st_->st_mtime),crc); crc = crc32b_continue(&mtimensec,sizeof(mtimensec),crc); crc = crc32b_continue(&st_->st_ctime,sizeof(st_->st_ctime),crc); crc = crc32b_continue(&ctimensec,sizeof(ctimensec),crc); crc = crc32b_finish(crc); /* 0 is reserved to mean "no valid stat cache fingerprint". */ return (crc ? crc : UINT32_MAX); } static void free_node(node_t *node_) { free(node_->name); node_free(node_); } static void node_table_reduce(struct node_table *t) { size_t newsize = t->size / 2; void *newarray; if(newsize < NODE_TABLE_MIN_SIZE) return; newarray = realloc(t->array,sizeof(node_t*) * newsize); if(newarray == NULL) return; t->array = (node_t**)newarray; t->size = newsize; t->split = t->size / 2; } static void remerge_id() { struct node_table *t = &f.id_table; int iter; if(t->split == 0) node_table_reduce(t); for(iter = 8; t->split > 0 && iter; iter--) { node_t **upper; t->split--; upper = &t->array[t->split + t->size / 2]; if(*upper) { node_t **nodep; for(nodep = &t->array[t->split]; *nodep; nodep = &(*nodep)->id_next); *nodep = *upper; *upper = NULL; break; } } } static void unhash_id(node_t *node) { node_t **nodep = &f.id_table.array[id_hash(node->nodeid)]; for(; *nodep != NULL; nodep = &(*nodep)->id_next) if(*nodep == node) { *nodep = node->id_next; f.id_table.use--; if(f.id_table.use < (f.id_table.size / 4)) remerge_id(); return; } } static int node_table_resize(struct node_table *t) { size_t newsize = t->size * 2; void *newarray; newarray = realloc(t->array,sizeof(node_t*) * newsize); if(newarray == NULL) return -1; t->array = (node_t**)newarray; memset(t->array + t->size,0,t->size * sizeof(node_t*)); t->size = newsize; t->split = 0; return 0; } static void rehash_id() { struct node_table *t = &f.id_table; node_t **nodep; node_t **next; size_t hash; if(t->split == t->size / 2) return; hash = t->split; t->split++; for(nodep = &t->array[hash]; *nodep != NULL; nodep = next) { node_t *node = *nodep; size_t newhash = id_hash(node->nodeid); if(newhash != hash) { next = nodep; *nodep = node->id_next; node->id_next = t->array[newhash]; t->array[newhash] = node; } else { next = &node->id_next; } } if(t->split == t->size / 2) node_table_resize(t); } static void hash_id(node_t *node) { size_t hash; hash = id_hash(node->nodeid); node->id_next = f.id_table.array[hash]; f.id_table.array[hash] = node; f.id_table.use++; if(f.id_table.use >= (f.id_table.size / 2)) rehash_id(); } static size_t name_hash(uint64_t parent, const char *name) { uint64_t hash = parent; uint64_t oldhash; for(; *name; name++) hash = hash * 31 + (unsigned char)*name; hash %= f.name_table.size; oldhash = hash % (f.name_table.size / 2); if(oldhash >= f.name_table.split) return oldhash; else return hash; } static void unref_node(node_t *node); static void remerge_name() { int iter; struct node_table *t = &f.name_table; if(t->split == 0) node_table_reduce(t); for(iter = 8; t->split > 0 && iter; iter--) { node_t **upper; t->split--; upper = &t->array[t->split + t->size / 2]; if(*upper) { node_t **nodep; for(nodep = &t->array[t->split]; *nodep; nodep = &(*nodep)->name_next); *nodep = *upper; *upper = NULL; break; } } } static void unhash_name(node_t *node) { if(not (node && node->name && node->parent)) return; size_t hash = name_hash(node->parent->nodeid,node->name); node_t **nodep = &f.name_table.array[hash]; for(; *nodep != NULL; nodep = &(*nodep)->name_next) { if(*nodep != node) continue; *nodep = node->name_next; node->name_next = NULL; unref_node(node->parent); free(node->name); node->name = NULL; node->parent = NULL; f.name_table.use--; if(f.name_table.use < f.name_table.size / 4) remerge_name(); return; } fatal::abort("mergerfs: unable to unhash node: {}", node->nodeid); } static void rehash_name() { struct node_table *t = &f.name_table; node_t **nodep; node_t **next; size_t hash; if(t->split == t->size / 2) return; hash = t->split; t->split++; for(nodep = &t->array[hash]; *nodep != NULL; nodep = next) { node_t *node = *nodep; if(node->parent == NULL) { next = &node->name_next; continue; } size_t newhash = name_hash(node->parent->nodeid,node->name); if(newhash != hash) { next = nodep; *nodep = node->name_next; node->name_next = t->array[newhash]; t->array[newhash] = node; } else { next = &node->name_next; } } if(t->split == t->size / 2) node_table_resize(t); } static int hash_name(node_t *node, uint64_t parentid, const char *name) { size_t hash; node_t *parent; hash = name_hash(parentid,name); parent = get_node(parentid); if(parent == NULL) return -1; node->name = strdup(name); if(node->name == NULL) return -1; parent->refctr++; node->parent = parent; node->name_next = f.name_table.array[hash]; f.name_table.array[hash] = node; f.name_table.use++; if(f.name_table.use >= f.name_table.size / 2) rehash_name(); return 0; } static inline bool remember_nodes() { return fuse_cfg.remember_nodes; } static void delete_node(node_t *node) { assert(node->treelock == 0); unhash_name(node); unhash_id(node); node_free(node); } static void unref_node(node_t *node) { assert(node->refctr > 0); node->refctr--; if(!node->refctr) delete_node(node); } static uint64_t rand64(void) { uint64_t rv; rv = rand(); rv <<= 32; rv |= rand(); return rv; } static node_t* lookup_node(uint64_t parent, const char *name) { size_t hash; node_t *node; hash = name_hash(parent,name); for(node = f.name_table.array[hash]; node != NULL; node = node->name_next) if(node->parent->nodeid == parent && strcmp(node->name,name) == 0) return node; return NULL; } static void inc_nlookup(node_t *node) { if(!node->nlookup) node->refctr++; node->nlookup++; } static void remember_node(node_t *node) { if(node->remembered) return; inc_nlookup(node); node->remembered = true; } static void unref_node_if_unused(node_t *node) { if((node->nlookup == 0) && (node->open_count == 0) && (node->treelock == 0)) unref_node(node); } static void unremember_node(node_t *node) { if(!node->remembered) return; assert(node->nlookup > 0); node->remembered = false; node->nlookup--; unref_node_if_unused(node); } static node_t* find_node(uint64_t parent, const char *name, bool remember) { node_t *node; mutex_lockguard(f.lock); if(!name) { node = get_node(parent); if(node == NULL) goto out_err; } else { node = lookup_node(parent,name); } if(node == NULL) { node = node_alloc(); if(node == NULL) goto out_err; node->nodeid = generate_nodeid(&f.nodeid_gen); if(remember && remember_nodes()) remember_node(node); if(hash_name(node,parent,name) == -1) { free_node(node); node = NULL; goto out_err; } hash_id(node); } inc_nlookup(node); out_err: return node; } static void unlock_path(uint64_t nodeid, node_t *wnode, node_t *end) { node_t *node; if(wnode) { assert(wnode->treelock == TREELOCK_WRITE); wnode->treelock = 0; /* unlink_node() can drop the retained lookup while the node is locked. */ unref_node_if_unused(wnode); } node = get_node(nodeid); if(node == NULL) return; for(;node != end && node->nodeid != FUSE_ROOT_ID; node = node->parent) { assert(node->treelock != 0); assert(node->treelock != TREELOCK_WAIT_OFFSET); assert(node->treelock != TREELOCK_WRITE); node->treelock--; if(node->treelock == TREELOCK_WAIT_OFFSET) node->treelock = 0; } } /* Path storage is the caller's stack-allocated fs::relpath; this function fills it via prepend() (walk leaf-to-root, prepending "/component"). No heap allocation per op. Final step before returning: consolidate_to_rel(true) moves the data to the rel anchor (REL_OFFSET) and strips the leading '/'. Callers (f.ops.* callbacks) receive a REL-layout fs::relpath with no leading slash, ready for set_prefix-style assembly in mergerfs's per-branch loops without further conversion. */ static int try_get_path(uint64_t nodeid, const char *name, fs::relpath *path, node_t **wnodep, bool need_lock) { node_t *node; node_t *wnode = NULL; int err; path->clear(); if(name != NULL) path->prepend(name); if(wnodep) { assert(need_lock); wnode = lookup_node(nodeid,name); if(wnode) { if(wnode->treelock != 0) { if(wnode->treelock > 0) wnode->treelock += TREELOCK_WAIT_OFFSET; err = -EAGAIN; goto out_err; } wnode->treelock = TREELOCK_WRITE; } } node = get_node(nodeid); if(node == NULL) { err = -ESTALE; goto out_unlock; } for(; node->nodeid != FUSE_ROOT_ID; node = node->parent) { err = -ESTALE; if(node->name == NULL || node->parent == NULL) goto out_unlock; path->prepend(node->name); if(need_lock) { err = -EAGAIN; if(node->treelock < 0) goto out_unlock; node->treelock++; } } // Move data from the prefix area to the rel anchor and strip the // leading '/' produced by prepend(). After this the path is in // mergerfs's canonical REL form (no leading slash, anchored at // REL_OFFSET). path->consolidate_to_rel(/*strip_leading_slash=*/true); if(wnodep) *wnodep = wnode; return 0; out_unlock: if(need_lock) unlock_path(nodeid,wnode,node); // Reset the path to empty on every error exit. This is load-bearing // for callers that mask -ESTALE to 0 (statx_path, see fuse_lib_ // statx_path): a nameless node yields a partial prepend chain that // must not be observed as a valid path. After reset, the // dispatcher's "is the path empty?" test cleanly distinguishes // (nodeid == FUSE_ROOT_ID) "root walk produced empty" from // (nodeid != FUSE_ROOT_ID) "nameless / ESTALE-masked" without // looking at `err`. path->clear(); out_err: return err; } static int try_get_path2(uint64_t nodeid1, const char *name1, uint64_t nodeid2, const char *name2, fs::relpath *path1, fs::relpath *path2, node_t **wnode1, node_t **wnode2) { int err; err = try_get_path(nodeid1,name1,path1,wnode1,true); if(!err) { err = try_get_path(nodeid2,name2,path2,wnode2,true); if(err) { node_t *wn1 = wnode1 ? *wnode1 : NULL; unlock_path(nodeid1,wn1,NULL); // path1 storage is stack-owned by the caller; no free. } } return err; } static void queue_element_wakeup(struct lock_queue_element *qe) { int err; if(!qe->path1) { /* Just waiting for it to be unlocked */ node_t *qnode = get_node(qe->nodeid1); if(qnode && (qnode->treelock == 0)) pthread_cond_signal(&qe->cond); return; } if(qe->done) return; if(!qe->path2) { err = try_get_path(qe->nodeid1, qe->name1, qe->path1, qe->wnode1, true); } else { err = try_get_path2(qe->nodeid1, qe->name1, qe->nodeid2, qe->name2, qe->path1, qe->path2, qe->wnode1, qe->wnode2); } if(err == -EAGAIN) return; qe->err = err; qe->done = true; pthread_cond_signal(&qe->cond); } static void wake_up_queued() { struct lock_queue_element *qe; for(qe = f.lockq; qe != NULL; qe = qe->next) queue_element_wakeup(qe); } static void queue_path(struct lock_queue_element *qe) { struct lock_queue_element **qp; qe->done = false; pthread_cond_init(&qe->cond,NULL); qe->next = NULL; for(qp = &f.lockq; *qp != NULL; qp = &(*qp)->next); *qp = qe; } static void dequeue_path(struct lock_queue_element *qe) { struct lock_queue_element **qp; pthread_cond_destroy(&qe->cond); for(qp = &f.lockq; *qp != qe; qp = &(*qp)->next); *qp = qe->next; } static int wait_path(struct lock_queue_element *qe) { queue_path(qe); do { pthread_cond_wait(&qe->cond,&f.lock); } while(!qe->done); dequeue_path(qe); return qe->err; } static int get_path_common(uint64_t nodeid, const char *name, fs::relpath *path, node_t **wnode) { int err; mutex_lock(f.lock); err = try_get_path(nodeid,name,path,wnode,true); if(err == -EAGAIN) { struct lock_queue_element qe = {}; qe.nodeid1 = nodeid; qe.name1 = name; qe.path1 = path; qe.wnode1 = wnode; err = wait_path(&qe); } mutex_unlock(f.lock); return err; } static int get_path(uint64_t nodeid, fs::relpath *path) { return get_path_common(nodeid,NULL,path,NULL); } static int get_path_name(uint64_t nodeid, const char *name, fs::relpath *path) { return get_path_common(nodeid,name,path,NULL); } static int get_path_wrlock(uint64_t nodeid, const char *name, fs::relpath *path, node_t **wnode) { return get_path_common(nodeid,name,path,wnode); } static int get_path2(uint64_t nodeid1, const char *name1, uint64_t nodeid2, const char *name2, fs::relpath *path1, fs::relpath *path2, node_t **wnode1, node_t **wnode2) { int err; mutex_lock(f.lock); err = try_get_path2(nodeid1,name1,nodeid2,name2, path1,path2,wnode1,wnode2); if(err == -EAGAIN) { struct lock_queue_element qe = {}; qe.nodeid1 = nodeid1; qe.name1 = name1; qe.path1 = path1; qe.wnode1 = wnode1; qe.nodeid2 = nodeid2; qe.name2 = name2; qe.path2 = path2; qe.wnode2 = wnode2; err = wait_path(&qe); } mutex_unlock(f.lock); return err; } static void free_path_wrlock(uint64_t nodeid, node_t *wnode) { mutex_lock(f.lock); unlock_path(nodeid,wnode,NULL); if(f.lockq) wake_up_queued(); mutex_unlock(f.lock); } static void update_stat(node_t *node_, const struct stat *stnew_) { uint32_t crc32b; // This function can only invalidate an existing fingerprint; it // never arms a new one. Arming (writing a non-zero CRC into // state_crc32b) happens exclusively in open_auto_cache(), which is // the single point that decides a file's page cache is worth // retaining across opens. If stat_crc32b is 0 here the cache is // already considered invalid and we have nothing to compare // against. if(node_->stat_crc32b == 0) return; crc32b = stat_crc32b(stnew_); if(crc32b != node_->stat_crc32b) node_->stat_crc32b = 0; } static void free_path(uint64_t nodeid, const fs::relpath *path) { // The caller's stack-owned fs::relpath is empty when no get_path*() // succeeded for this op (or when an intermediate "free + reuse" // step like free_path_and_inc_open already released the read locks // and clear()d the path). In either case there is nothing to do. if(path->empty()) return; free_path_wrlock(nodeid,NULL); } static node_t* free_path_and_inc_open(u64 free_nodeid_, u64 inc_open_ino_, bool remember_too_) { node_t *node; mutex_lock(f.lock); unlock_path(free_nodeid_,NULL,NULL); node = get_node(inc_open_ino_); if(node) { if(remember_too_ && remember_nodes()) remember_node(node); node->open_count++; } if(f.lockq) wake_up_queued(); mutex_unlock(f.lock); return node; } static bool free_path_and_update_stat(u64 nodeid_, const struct stat *stnew_) { bool ok; node_t *node; mutex_lock(f.lock); unlock_path(nodeid_,NULL,NULL); node = get_node(nodeid_); ok = (node != NULL); if(ok) update_stat(node,stnew_); if(f.lockq) wake_up_queued(); mutex_unlock(f.lock); return ok; } static void free_path2(uint64_t nodeid1, uint64_t nodeid2, node_t *wnode1, node_t *wnode2) { mutex_lock(f.lock); unlock_path(nodeid1,wnode1,NULL); unlock_path(nodeid2,wnode2,NULL); wake_up_queued(); mutex_unlock(f.lock); } static void forget_node(const uint64_t nodeid, const uint64_t nlookup) { node_t *node; if(nodeid == FUSE_ROOT_ID) return; mutex_lockguard(f.lock); node = get_node(nodeid); if(node == NULL) return; /* * Node may still be locked due to interrupt idiocy in open, * create and opendir */ while(node->nlookup == nlookup && node->treelock) { struct lock_queue_element qe = {}; qe.nodeid1 = nodeid; queue_path(&qe); do { pthread_cond_wait(&qe.cond,&f.lock); } while((node->nlookup == nlookup) && node->treelock); dequeue_path(&qe); } assert(node->nlookup >= nlookup); node->nlookup -= nlookup; unref_node_if_unused(node); } static void unlink_node(node_t *node) { unhash_name(node); unremember_node(node); } static void remove_node(uint64_t dir, const char *name) { node_t *node; mutex_lock(f.lock); node = lookup_node(dir,name); if(node != NULL) unlink_node(node); mutex_unlock(f.lock); } static int rename_node(uint64_t olddir, const char *oldname, uint64_t newdir, const char *newname) { int err = 0; node_t *node; node_t *newnode; mutex_lock(f.lock); node = lookup_node(olddir,oldname); newnode = lookup_node(newdir,newname); if(node == NULL) goto out; if((newnode != NULL) && (newnode != node)) unlink_node(newnode); unhash_name(node); if(hash_name(node,newdir,newname) == -1) { err = -ENOMEM; goto out; } out: mutex_unlock(f.lock); return err; } static void set_stat(uint64_t nodeid, struct stat *stbuf) { if(fuse_cfg.valid_uid()) stbuf->st_uid = fuse_cfg.uid; if(fuse_cfg.valid_gid()) stbuf->st_gid = fuse_cfg.gid; if(fuse_cfg.valid_umask()) stbuf->st_mode = (stbuf->st_mode & S_IFMT) | (0777 & ~fuse_cfg.umask); } static int node_open(const node_t *node_) { return ((node_ != NULL) && (node_->open_count > 0)); } static int set_path_info(uint64_t nodeid_, const char *name_, struct fuse_entry_param *e_, bool remember_ = true) { node_t *node; mutex_lock(f.lock); if(!name_) { node = get_node(nodeid_); if(node == NULL) goto out_unlock; } else { node = lookup_node(nodeid_,name_); } if(node == NULL) { node = node_alloc(); if(node == NULL) goto out_unlock; node->nodeid = generate_nodeid(&f.nodeid_gen); if(remember_ && remember_nodes()) remember_node(node); if(hash_name(node,nodeid_,name_) == -1) { free_node(node); node = NULL; goto out_unlock; } hash_id(node); } inc_nlookup(node); e_->ino = node->nodeid; e_->generation = ((e_->ino == FUSE_ROOT_ID) ? 0 : f.nodeid_gen.generation); update_stat(node,&e_->attr); mutex_unlock(f.lock); set_stat(e_->ino,&e_->attr); return 0; out_unlock: mutex_unlock(f.lock); return -ENOMEM; } /* lookup requests only come in for FUSE_ROOT_ID when a "parent of child of root node" request is made. This can happen when using EXPORT_SUPPORT=true and a file handle is used to keep a reference to a node which has been forgotten. Mostly a NFS concern but not excluslively. Root node always has a nodeid of 1 and generation of 0. To ensure this set_path_info() explicitly ensures the root id has a generation of 0. */ static int lookup_path(fuse_req_t *req_, uint64_t nodeid, const char *name, const fs::relpath &fusepath, struct fuse_entry_param *e, fuse_file_info_t *fi, bool remember = true) { int rv; assert(req_ != NULL); memset(e,0,sizeof(struct fuse_entry_param)); rv = ((fi == NULL) ? f.ops.getattr(&req_->ctx,fusepath,&e->attr,&e->timeout) : f.ops.fgetattr(&req_->ctx,fi->fh,&e->attr,&e->timeout)); if(rv) return rv; return set_path_info(nodeid,name,e,remember); } static void reply_entry(fuse_req_t *req_, const struct fuse_entry_param *e, int err) { if(!err) { if(fuse_reply_entry(req_,e) == -ENOENT) { /* Skip forget for negative result */ if(e->ino != 0) forget_node(e->ino,1); } } else { fuse_reply_err(req_,err); } } static void fuse_lib_init(fuse_conn_info_t *conn) { f.ops.init(conn); } static void fuse_lib_destroy() { f.ops.destroy(); } static void fuse_lib_lookup(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err; uint64_t nodeid; fs::relpath fusepath; const char *name; node_t *dot = NULL; struct fuse_entry_param e = {}; name = (const char*)fuse_hdr_arg(hdr_); nodeid = hdr_->nodeid; if(name[0] == '.') { if(name[1] == '\0') { name = NULL; mutex_lockguard(f.lock); dot = get_node(nodeid); if(dot == NULL) { reply_entry(req_,&e,-ESTALE); return; } dot->refctr++; } else if((name[1] == '.') && (name[2] == '\0')) { if(nodeid == FUSE_ROOT_ID) { reply_entry(req_,&e,-ENOENT); return; } name = NULL; mutex_lockguard(f.lock); { node_t *pnode = get_node(nodeid); if((pnode == NULL) || (pnode->parent == NULL)) { reply_entry(req_,&e,-ESTALE); return; } nodeid = pnode->parent->nodeid; } } } err = get_path_name(nodeid,name,&fusepath); if(!err) { err = lookup_path(req_,nodeid,name,fusepath,&e,NULL); if(err == -ENOENT) { e.ino = 0; err = 0; } free_path(nodeid,&fusepath); } if(dot) { mutex_lock(f.lock); unref_node(dot); mutex_unlock(f.lock); } reply_entry(req_,&e,err); } static void fuse_lib_lseek(fuse_req_t *req_, struct fuse_in_header *hdr_) { fuse_reply_err(req_,ENOSYS); } static void fuse_lib_forget(fuse_req_t *req_, struct fuse_in_header *hdr_) { struct fuse_forget_in *arg; arg = (fuse_forget_in*)fuse_hdr_arg(hdr_); forget_node(hdr_->nodeid,arg->nlookup); fuse_reply_none(req_); } static void fuse_lib_forget_multi(fuse_req_t *req_, struct fuse_in_header *hdr_) { struct fuse_batch_forget_in *arg; struct fuse_forget_one *entry; arg = (fuse_batch_forget_in*)fuse_hdr_arg(hdr_); entry = (fuse_forget_one*)PARAM(arg); for(uint32_t i = 0; i < arg->count; i++) { forget_node(entry[i].nodeid, entry[i].nlookup); } fuse_reply_none(req_); } static void fuse_lib_getattr(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err; fs::relpath fusepath; uint64_t fh; struct stat buf; node_t *node; fuse_timeouts_t timeouts{}; const struct fuse_getattr_in *arg; arg = (fuse_getattr_in*)fuse_hdr_arg(hdr_); fh = 0; if(arg->getattr_flags & FUSE_GETATTR_FH) fh = arg->fh; memset(&buf,0,sizeof(buf)); err = 0; fusepath.clear(); if(fh == 0) { err = get_path(hdr_->nodeid,&fusepath); if(err == -ESTALE) // unlinked but open err = 0; } if(!err) { bool node_ok = true; // Three-way dispatch: // - explicit fh (fh != 0) -> fgetattr(fh) // - empty fusepath, not FUSE_ROOT_ID -> nameless node // (ESTALE was masked // above): fall back // to fgetattr(0) so // mergerfs can find // the file via // OpenFiles. // - otherwise -> path-based getattr, // including // FUSE_ROOT_ID where // the rel is empty. const bool path_based = (fh == 0) && (!fusepath.empty() || hdr_->nodeid == FUSE_ROOT_ID); if(path_based) { err = f.ops.getattr(&req_->ctx,fusepath,&buf,&timeouts); if(!err) node_ok = free_path_and_update_stat(hdr_->nodeid,&buf); else free_path(hdr_->nodeid,&fusepath); } else { err = f.ops.fgetattr(&req_->ctx,fh,&buf,&timeouts); if(!err) { mutex_lockguard(f.lock); node = get_node(hdr_->nodeid); node_ok = (node != NULL); if(node_ok) update_stat(node,&buf); } // get_path may have run and produced an empty path on the // ESTALE-masked nameless branch; release any state it left. if(fh == 0) free_path(hdr_->nodeid,&fusepath); } if(!err && !node_ok) { fuse_reply_err(req_,ESTALE); return; } } if(!err) { set_stat(hdr_->nodeid,&buf); fuse_reply_attr(req_,&buf,timeouts.attr); } else { fuse_reply_err(req_,err); } } static void fuse_lib_statx_path(fuse_req_t *req_, struct fuse_in_header *hdr_, fuse_statx_in *inarg_) { int err; fs::relpath fusepath; struct fuse_statx st{}; fuse_timeouts_t timeouts{}; err = get_path(hdr_->nodeid,&fusepath); if(err == -ESTALE) err = 0; if(err) { fuse_reply_err(req_,err); return; } // Three cases for an empty fusepath after get_path: // 1. nodeid == FUSE_ROOT_ID, walk produced 0 components -- a real // path query for the mountpoint root. // 2. nodeid != FUSE_ROOT_ID, ESTALE was masked above -- a nameless // node (file held open after unlink). The named-path lookup // cannot resolve it; fall back to fh-based statx, which lets // mergerfs find the file via OpenFiles by its FUSE handle. // 3. nodeid != FUSE_ROOT_ID, success -- the path is actually // empty only if the call returned 0 with empty(); the only // such path is FUSE_ROOT_ID, so this case does not arise. // try_get_path()'s error paths now reset the path to empty, so // case 2 always observably produces an empty fusepath -- the // (nodeid, empty) pair uniquely identifies the nameless case. if(!fusepath.empty() || hdr_->nodeid == FUSE_ROOT_ID) err = f.ops.statx(&req_->ctx, fusepath, inarg_->sx_flags, inarg_->sx_mask, &st, &timeouts); else err = f.ops.statx_fh(&req_->ctx, 0, inarg_->sx_flags, inarg_->sx_mask, &st, &timeouts); free_path(hdr_->nodeid,&fusepath); if(err) fuse_reply_err(req_,err); else fuse_reply_statx(req_,0,&st,timeouts.attr); } static void fuse_lib_statx_fh(fuse_req_t *req_, struct fuse_in_header *hdr_, fuse_statx_in *inarg_) { int err; struct fuse_statx st{}; fuse_timeouts_t timeouts{}; err = f.ops.statx_fh(&req_->ctx, inarg_->fh, inarg_->sx_flags, inarg_->sx_mask, &st, &timeouts); if(err) fuse_reply_err(req_,err); else fuse_reply_statx(req_,0,&st,timeouts.attr); } static void fuse_lib_statx(fuse_req_t *req_, struct fuse_in_header *hdr_) { fuse_statx_in *inarg; inarg = (fuse_statx_in*)fuse_hdr_arg(hdr_); if(inarg->getattr_flags & FUSE_GETATTR_FH) fuse_lib_statx_fh(req_,hdr_,inarg); else fuse_lib_statx_path(req_,hdr_,inarg); } static void fuse_lib_setattr(fuse_req_t *req_, struct fuse_in_header *hdr_) { uint64_t fh; struct stat stbuf = {}; fs::relpath fusepath; int err; fuse_timeouts_t timeouts{}; struct fuse_setattr_in *arg; arg = (fuse_setattr_in*)fuse_hdr_arg(hdr_); fh = 0; if(arg->valid & FATTR_FH) fh = arg->fh; err = 0; fusepath.clear(); if(fh == 0) { err = get_path(hdr_->nodeid,&fusepath); if(err == -ESTALE) err = 0; } if(!err) { // Three-way dispatch (see fuse_lib_getattr for full notes): // - explicit fh -> fXXX(fh) // - empty fusepath, not FUSE_ROOT_ID -> nameless node // (ESTALE was masked): // fall back to fXXX(0) // - otherwise -> path-based op const bool path_based = (fh == 0) && (!fusepath.empty() || hdr_->nodeid == FUSE_ROOT_ID); err = 0; if(!err && (arg->valid & FATTR_MODE)) err = (path_based ? f.ops.chmod(&req_->ctx,fusepath,arg->mode) : f.ops.fchmod(&req_->ctx,fh,arg->mode)); if(!err && (arg->valid & (FATTR_UID | FATTR_GID))) { uid_t uid = ((arg->valid & FATTR_UID) ? arg->uid : (uid_t)-1); gid_t gid = ((arg->valid & FATTR_GID) ? arg->gid : (gid_t)-1); err = (path_based ? f.ops.chown(&req_->ctx,fusepath,uid,gid) : f.ops.fchown(&req_->ctx,fh,uid,gid)); } if(!err && (arg->valid & FATTR_SIZE)) err = (path_based ? f.ops.truncate(&req_->ctx,fusepath,arg->size) : f.ops.ftruncate(&req_->ctx,fh,arg->size)); if(!err && (arg->valid & (FATTR_ATIME | FATTR_MTIME))) { struct timespec tv[2]; tv[0].tv_sec = 0; tv[1].tv_sec = 0; tv[0].tv_nsec = UTIME_OMIT; tv[1].tv_nsec = UTIME_OMIT; if(arg->valid & FATTR_ATIME_NOW) tv[0].tv_nsec = UTIME_NOW; else if(arg->valid & FATTR_ATIME) tv[0] = (struct timespec){ static_cast(arg->atime), static_cast(arg->atimensec) }; if(arg->valid & FATTR_MTIME_NOW) tv[1].tv_nsec = UTIME_NOW; else if(arg->valid & FATTR_MTIME) tv[1] = (struct timespec){ static_cast(arg->mtime), static_cast(arg->mtimensec) }; err = (path_based ? f.ops.utimens(&req_->ctx,fusepath,tv) : f.ops.futimens(&req_->ctx,fh,tv)); } if(!err) err = (path_based ? f.ops.getattr(&req_->ctx,fusepath,&stbuf,&timeouts) : f.ops.fgetattr(&req_->ctx,fh,&stbuf,&timeouts)); bool stat_updated = false; // Only the path-based branch held a path lock that needs // unlock_path; the fh-based branch (incl. nameless fallback) // never acquired one. free_path() handles both cases on the // shared cleanup edge below. if(!err && path_based) { node_t *node; mutex_lock(f.lock); unlock_path(hdr_->nodeid,NULL,NULL); node = get_node(hdr_->nodeid); if(node) update_stat(node,&stbuf); if(f.lockq) wake_up_queued(); mutex_unlock(f.lock); stat_updated = true; } else { free_path(hdr_->nodeid,&fusepath); } if(!err) { if(!stat_updated) { node_t *node; mutex_lockguard(f.lock); node = get_node(hdr_->nodeid); if(node) update_stat(node,&stbuf); } set_stat(hdr_->nodeid,&stbuf); fuse_reply_attr(req_,&stbuf,timeouts.attr); } else { fuse_reply_err(req_,err); } return; } fuse_reply_err(req_,err); } static void fuse_lib_access(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err; fs::relpath fusepath; struct fuse_access_in *arg; arg = (fuse_access_in*)fuse_hdr_arg(hdr_); err = get_path(hdr_->nodeid,&fusepath); if(!err) { err = f.ops.access(&req_->ctx, fusepath, arg->mask); free_path(hdr_->nodeid,&fusepath); } fuse_reply_err(req_,err); } static void fuse_lib_readlink(fuse_req_t *req_, struct fuse_in_header *hdr_) { ssize_t rv; fs::relpath fusepath; char linkname[PATH_MAX + 1]; rv = get_path(hdr_->nodeid,&fusepath); if(!rv) { rv = f.ops.readlink(&req_->ctx, fusepath, linkname, sizeof(linkname)); free_path(hdr_->nodeid,&fusepath); } if(rv >= 0) fuse_reply_readlink(req_,linkname,rv); else fuse_reply_err(req_,rv); } static void fuse_lib_mknod(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err; fs::relpath fusepath; const char* name; struct fuse_entry_param e; struct fuse_mknod_in *arg; arg = (fuse_mknod_in*)fuse_hdr_arg(hdr_); name = (const char*)PARAM(arg); if(req_->conn.proto_minor >= 12) req_->ctx.umask = arg->umask; else name = (char*)arg + FUSE_COMPAT_MKNOD_IN_SIZE; err = get_path_name(hdr_->nodeid,name,&fusepath); if(!err) { err = -ENOSYS; if(S_ISREG(arg->mode)) { fuse_file_info_t ffi = {}; ffi.flags = O_CREAT | O_EXCL | O_WRONLY; err = f.ops.create(&req_->ctx, fusepath, arg->mode, &ffi); if(!err) { err = lookup_path(req_,hdr_->nodeid,name,fusepath,&e,&ffi); f.ops.release(&req_->ctx, &ffi); } } if(err == -ENOSYS) { err = f.ops.mknod(&req_->ctx, fusepath, arg->mode, arg->rdev); if(!err) err = lookup_path(req_,hdr_->nodeid,name,fusepath,&e,NULL); } free_path(hdr_->nodeid,&fusepath); } reply_entry(req_,&e,err); } static void fuse_lib_mkdir(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err; fs::relpath fusepath; const char *name; struct fuse_entry_param e; struct fuse_mkdir_in *arg; arg = (fuse_mkdir_in*)fuse_hdr_arg(hdr_); name = (const char*)PARAM(arg); if(req_->conn.proto_minor >= 12) req_->ctx.umask = arg->umask; err = get_path_name(hdr_->nodeid,name,&fusepath); if(!err) { err = f.ops.mkdir(&req_->ctx, fusepath, arg->mode); if(!err) err = lookup_path(req_,hdr_->nodeid,name,fusepath,&e,NULL); free_path(hdr_->nodeid,&fusepath); } reply_entry(req_,&e,err); } static void fuse_lib_unlink(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err; fs::relpath fusepath; const char *name; node_t *wnode; name = (const char*)PARAM(hdr_); err = get_path_wrlock(hdr_->nodeid,name,&fusepath,&wnode); if(!err) { if(node_open(wnode)) req_->ctx.nodeid = wnode->nodeid; err = f.ops.unlink(&req_->ctx, fusepath); if(!err) remove_node(hdr_->nodeid,name); free_path_wrlock(hdr_->nodeid,wnode); } fuse_reply_err(req_,err); } static void fuse_lib_rmdir(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err; fs::relpath fusepath; const char *name; node_t *wnode; name = (const char*)PARAM(hdr_); err = get_path_wrlock(hdr_->nodeid,name,&fusepath,&wnode); if(!err) { err = f.ops.rmdir(&req_->ctx, fusepath); if(!err) remove_node(hdr_->nodeid,name); free_path_wrlock(hdr_->nodeid,wnode); } fuse_reply_err(req_,err); } static void fuse_lib_symlink(fuse_req_t *req_, struct fuse_in_header *hdr_) { int rv; fs::relpath fusepath; const char *name; const char *linkname; struct fuse_entry_param e = {}; name = (const char*)fuse_hdr_arg(hdr_); linkname = (name + strlen(name) + 1); rv = get_path_name(hdr_->nodeid,name,&fusepath); if(rv == 0) { rv = f.ops.symlink(&req_->ctx, linkname, fusepath, &e.attr, &e.timeout); if(rv == 0) rv = set_path_info(hdr_->nodeid,name,&e); free_path(hdr_->nodeid,&fusepath); } reply_entry(req_,&e,rv); } static void fuse_lib_rename(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err; fs::relpath oldpath; fs::relpath newpath; const char *oldname; const char *newname; node_t *wnode1; node_t *wnode2; struct fuse_rename_in *arg; arg = (fuse_rename_in*)fuse_hdr_arg(hdr_); oldname = (const char*)PARAM(arg); newname = (oldname + strlen(oldname) + 1); err = get_path2(hdr_->nodeid, oldname, arg->newdir, newname, &oldpath, &newpath, &wnode1, &wnode2); if(!err) { err = f.ops.rename(&req_->ctx, oldpath, newpath); if(!err) err = rename_node(hdr_->nodeid,oldname,arg->newdir,newname); free_path2(hdr_->nodeid,arg->newdir,wnode1,wnode2); } fuse_reply_err(req_,err); } static void fuse_lib_rename2(fuse_req_t *req_, struct fuse_in_header *hdr_) { fuse_reply_err(req_,ENOSYS); } static void fuse_lib_retrieve_reply(fuse_req_t *req_, void *cookie_, uint64_t ino_, off_t offset_) { } static void fuse_lib_link(fuse_req_t *req_, struct fuse_in_header *hdr_) { int rv; fs::relpath oldpath; fs::relpath newpath; const char *newname; struct fuse_link_in *arg; struct fuse_entry_param e = {}; arg = (fuse_link_in*)fuse_hdr_arg(hdr_); newname = (const char*)PARAM(arg); rv = get_path2(arg->oldnodeid, NULL, hdr_->nodeid, newname, &oldpath, &newpath, NULL, NULL); if(!rv) { rv = f.ops.link(&req_->ctx, oldpath, newpath, &e.attr, &e.timeout); if(rv == 0) rv = set_path_info(hdr_->nodeid,newname,&e); free_path2(arg->oldnodeid,hdr_->nodeid,NULL,NULL); } reply_entry(req_,&e,rv); } static void fuse_do_release(fuse_req_ctx_t *req_ctx_, uint64_t ino_, fuse_file_info_t *ffi_) { f.ops.release(req_ctx_, ffi_); mutex_lockguard(f.lock); { node_t *node; node = get_node(ino_); if(node) { assert(node->open_count > 0); node->open_count--; unref_node_if_unused(node); } } } static void fuse_lib_create(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err; fs::relpath fusepath; const char *name; uint64_t new_nodeid; fuse_file_info_t ffi = {}; struct fuse_entry_param e; struct fuse_create_in *arg; arg = (fuse_create_in*)fuse_hdr_arg(hdr_); name = (const char*)PARAM(arg); ffi.flags = arg->flags; if(req_->conn.proto_minor >= 12) req_->ctx.umask = arg->umask; else name = ((char*)arg + sizeof(struct fuse_open_in)); // Opportunistically allocate a provisional node so passthrough create has // a nodeid. It is remembered only after create fully succeeds. { node_t *node; node = find_node(hdr_->nodeid,name,false); if(node == NULL) { fuse_reply_err(req_,ENOMEM); return; } new_nodeid = node->nodeid; req_->ctx.nodeid = new_nodeid; } err = get_path_name(hdr_->nodeid,name,&fusepath); if(!err) { err = f.ops.create(&req_->ctx, fusepath, arg->mode, &ffi); if(!err) { err = lookup_path(req_,hdr_->nodeid,name,fusepath,&e,&ffi,false); if(err) { f.ops.release(&req_->ctx,&ffi); } else if(!S_ISREG(e.attr.st_mode)) { err = -EIO; f.ops.release(&req_->ctx,&ffi); forget_node(e.ino,1); } } } if(!err) { free_path_and_inc_open(hdr_->nodeid,e.ino,true); fusepath.clear(); if(fuse_reply_create(req_,&e,&ffi) == -ENOENT) { /* The open syscall was interrupted,so it must be cancelled */ fuse_do_release(&req_->ctx,e.ino,&ffi); forget_node(e.ino,1); } } else { forget_node(new_nodeid,1); fuse_reply_err(req_,err); } free_path(hdr_->nodeid,&fusepath); } static void open_auto_cache(fuse_req_ctx_t *req_ctx_, uint64_t ino, const char *path, fuse_file_info_t *fi) { node_t *node; fuse_timeouts_t timeouts{}; mutex_lock(f.lock); node = get_node(ino); if(not node) { mutex_unlock(f.lock); return; } { int err; struct stat stbuf; mutex_unlock(f.lock); err = f.ops.fgetattr(req_ctx_, fi->fh, &stbuf, &timeouts); mutex_lock(f.lock); if(!err) { uint32_t old_crc32b; uint32_t new_crc32b; old_crc32b = node->stat_crc32b; new_crc32b = stat_crc32b(&stbuf); if((old_crc32b != 0) && (old_crc32b == new_crc32b)) fi->keep_cache = 1; node->stat_crc32b = new_crc32b; } else { node->stat_crc32b = 0; } } mutex_unlock(f.lock); } static void fuse_lib_open(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err; fs::relpath fusepath; fuse_file_info_t ffi = {}; struct fuse_open_in *arg; arg = (fuse_open_in*)fuse_hdr_arg(hdr_); ffi.flags = arg->flags; err = get_path(hdr_->nodeid,&fusepath); if(!err) { err = f.ops.open(&req_->ctx, fusepath, &ffi); if(!err) { if(ffi.auto_cache && (ffi.passthrough == false)) open_auto_cache(&req_->ctx,hdr_->nodeid,fusepath,&ffi); } } if(!err) { free_path_and_inc_open(hdr_->nodeid,hdr_->nodeid,false); fusepath.clear(); /* The open syscall was interrupted,so it must be cancelled */ if(fuse_reply_open(req_,&ffi) == -ENOENT) fuse_do_release(&req_->ctx,hdr_->nodeid,&ffi); } else { fuse_reply_err(req_,err); } free_path(hdr_->nodeid,&fusepath); } static void fuse_lib_read(fuse_req_t *req_, struct fuse_in_header *hdr_) { int res; fuse_file_info_t ffi = {}; struct fuse_read_in *arg; fuse_msgbuf_t *msgbuf; arg = (fuse_read_in*)fuse_hdr_arg(hdr_); ffi.fh = arg->fh; if(req_->conn.proto_minor >= 9) { ffi.flags = arg->flags; ffi.lock_owner = arg->lock_owner; } msgbuf = msgbuf_alloc_page_aligned(); if(msgbuf == NULL) { fuse_reply_err(req_,ENOMEM); return; } res = f.ops.read(&req_->ctx, &ffi, msgbuf->mem, arg->size, arg->offset); if(res >= 0) fuse_reply_data(req_,msgbuf->mem,res); else fuse_reply_err(req_,res); msgbuf_free(msgbuf); } static void fuse_lib_write(fuse_req_t *req_, struct fuse_in_header *hdr_) { int res; char *data; fuse_file_info_t ffi = {}; struct fuse_write_in *arg; arg = (fuse_write_in*)fuse_hdr_arg(hdr_); ffi.fh = arg->fh; ffi.writepage = !!(arg->write_flags & 1); if(req_->conn.proto_minor < 9) { data = ((char*)arg) + FUSE_COMPAT_WRITE_IN_SIZE; } else { ffi.flags = arg->flags; ffi.lock_owner = arg->lock_owner; data = (char*)PARAM(arg); } res = f.ops.write(&req_->ctx, &ffi, data, arg->size, arg->offset); if(res >= 0) fuse_reply_write(req_,res); else fuse_reply_err(req_,res); } static void fuse_lib_fsync(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err; int is_datasync; struct fuse_fsync_in *arg; arg = (fuse_fsync_in*)fuse_hdr_arg(hdr_); is_datasync = !!(arg->fsync_flags & FUSE_FSYNC_FDATASYNC); err = f.ops.fsync(&req_->ctx, arg->fh, is_datasync); fuse_reply_err(req_,err); } static struct fuse_dh* get_dirhandle(const fuse_file_info_t *llfi, fuse_file_info_t *ffi) { struct fuse_dh *dh = (struct fuse_dh *)(uintptr_t)llfi->fh; if(dh == NULL) return NULL; *ffi = {}; ffi->fh = dh->fh; return dh; } static void fuse_lib_opendir(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err; fs::relpath fusepath; struct fuse_dh *dh; fuse_file_info_t llffi = {}; fuse_file_info_t ffi = {}; struct fuse_open_in *arg; arg = (fuse_open_in*)fuse_hdr_arg(hdr_); llffi.flags = arg->flags; dh = (fuse_dh*)calloc(1,sizeof(fuse_dh)); if(dh == NULL) { fuse_reply_err(req_,ENOMEM); return; } err = fuse_dirents_init(&dh->d); if(err) { free(dh); fuse_reply_err(req_,ENOMEM); return; } mutex_init(dh->lock); llffi.fh = (uintptr_t)dh; ffi.flags = llffi.flags; err = get_path(hdr_->nodeid,&fusepath); if(!err) { err = f.ops.opendir(&req_->ctx, fusepath, &ffi); dh->fh = ffi.fh; llffi.keep_cache = ffi.keep_cache; llffi.cache_readdir = ffi.cache_readdir; } if(!err) { if(fuse_reply_open(req_,&llffi) == -ENOENT) { /* The opendir syscall was interrupted,so it must be cancelled */ f.ops.releasedir(&req_->ctx,&ffi); fuse_dirents_free(&dh->d); mutex_destroy(dh->lock); free(dh); } } else { fuse_reply_err(req_,err); fuse_dirents_free(&dh->d); mutex_destroy(dh->lock); free(dh); } free_path(hdr_->nodeid,&fusepath); } static size_t readdir_buf_size(fuse_dirents_t *d_, size_t size_, off_t off_) { if((size_t)off_ >= kv_size(d_->offs)) return 0; if((kv_A(d_->offs,off_) + size_) > kv_size(d_->data)) return (kv_size(d_->data) - kv_A(d_->offs,off_)); return size_; } static char* readdir_buf(fuse_dirents_t *d_, off_t off_) { size_t i; i = kv_A(d_->offs,off_); return &kv_A(d_->data,i); } static void fuse_lib_readdir(fuse_req_t *req_, struct fuse_in_header *hdr_) { int rv; size_t size; fuse_dirents_t *d; struct fuse_dh *dh; fuse_file_info_t ffi = {}; fuse_file_info_t llffi = {}; struct fuse_read_in *arg; arg = (fuse_read_in*)fuse_hdr_arg(hdr_); size = arg->size; llffi.fh = arg->fh; dh = get_dirhandle(&llffi,&ffi); if(not dh) { fuse_reply_err(req_,EBADF); return; } d = &dh->d; mutex_lockguard(dh->lock); rv = 0; if((arg->offset == 0) || (kv_size(d->data) == 0)) rv = f.ops.readdir(&req_->ctx, &ffi, d); if(rv) { fuse_reply_err(req_,rv); return; } size = readdir_buf_size(d,size,arg->offset); if(size == 0) fuse_reply_buf(req_,NULL,0); else fuse_reply_buf(req_, readdir_buf(d,arg->offset), size); } static void fuse_lib_readdir_plus(fuse_req_t *req_, struct fuse_in_header *hdr_) { int rv; size_t size; fuse_dirents_t *d; struct fuse_dh *dh; fuse_file_info_t ffi = {}; fuse_file_info_t llffi = {}; struct fuse_read_in *arg; arg = (fuse_read_in*)fuse_hdr_arg(hdr_); size = arg->size; llffi.fh = arg->fh; dh = get_dirhandle(&llffi,&ffi); if(not dh) { fuse_reply_err(req_,EBADF); return; } d = &dh->d; mutex_lockguard(dh->lock); rv = 0; if((arg->offset == 0) || (kv_size(d->data) == 0)) rv = f.ops.readdir_plus(&req_->ctx, &ffi, d); if(rv) { fuse_reply_err(req_,rv); return; } size = readdir_buf_size(d,size,arg->offset); if(size == 0) fuse_reply_buf(req_,NULL,0); else fuse_reply_buf(req_, readdir_buf(d,arg->offset), size); } static void fuse_lib_releasedir(fuse_req_t *req_, struct fuse_in_header *hdr_) { struct fuse_dh *dh; fuse_file_info_t ffi = {}; fuse_file_info_t llffi = {}; struct fuse_release_in *arg; arg = (fuse_release_in*)fuse_hdr_arg(hdr_); llffi.fh = arg->fh; llffi.flags = arg->flags; dh = get_dirhandle(&llffi,&ffi); if(not dh) { fuse_reply_err(req_,EBADF); return; } f.ops.releasedir(&req_->ctx, &ffi); // Acquire/release barrier: not a real critical section. A // concurrent readdir may still hold dh->lock at this point. This // lock+unlock pair blocks until the last readdir drops the lock // guaranteeing that the subsequent destroy/free sees no in-flight // readdir accessing dh->d. Do NOT remove. mutex_lock(dh->lock); mutex_unlock(dh->lock); mutex_destroy(dh->lock); fuse_dirents_free(&dh->d); free(dh); fuse_reply_err(req_,0); } static void fuse_lib_fsyncdir(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err; int is_datasync; fuse_dh *dh; fuse_file_info_t ffi = {}; fuse_file_info_t llffi = {}; struct fuse_fsync_in *arg; arg = (fuse_fsync_in*)fuse_hdr_arg(hdr_); is_datasync = !!(arg->fsync_flags & FUSE_FSYNC_FDATASYNC); llffi.fh = arg->fh; dh = get_dirhandle(&llffi,&ffi); if(not dh) { fuse_reply_err(req_,EBADF); return; } err = f.ops.fsyncdir(&req_->ctx, &ffi, is_datasync); fuse_reply_err(req_,err); } static void fuse_lib_statfs(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err = 0; fs::relpath fusepath; struct statvfs buf = {}; if(hdr_->nodeid) err = get_path(hdr_->nodeid,&fusepath); if(!err) { err = f.ops.statfs(&req_->ctx, fusepath, &buf); free_path(hdr_->nodeid,&fusepath); } if(!err) fuse_reply_statfs(req_,&buf); else fuse_reply_err(req_,err); } static void fuse_lib_setxattr(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err; fs::relpath fusepath; const char *name; const char *value; struct fuse_setxattr_in *arg; arg = (fuse_setxattr_in*)fuse_hdr_arg(hdr_); if((req_->conn.capable & FUSE_SETXATTR_EXT) && (req_->conn.want & FUSE_SETXATTR_EXT)) name = (const char*)PARAM(arg); else name = (((char*)arg) + FUSE_COMPAT_SETXATTR_IN_SIZE); value = (name + strlen(name) + 1); err = get_path(hdr_->nodeid,&fusepath); if(!err) { err = f.ops.setxattr(&req_->ctx, fusepath, name, value, arg->size, arg->flags); free_path(hdr_->nodeid,&fusepath); } fuse_reply_err(req_,err); } static int common_getxattr(fuse_req_t *req_, uint64_t ino, const char *name, char *value, size_t size) { int err; fs::relpath fusepath; err = get_path(ino,&fusepath); if(!err) { err = f.ops.getxattr(&req_->ctx, fusepath, name, value, size); free_path(ino,&fusepath); } return err; } static void fuse_lib_getxattr(fuse_req_t *req_, struct fuse_in_header *hdr_) { int res; const char* name; struct fuse_getxattr_in *arg; arg = (fuse_getxattr_in*)fuse_hdr_arg(hdr_); name = (const char*)PARAM(arg); if(arg->size) { char *value = (char*)malloc(arg->size); if(value == NULL) { fuse_reply_err(req_,ENOMEM); return; } res = common_getxattr(req_,hdr_->nodeid,name,value,arg->size); if(res > 0) fuse_reply_buf(req_,value,res); else fuse_reply_err(req_,res); free(value); } else { res = common_getxattr(req_,hdr_->nodeid,name,NULL,0); if(res >= 0) fuse_reply_xattr(req_,res); else fuse_reply_err(req_,res); } } static int common_listxattr(fuse_req_t *req_, uint64_t ino, char *list, size_t size) { int err; fs::relpath fusepath; err = get_path(ino,&fusepath); if(!err) { err = f.ops.listxattr(&req_->ctx, fusepath, list, size); free_path(ino,&fusepath); } return err; } static void fuse_lib_listxattr(fuse_req_t *req_, struct fuse_in_header *hdr_) { int res; struct fuse_getxattr_in *arg; arg = (fuse_getxattr_in*)fuse_hdr_arg(hdr_); if(arg->size) { char *list = (char*)malloc(arg->size); if(list == NULL) { fuse_reply_err(req_,ENOMEM); return; } res = common_listxattr(req_,hdr_->nodeid,list,arg->size); if(res > 0) fuse_reply_buf(req_,list,res); else fuse_reply_err(req_,res); free(list); } else { res = common_listxattr(req_,hdr_->nodeid,NULL,0); if(res >= 0) fuse_reply_xattr(req_,res); else fuse_reply_err(req_,res); } } static void fuse_lib_removexattr(fuse_req_t *req_, const struct fuse_in_header *hdr_) { int err; fs::relpath fusepath; const char *name; name = (const char*)fuse_hdr_arg(hdr_); err = get_path(hdr_->nodeid,&fusepath); if(!err) { err = f.ops.removexattr(&req_->ctx, fusepath, name); free_path(hdr_->nodeid,&fusepath); } fuse_reply_err(req_,err); } static void fuse_lib_copy_file_range(fuse_req_t *req_, const struct fuse_in_header *hdr_) { ssize_t rv; fuse_file_info_t ffi_in = {}; fuse_file_info_t ffi_out = {}; const struct fuse_copy_file_range_in *arg; struct fuse_copy_file_range_in arg_tmp; arg = (fuse_copy_file_range_in*)fuse_hdr_arg(hdr_); if(((hdr_->opcode == FUSE_COPY_FILE_RANGE) || (sizeof(size_t) == 4)) && (arg->len > 0xfffff000ULL)) { /* Avoid truncation in the old reply format or 32-bit size_t conversion. */ arg_tmp = *arg; arg_tmp.len = 0xfffff000ULL; arg = &arg_tmp; } ffi_in.fh = arg->fh_in; ffi_out.fh = arg->fh_out; rv = f.ops.copy_file_range(&req_->ctx, &ffi_in, arg->off_in, &ffi_out, arg->off_out, arg->len, arg->flags); if(rv >= 0) { if(hdr_->opcode == FUSE_COPY_FILE_RANGE_64) fuse_reply_copy_file_range_64(req_,rv); else fuse_reply_write(req_,rv); } else fuse_reply_err(req_,rv); } static void fuse_lib_setupmapping(fuse_req_t *req_, const struct fuse_in_header *hdr_) { fuse_reply_err(req_,ENOSYS); } static void fuse_lib_removemapping(fuse_req_t *req_, const struct fuse_in_header *hdr_) { fuse_reply_err(req_,ENOSYS); } static void fuse_lib_syncfs(fuse_req_t *req_, const struct fuse_in_header *hdr_) { fuse_reply_err(req_,ENOSYS); } // name is always '/' // nodeid is the base directory static void fuse_lib_tmpfile(fuse_req_t *req_, const struct fuse_in_header *hdr_) { int err; fs::relpath fusepath; const char *name; uint64_t new_nodeid; fuse_file_info_t ffi = {}; struct fuse_entry_param e; struct fuse_create_in *arg; arg = (fuse_create_in*)fuse_hdr_arg(hdr_); name = (const char*)PARAM(arg); ffi.flags = arg->flags; if(req_->conn.proto_minor >= 12) req_->ctx.umask = arg->umask; else name = (char*)arg + sizeof(struct fuse_open_in); // Opportunistically allocate a provisional node so passthrough tmpfile has // a nodeid. It is remembered only after tmpfile fully succeeds. { node_t *node; node = find_node(hdr_->nodeid,name,false); if(node == NULL) { fuse_reply_err(req_,ENOMEM); return; } new_nodeid = node->nodeid; req_->ctx.nodeid = new_nodeid; } err = get_path_name(hdr_->nodeid,name,&fusepath); if(!err) { err = f.ops.tmpfile(&req_->ctx, fusepath, arg->mode, &ffi); if(!err) { err = lookup_path(req_,hdr_->nodeid,name,fusepath,&e,&ffi,false); if(err) { f.ops.release(&req_->ctx, &ffi); } else if(!S_ISREG(e.attr.st_mode)) { err = -EIO; f.ops.release(&req_->ctx, &ffi); forget_node(e.ino,1); } } } if(!err) { free_path_and_inc_open(hdr_->nodeid,e.ino,true); fusepath.clear(); if(fuse_reply_create(req_,&e,&ffi) == -ENOENT) { /* The open syscall was interrupted,so it must be cancelled */ fuse_do_release(&req_->ctx,e.ino,&ffi); forget_node(e.ino,1); } } else { forget_node(new_nodeid,1); fuse_reply_err(req_,err); } free_path(hdr_->nodeid,&fusepath); } static int fuse_flush_common(fuse_req_t *req_, fuse_file_info_t *ffi_) { return f.ops.flush(&req_->ctx, ffi_); } static void fuse_lib_release(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err = 0; fuse_file_info_t ffi = {}; struct fuse_release_in *arg; arg = (fuse_release_in*)fuse_hdr_arg(hdr_); ffi.fh = arg->fh; ffi.flags = arg->flags; if(req_->conn.proto_minor >= 8) { ffi.flush = !!(arg->release_flags & FUSE_RELEASE_FLUSH); ffi.lock_owner = arg->lock_owner; } else { ffi.flock_release = 1; ffi.lock_owner = arg->lock_owner; } if(ffi.flush) { err = fuse_flush_common(req_,&ffi); if(err == -ENOSYS) err = 0; } fuse_do_release(&req_->ctx, hdr_->nodeid, &ffi); fuse_reply_err(req_,err); } static void fuse_lib_flush(fuse_req_t *req_, struct fuse_in_header *hdr_) { int err; fuse_file_info_t ffi = {}; struct fuse_flush_in *arg; arg = (fuse_flush_in*)fuse_hdr_arg(hdr_); ffi.fh = arg->fh; ffi.flush = 1; if(req_->conn.proto_minor >= 7) ffi.lock_owner = arg->lock_owner; err = fuse_flush_common(req_,&ffi); fuse_reply_err(req_,err); } static void fuse_lib_bmap(fuse_req_t *req_, const struct fuse_in_header *hdr_) { int err; fs::relpath fusepath; uint64_t block; const struct fuse_bmap_in *arg; arg = (fuse_bmap_in*)fuse_hdr_arg(hdr_); block = arg->block; err = get_path(hdr_->nodeid,&fusepath); if(!err) { err = f.ops.bmap(&req_->ctx, fusepath, arg->blocksize, &block); free_path(hdr_->nodeid,&fusepath); } if(!err) fuse_reply_bmap(req_,block); else fuse_reply_err(req_,err); } static void fuse_lib_ioctl(fuse_req_t *req_, const struct fuse_in_header *hdr_) { int err; char *out_buf = NULL; fuse_dh *dh; fuse_file_info_t ffi = {}; fuse_file_info_t llffi = {}; const void *in_buf; uint32_t out_size; const struct fuse_ioctl_in *arg; arg = (fuse_ioctl_in*)fuse_hdr_arg(hdr_); err = -EPERM; if(arg->flags & FUSE_IOCTL_UNRESTRICTED) goto err; if((sizeof(void*) == 4) && (req_->conn.proto_minor >= 16) && !(arg->flags & FUSE_IOCTL_32BIT)) { req_->ioctl_64bit = 1; } llffi.fh = arg->fh; out_size = arg->out_size; in_buf = (arg->in_size ? PARAM(arg) : NULL); if(arg->flags & FUSE_IOCTL_DIR) { dh = get_dirhandle(&llffi,&ffi); if(not dh) { err = -EBADF; goto err; } } else { ffi = llffi; } if((arg->in_size > 0) && (out_size > 0) && (arg->in_size != out_size)) { err = -EINVAL; goto err; } if(out_size) { err = -ENOMEM; out_buf = (char*)malloc(out_size); if(!out_buf) goto err; } if(in_buf && out_buf) memcpy(out_buf,in_buf,out_size); err = f.ops.ioctl(&req_->ctx, &ffi, arg->cmd, (void*)(uintptr_t)arg->arg, arg->flags, out_buf ? out_buf : (void*)in_buf, &out_size); if(err < 0) goto err; fuse_reply_ioctl(req_,err,out_buf,out_size); goto out; err: fuse_reply_err(req_,err); out: free(out_buf); } static void fuse_lib_poll(fuse_req_t *req_, const struct fuse_in_header *hdr_) { int err; unsigned revents = 0; fuse_file_info_t ffi = {}; fuse_pollhandle_t *ph = NULL; const struct fuse_poll_in *arg; arg = (fuse_poll_in*)fuse_hdr_arg(hdr_); ffi.fh = arg->fh; if(arg->flags & FUSE_POLL_SCHEDULE_NOTIFY) { ph = (fuse_pollhandle_t*)malloc(sizeof(fuse_pollhandle_t)); if(ph == NULL) { fuse_reply_err(req_,ENOMEM); return; } ph->kh = arg->kh; ph->se = req_->se; } err = f.ops.poll(&req_->ctx, &ffi, ph, &revents); if(!err) fuse_reply_poll(req_,revents); else fuse_reply_err(req_,err); } static void fuse_lib_fallocate(fuse_req_t *req_, const struct fuse_in_header *hdr_) { int err; const struct fuse_fallocate_in *arg; arg = (fuse_fallocate_in*)fuse_hdr_arg(hdr_); err = f.ops.fallocate(&req_->ctx, arg->fh, arg->mode, arg->offset, arg->length); fuse_reply_err(req_,err); } static struct fuse_lowlevel_ops fuse_path_ops = { .access = fuse_lib_access, .bmap = fuse_lib_bmap, .copy_file_range = fuse_lib_copy_file_range, .create = fuse_lib_create, .destroy = fuse_lib_destroy, .fallocate = fuse_lib_fallocate, .flush = fuse_lib_flush, .forget = fuse_lib_forget, .forget_multi = fuse_lib_forget_multi, .fsync = fuse_lib_fsync, .fsyncdir = fuse_lib_fsyncdir, .getattr = fuse_lib_getattr, .getxattr = fuse_lib_getxattr, .init = fuse_lib_init, .ioctl = fuse_lib_ioctl, .link = fuse_lib_link, .listxattr = fuse_lib_listxattr, .lookup = fuse_lib_lookup, .lseek = fuse_lib_lseek, .mkdir = fuse_lib_mkdir, .mknod = fuse_lib_mknod, .open = fuse_lib_open, .opendir = fuse_lib_opendir, .poll = fuse_lib_poll, .read = fuse_lib_read, .readdir = fuse_lib_readdir, .readdir_plus = fuse_lib_readdir_plus, .readlink = fuse_lib_readlink, .release = fuse_lib_release, .releasedir = fuse_lib_releasedir, .removemapping = fuse_lib_removemapping, .removexattr = fuse_lib_removexattr, .rename = fuse_lib_rename, .rename2 = fuse_lib_rename2, .retrieve_reply = fuse_lib_retrieve_reply, .rmdir = fuse_lib_rmdir, .setattr = fuse_lib_setattr, .setupmapping = fuse_lib_setupmapping, .setxattr = fuse_lib_setxattr, .statfs = fuse_lib_statfs, .statx = fuse_lib_statx, .symlink = fuse_lib_symlink, .syncfs = fuse_lib_syncfs, .tmpfile = fuse_lib_tmpfile, .unlink = fuse_lib_unlink, .write = fuse_lib_write, }; int fuse_notify_poll(fuse_pollhandle_t *ph) { return fuse_lowlevel_notify_poll(ph); } int fuse_exited() { return fuse_session_exited(f.se); } struct fuse_session* fuse_get_session() { return f.se; } void fuse_exit() { f.se->exited = 1; } static int node_table_init(struct node_table *t) { t->size = NODE_TABLE_MIN_SIZE; t->array = (node_t **)calloc(1,sizeof(node_t *) * t->size); if(t->array == NULL) { fprintf(stderr,"fuse: memory allocation failed\n"); return -1; } t->use = 0; t->split = 0; return 0; } static void metrics_log_nodes_info(FILE *file_) { char buf[1024]; char time_str[64]; struct tm tm; struct timeval tv; uint64_t sizeof_node; gettimeofday(&tv,NULL); localtime_r(&tv.tv_sec,&tm); strftime(time_str,sizeof(time_str),"%Y-%m-%dT%H:%M:%S.000%z",&tm); sizeof_node = sizeof(node_t); snprintf(buf,sizeof(buf), "time: %s\n" "sizeof(node): %" PRIu64 "\n" "node id_table size: %" PRIu64 "\n" "node id_table usage: %" PRIu64 "\n" "node id_table total allocated memory: %" PRIu64 "\n" "node name_table size: %" PRIu64 "\n" "node name_table usage: %" PRIu64 "\n" "node name_table total allocated memory: %" PRIu64 "\n" "msgbuf bufsize: %" PRIu64 "\n" "msgbuf allocation count: %" PRIu64 "\n" "msgbuf total allocated memory: %" PRIu64 "\n" "\n" , time_str, sizeof_node, (uint64_t)f.id_table.size, (uint64_t)f.id_table.use, (uint64_t)(f.id_table.size * sizeof(node_t*)), (uint64_t)f.name_table.size, (uint64_t)f.name_table.use, (uint64_t)(f.name_table.size * sizeof(node_t*)), msgbuf_get_bufsize(), msgbuf_alloc_count(), msgbuf_alloc_count() * msgbuf_get_bufsize() ); fputs(buf,file_); } static void metrics_log_nodes_info_to_tmp_dir(struct fuse *f_) { int rv; FILE *file; char filepath[256]; struct stat st; char const *mode = "a"; snprintf(filepath, sizeof(filepath), "/tmp/mergerfs.%d.info", getpid()); rv = lstat(filepath,&st); if((rv == 0) && (st.st_size > INFO_FILE_MAX_SIZE)) mode = "w"; file = fopen(filepath,mode); if(file == NULL) return; metrics_log_nodes_info(file); fclose(file); } static void fuse_malloc_trim(void) { #ifdef __GLIBC__ malloc_trim(1024 * 1024); #endif } void fuse_invalidate_all_nodes() { std::vector names; mutex_lock(f.lock); for(size_t i = 0; i < f.id_table.size; i++) { node_t *node; for(node = f.id_table.array[i]; node != NULL; node = node->id_next) { if(node->nodeid == FUSE_ROOT_ID) continue; if(node->name == NULL) continue; if(node->parent == NULL) continue; if(node->parent->nodeid != FUSE_ROOT_ID) continue; names.emplace_back(node->name); } } mutex_unlock(f.lock); SysLog::info("invalidating {} file entries", names.size()); for(auto &name : names) { fuse_lowlevel_notify_inval_entry(f.se, FUSE_ROOT_ID, name.c_str(), name.size()); } } void fuse_gc() { SysLog::info("running thorough garbage collection"); node_clear(); msgbuf_clear(); fuse_malloc_trim(); } void fuse_gc1() { SysLog::info("running basic garbage collection"); node_gc(); msgbuf_gc(); fuse_malloc_trim(); } void fuse_populate_maintenance_thread(struct fuse *f_) { MaintenanceThread::push_job([](u64 count_) { if((count_ % 15) == 0) fuse_gc1(); }); MaintenanceThread::push_job([=](u64 count_) { if(fuse_cfg.debug) metrics_log_nodes_info_to_tmp_dir(f_); }); } struct fuse* fuse_new(int fd_, size_t bufsize_, struct fuse_args *args, const struct fuse_operations *ops_) { node_t *root; struct fuse_lowlevel_ops llop = fuse_path_ops; f.ops = *ops_; f.se = fuse_lowlevel_new_common(args,&llop,sizeof(llop)); if(f.se == NULL) goto out_free_fs; /* Inlined channel fields - direct assignment */ f.se->fd = fd_; f.se->bufsize = bufsize_; /* Trace topmost layer by default */ srand(time(NULL)); f.nodeid_gen.nodeid = FUSE_ROOT_ID; f.nodeid_gen.generation = rand64(); if(node_table_init(&f.name_table) == -1) goto out_free_session; if(node_table_init(&f.id_table) == -1) goto out_free_name_table; mutex_init(f.lock); root = node_alloc(); if(root == NULL) { fprintf(stderr,"mergerfs: root node alloc failed\n"); goto out_free_id_table; } root->name = xstrdup("/"); root->parent = NULL; root->nodeid = FUSE_ROOT_ID; inc_nlookup(root); hash_id(root); return &f; out_free_id_table: free(f.id_table.array); out_free_name_table: free(f.name_table.array); out_free_session: fuse_session_destroy(f.se); out_free_fs: /* Horrible compatibility hack to stop the destructor from being called on the filesystem without init being called first */ f.ops.destroy = NULL; return NULL; } void fuse_destroy(struct fuse *) { size_t i; for(i = 0; i < f.id_table.size; i++) { node_t *node; node_t *next; for(node = f.id_table.array[i]; node != NULL; node = next) { next = node->id_next; free_node(node); f.id_table.use--; } } free(f.id_table.array); free(f.name_table.array); mutex_destroy(f.lock); fuse_session_destroy(f.se); } int fuse_passthrough_open(const int fd_) { int rv; int dev_fuse_fd; struct fuse_backing_map bm = {}; dev_fuse_fd = f.se->fd; bm.fd = fd_; rv = ::ioctl(dev_fuse_fd,FUSE_DEV_IOC_BACKING_OPEN,&bm); return ((rv < 0) ? INVALID_BACKING_ID : rv); } int fuse_passthrough_close(const int backing_id_) { int dev_fuse_fd; if(!fuse_backing_id_is_valid(backing_id_)) return 0; dev_fuse_fd = f.se->fd; return ::ioctl(dev_fuse_fd,FUSE_DEV_IOC_BACKING_CLOSE,&backing_id_); }