/*
* Copyright © 2018 Red Hat, Inc
* Copyright © 2023 GNOME Foundation Inc.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see .
*
* Authors:
* Alexander Larsson
* Hubert Figuière
*/
#include "config.h"
#define FUSE_USE_VERSION 35
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#ifdef HAVE_SYS_STATFS_H
#include
#endif
#include
#ifdef HAVE_SYS_MOUNT_H
#include
#endif
#ifdef HAVE_SYS_XATTR_H
#include
#endif
#ifdef HAVE_SYS_EXTATTR_H
#include
#endif
#include
#include
#include "document-portal-fuse.h"
#include "document-store.h"
#include "src/xdp-utils.h"
#ifndef O_FSYNC
#define O_FSYNC O_SYNC
#endif
#ifndef ENODATA
#define ENODATA ENOATTR
#endif
#define XDP_XATTR_HOST_PATH "user.document-portal.host-path"
/* man listxattr:
* The list is the set of (null-terminated) names, one after the other.
*
* Example:
* user.name1\0system.name1\0user.name2\0
*
* We can statically define the string by doing this:
* XDP_XATTR_MY_ATTR "\0" XDP_XATTR_ANOTHER_ATTR
*/
#define XDP_XATTR_ATTRIBUTE_NAME_LIST XDP_XATTR_HOST_PATH
/* Inode ownership model
*
* The document portal exposes something as a filesystem that it
* doesn't have full control over. For instance at any point some
* other process can rename an exposed file on the real filesystem and
* we won't be told about this. This means that in general we always
* return 0 for the cacheable lifetimes of entries and file attributes.
* (Except for the virtual directories we have full control of, the
* section below only discusses real files).
*
* However, even though we don't have full control of the underlying
* filesystem the *kernel* has. This means we can use that to get
* the correct semantics.
*
* For example, assume that a directory is held open by a process
* (for example, it could be the CWD of the process). When we open
* the directory via a LOOKUP operation we return an inode to it,
* and, for as long as the kernel has this inode around (i.e. until it
* sent a FORGET message), it can send operations on this inode without
* looking it up again. For example, if the above process used a
* relative path.
*
* Now, consider the case where the app chdir():ed into the fuse
* directory, but after the backing directory was renamed outside
* the fuse filesystem. The fuse inode representation for the inode
* cannot be the directory name, because the expected semantics is
* that further relative pathnames from the app will still resolve
* to the same directory independent of its location in the tree.
*
* The way we do this is to keep a O_PATH file descriptor around for
* each underlying inode. This is represented by the XdpPhysicalInode
* type and we have a hashtable from backing dev+inode to a these
* so that we can use one fd per backing inode even when the file
* is visible in many places.
*
* Since we don't do any caching, each LOOKUP operation will do a
* statat() on the underlying filesystem. However, we then use the
* result of that to lookup (via backing dev+ino) the previous inode
* (as long as it still lives) if the backing file was unchanged.
*
* One problem with this approach is that the kernel tends to keep
* inodes alive for a very long time even if they are *only*
* referenced by the dcache (Directory Entry Cache) (even though we
* will not really use the dcache info due to the 0 valid time). This
* is unfortunate, because it means we will keep a lot of file
* descriptors open. But, we can not know if the kernel needs the inode
* for some non-dcache use so we can't close the file descriptors.
*
* To work around this we regularly emit entry invalidation calls
* to the kernel, which will make it forget the inodes that are
* only pinned by the dcache.
*/
#define NON_DOC_DIR_PERMS 0500
#define DOC_DIR_PERMS_FILE 0700
#define DOC_DIR_PERMS_DIR 0500
static GThread *fuse_thread = NULL;
static struct fuse_session *session = NULL;
G_LOCK_DEFINE (session);
static char *mount_path = NULL;
static pthread_t fuse_pthread = 0;
static uid_t my_uid;
static gid_t my_gid;
static GList *open_files = NULL;
G_LOCK_DEFINE (open_files);
/* from libfuse */
#define FUSE_UNKNOWN_INO 0xffffffff
#define BY_APP_NAME "by-app"
typedef struct {
ino_t ino;
dev_t dev;
} DevIno;
typedef enum {
XDP_DOMAIN_ROOT,
XDP_DOMAIN_BY_APP,
XDP_DOMAIN_APP,
XDP_DOMAIN_DOCUMENT,
} XdpDomainType;
typedef struct _XdpDomain XdpDomain;
typedef struct _XdpInode XdpInode;
struct _XdpDomain {
gint ref_count; /* atomic */
XdpDomainType type;
XdpDomain *parent;
XdpInode *parent_inode; /* Inode of the parent domain (NULL for root) */
char *doc_id; /* NULL for root, by-app, app */
char *app_id; /* NULL for root, by-app, non-app id */
/* root: by docid
* app: by docid
* by_app: by app
* document: by physical
*/
GHashTable *inodes; /* Protected by domain_inodes */
/* Below only used for XDP_DOMAIN_DOCUMENT */
char *doc_path; /* path to the directory the files are in */
char *doc_file; /* != NULL for non-directory documents */
guint64 doc_dir_device;
guint64 doc_dir_inode;
guint32 doc_flags;
/* Below is mutable, protected by mutex */
GMutex tempfile_mutex;
GHashTable *tempfiles; /* Name -> physical */
};
static void xdp_domain_unref (XdpDomain *domain);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpDomain, xdp_domain_unref)
G_LOCK_DEFINE (domain_inodes);
typedef struct {
gint ref_count; /* atomic */
DevIno backing_devino;
int fd; /* O_PATH fd */
} XdpPhysicalInode;
static XdpPhysicalInode *xdp_physical_inode_ref (XdpPhysicalInode *inode);
static void xdp_physical_inode_unref (XdpPhysicalInode *inode);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpPhysicalInode, xdp_physical_inode_unref)
typedef struct {
gint ref_count; /* atomic */
char *name; /* This changes over time (i.e. in renames)
protected by domain->tempfile_mutex,
used as key in domain->tempfiles */
char *tempname; /* Real filename on disk.
This can be NULLed to avoid unlink at finalize */
XdpInode *inode;
} XdpTempfile;
static XdpTempfile *xdp_tempfile_ref (XdpTempfile *tempfile);
static void xdp_tempfile_unref (XdpTempfile *tempfile);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpTempfile, xdp_tempfile_unref)
struct _XdpInode {
guint64 ino;
gint ref_count; /* atomic, includes one ref if kernel_ref_count != 0 */
gint kernel_ref_count; /* atomic */
XdpDomain *domain;
/* The below are only used for XDP_DOMAIN_DOCUMENT inodes */
XdpPhysicalInode *physical;
/* The root of the domain, or NULL for the domain. We use this to
* keep the root document inode alive so that when the kernel
* forgets it and then looks it up we will not get a new inode and
* thus a new domain. */
XdpInode *domain_root_inode;
};
typedef struct {
int fd;
GList *link;
} XdpFile;
typedef struct {
DIR *dir;
struct dirent *entry;
off_t offset;
char *dirbuf;
gsize dirbuf_size;
} XdpDir;
XdpInode *root_inode;
XdpInode *by_app_inode;
typedef struct {
ino_t parent_ino;
char name[0];
} XdpInvalidateData;
typedef struct {
gboolean use_splice;
} XdpFuseOptions;
static GList *invalidate_list;
G_LOCK_DEFINE (invalidate_list);
static XdpInode *xdp_inode_ref (XdpInode *inode);
static void xdp_inode_unref (XdpInode *inode);
/* Lookup by inode for verification */
static GHashTable *all_inodes; /* guint64 -> XdpInode */
static guint64 next_virtual_inode = FUSE_ROOT_ID; /* root is the first inode created, so it gets this */
G_LOCK_DEFINE (all_inodes);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpInode, xdp_inode_unref)
static int ensure_docdir_inode (XdpInode *parent,
int o_path_fd_in, /* Takes ownership */
struct fuse_entry_param *e,
XdpInode **inode_out);
static void queue_invalidate_dentry (XdpInode *parent, const char *name);
static gboolean
app_can_write_doc (PermissionDbEntry *entry, const char *app_id)
{
if (app_id == NULL)
return TRUE;
if (document_entry_has_permissions_by_app_id (entry, app_id, DOCUMENT_PERMISSION_FLAGS_WRITE))
return TRUE;
return FALSE;
}
static gboolean
app_can_see_doc (PermissionDbEntry *entry, const char *app_id)
{
if (app_id == NULL)
return TRUE;
if (document_entry_has_permissions_by_app_id (entry, app_id, DOCUMENT_PERMISSION_FLAGS_READ))
return TRUE;
return FALSE;
}
static char *
fd_to_path (int fd)
{
return g_strdup_printf ("/proc/self/fd/%d", fd);
}
static char *
open_flags_to_string (int flags)
{
GString *s;
const char *mode;
switch (flags & O_ACCMODE)
{
case O_RDONLY:
mode = "RDONLY";
break;
case O_WRONLY:
mode = "WRONLY";
break;
case O_RDWR:
default:
mode = "RDWR";
break;
}
s = g_string_new (mode);
if (flags & O_NONBLOCK)
g_string_append (s, ",NONBLOCK");
if (flags & O_APPEND)
g_string_append (s, ",APPEND");
if (flags & O_SYNC)
g_string_append (s, ",SYNC");
if (flags & O_ASYNC)
g_string_append (s, ",ASYNC");
if (flags & O_FSYNC)
g_string_append (s, ",FSYNC");
#ifdef O_DSYNC
if (flags & O_DSYNC)
g_string_append (s, ",DSYNC");
#endif
if (flags & O_CREAT)
g_string_append (s, ",CREAT");
if (flags & O_TRUNC)
g_string_append (s, ",TRUNC");
if (flags & O_EXCL)
g_string_append (s, ",EXCL");
if (flags & O_CLOEXEC)
g_string_append (s, ",CLOEXEC");
if (flags & O_DIRECT)
g_string_append (s, ",DIRECT");
#ifdef O_LARGEFILE
if (flags & O_LARGEFILE)
g_string_append (s, ",LARGEFILE");
#endif
#ifdef O_NOATIME
if (flags & O_NOATIME)
g_string_append (s, ",NOATIME");
#endif
if (flags & O_NOCTTY)
g_string_append (s, ",NOCTTY");
if (flags & O_PATH)
g_string_append (s, ",PATH");
#ifdef O_TMPFILE
if (flags & O_TMPFILE)
g_string_append (s, ",TMPFILE");
#endif
return g_string_free (s, FALSE);
}
static char *
setattr_flags_to_string (int flags)
{
GString *s = g_string_new ("");
if (flags & FUSE_SET_ATTR_MODE)
g_string_append (s, "MODE,");
if (flags & FUSE_SET_ATTR_UID)
g_string_append (s, "UID,");
if (flags & FUSE_SET_ATTR_GID)
g_string_append (s, "GID,");
if (flags & FUSE_SET_ATTR_SIZE)
g_string_append (s, "SIZE,");
if (flags & FUSE_SET_ATTR_ATIME)
g_string_append (s, "ATIME,");
if (flags & FUSE_SET_ATTR_MTIME)
g_string_append (s, "MTIME,");
if (flags & FUSE_SET_ATTR_ATIME_NOW)
g_string_append (s, "ATIME_NOW,");
if (flags & FUSE_SET_ATTR_MTIME_NOW)
g_string_append (s, "MTIME_NOW,");
/* Remove last comma */
if (s->len > 0)
g_string_truncate (s, s->len - 1);
return g_string_free (s, FALSE);
}
static char *
renameat2_flags_to_string (int flags)
{
#if HAVE_RENAMEAT2
GString *s = g_string_new ("");
if (flags & RENAME_EXCHANGE)
g_string_append (s, "EXCHANGE,");
if (flags & RENAME_NOREPLACE)
g_string_append (s, "NOREPLACE,");
if (flags & RENAME_WHITEOUT)
g_string_append (s, "WHITEOUT,");
/* Remove last comma */
if (s->len > 0)
g_string_truncate (s, s->len - 1);
return g_string_free (s, FALSE);
#else
return g_strdup_printf ("%#x", flags);
#endif
}
static guint
devino_hash (gconstpointer key)
{
DevIno *devino = (DevIno *)key;
return (devino->ino >> 2) ^ devino->dev;
}
static gboolean
devino_equal (gconstpointer _a,
gconstpointer _b)
{
DevIno *a = (DevIno *)_a;
DevIno *b = (DevIno *)_b;
return a->ino == b->ino && a->dev == b->dev;
}
/* Lookup by physical backing devino */
static GHashTable *physical_inodes;
G_LOCK_DEFINE (physical_inodes);
/* Takes ownership of the o_path fd if passed in */
static XdpPhysicalInode *
ensure_physical_inode (dev_t dev, ino_t ino, int o_path_fd)
{
DevIno devino = {ino, dev};
XdpPhysicalInode *inode = NULL;
G_LOCK (physical_inodes);
inode = g_hash_table_lookup (physical_inodes, &devino);
if (inode != NULL)
{
inode = xdp_physical_inode_ref (inode);
close (o_path_fd);
}
else
{
/* Takes ownership of fd */
inode = g_new0 (XdpPhysicalInode, 1);
inode->ref_count = 1;
inode->fd = o_path_fd;
inode->backing_devino = devino;
g_hash_table_insert (physical_inodes, &inode->backing_devino, inode);
}
G_UNLOCK (physical_inodes);
return inode;
}
static XdpPhysicalInode *
xdp_physical_inode_ref (XdpPhysicalInode *inode)
{
g_atomic_int_inc (&inode->ref_count);
return inode;
}
static void
xdp_physical_inode_unref (XdpPhysicalInode *inode)
{
gint old_ref;
/* here we want to atomically do: if (ref_count>1) { ref_count--; return; } */
retry_atomic_decrement1:
old_ref = g_atomic_int_get (&inode->ref_count);
if (old_ref > 1)
{
if (!g_atomic_int_compare_and_exchange ((int *) &inode->ref_count, old_ref, old_ref - 1))
goto retry_atomic_decrement1;
}
else
{
if (old_ref <= 0)
{
g_warning ("Can't unref dead inode");
return;
}
/* Might be revived from physical_inodes hash by this time, so protect by lock */
G_LOCK (physical_inodes);
if (!g_atomic_int_compare_and_exchange ((int *) &inode->ref_count, old_ref, old_ref - 1))
{
G_UNLOCK (physical_inodes);
goto retry_atomic_decrement1;
}
g_hash_table_remove (physical_inodes, &inode->backing_devino);
G_UNLOCK (physical_inodes);
close (inode->fd);
g_free (inode);
}
}
static XdpDomain *
xdp_domain_ref (XdpDomain *domain)
{
g_atomic_int_inc (&domain->ref_count);
return domain;
}
static void
xdp_domain_unref (XdpDomain *domain)
{
if (g_atomic_int_dec_and_test (&domain->ref_count))
{
g_free (domain->doc_id);
g_free (domain->app_id);
g_free (domain->doc_path);
g_free (domain->doc_file);
if (domain->inodes)
g_assert (g_hash_table_size (domain->inodes) == 0);
g_clear_pointer (&domain->inodes, g_hash_table_unref);
g_clear_pointer (&domain->parent, xdp_domain_unref);
g_clear_pointer (&domain->parent_inode, xdp_inode_unref);
g_clear_pointer (&domain->tempfiles, g_hash_table_unref);
g_mutex_clear (&domain->tempfile_mutex);
g_free (domain);
}
}
static gboolean
xdp_domain_is_virtual_type (XdpDomain *domain)
{
return
domain->type == XDP_DOMAIN_ROOT ||
domain->type == XDP_DOMAIN_BY_APP ||
domain->type == XDP_DOMAIN_APP;
}
static XdpDomain *
_xdp_domain_new (XdpDomainType type)
{
XdpDomain *domain = g_new0 (XdpDomain, 1);
domain->ref_count = 1;
domain->type = type;
g_mutex_init (&domain->tempfile_mutex);
return domain;
}
static XdpDomain *
xdp_domain_new_root (void)
{
XdpDomain *domain = _xdp_domain_new (XDP_DOMAIN_ROOT);
domain->inodes = g_hash_table_new (g_str_hash, g_str_equal);
return domain;
}
static XdpDomain *
xdp_domain_new_by_app (XdpInode *root_inode)
{
XdpDomain *root_domain = root_inode->domain;
XdpDomain *domain = _xdp_domain_new (XDP_DOMAIN_BY_APP);
domain->parent = xdp_domain_ref (root_domain);
domain->parent_inode = xdp_inode_ref (root_inode);
domain->inodes = g_hash_table_new (g_str_hash, g_str_equal);
return domain;
}
static XdpDomain *
xdp_domain_new_app (XdpInode *parent_inode,
const char *app_id)
{
XdpDomain *parent = parent_inode->domain;
XdpDomain *domain = _xdp_domain_new (XDP_DOMAIN_APP);
domain->parent = xdp_domain_ref (parent);
domain->parent_inode = xdp_inode_ref (parent_inode);
domain->app_id = g_strdup (app_id);
domain->inodes = g_hash_table_new (g_str_hash, g_str_equal);
return domain;
}
static gboolean
xdp_document_domain_is_dir (XdpDomain *domain)
{
return (domain->doc_flags & DOCUMENT_ENTRY_FLAG_DIRECTORY) != 0;
}
static XdpDomain *
xdp_domain_new_document (XdpDomain *parent,
const char *doc_id,
PermissionDbEntry *doc_entry)
{
XdpDomain *domain = _xdp_domain_new (XDP_DOMAIN_DOCUMENT);
const char *db_path;
domain->parent = xdp_domain_ref (parent);
domain->doc_id = g_strdup (doc_id);
domain->app_id = g_strdup (parent->app_id);
domain->inodes = g_hash_table_new (g_direct_hash, g_direct_equal);
domain->doc_flags = document_entry_get_flags (doc_entry);
domain->doc_dir_device = document_entry_get_device (doc_entry);
domain->doc_dir_inode = document_entry_get_inode (doc_entry);
db_path = document_entry_get_path (doc_entry);
if (xdp_document_domain_is_dir (domain))
{
domain->doc_path = g_strdup (db_path);
domain->doc_file = g_path_get_basename (db_path);
}
else
{
domain->doc_path = g_path_get_dirname (db_path);
domain->doc_file = g_path_get_basename (db_path);
}
domain->tempfiles = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)xdp_tempfile_unref);
return domain;
}
static gboolean
xdp_document_domain_can_see (XdpDomain *domain)
{
if (domain->app_id != NULL)
{
g_autoptr(PermissionDbEntry) entry = xdp_lookup_doc (domain->doc_id);
if (entry == NULL ||
!app_can_see_doc (entry, domain->app_id))
return FALSE;
}
return TRUE;
}
static gboolean
xdp_document_domain_can_write (XdpDomain *domain)
{
if (domain->app_id != NULL)
{
g_autoptr(PermissionDbEntry) entry = xdp_lookup_doc (domain->doc_id);
if (entry == NULL ||
!app_can_write_doc (entry, domain->app_id))
return FALSE;
}
return TRUE;
}
static char **
xdp_domain_get_inode_keys_as_string (XdpDomain *domain)
{
char **res;
guint length, i;
g_assert (domain->type == XDP_DOMAIN_BY_APP);
G_LOCK (domain_inodes);
res = (char **)g_hash_table_get_keys_as_array (domain->inodes, &length);
for (i = 0; i < length; i++)
res[i] = g_strdup (res[i]);
G_UNLOCK (domain_inodes);
return res;
}
static XdpTempfile *
xdp_tempfile_ref (XdpTempfile *tempfile)
{
g_atomic_int_inc (&tempfile->ref_count);
return tempfile;
}
static XdpTempfile *
xdp_tempfile_new (XdpInode *inode,
const char *name,
const char *tempname)
{
XdpTempfile *tempfile = g_new0 (XdpTempfile, 1);
tempfile->ref_count = 1;
tempfile->inode = xdp_inode_ref (inode);
tempfile->name = g_strdup (name);
tempfile->tempname = g_strdup (tempname);
return tempfile;
}
static void
xdp_tempfile_unref (XdpTempfile *tempfile)
{
if (g_atomic_int_dec_and_test (&tempfile->ref_count))
{
if (tempfile->tempname)
{
g_autofree char *temppath = g_build_filename (tempfile->inode->domain->doc_path, tempfile->tempname, NULL);
(void)unlink (temppath);
}
g_free (tempfile->name);
g_free (tempfile->tempname);
g_clear_pointer (&tempfile->inode, xdp_inode_unref);
g_free (tempfile);
}
}
static XdpInode *
_xdp_inode_new (void)
{
XdpInode *inode = g_new0 (XdpInode, 1);
inode->ref_count = 1;
inode->kernel_ref_count = 0;
return inode;
}
/* We try to create persistent inode number based on the backing
* device and inode numbers, as well as the doc/app id (since the same
* backing dev/ino should be different inodes in the fuse
* filesystem). We do this by hashing the data to generate a value.
* For non-physical files or accidental collisions we just pick a free
* number by incrementing.
*/
static guint64
generate_persistent_ino (DevIno *backing_devino,
const char *doc_id,
const char *app_id)
{
g_autoptr(GChecksum) checksum = g_checksum_new (G_CHECKSUM_MD5);
guchar digest[64];
gsize digest_len = 64;
guint64 res;
g_checksum_update (checksum, (guchar *)backing_devino, sizeof (DevIno));
if (doc_id)
g_checksum_update (checksum, (guchar *)doc_id, strlen (doc_id));
if (app_id)
g_checksum_update (checksum, (guchar *)app_id, strlen (app_id));
g_checksum_get_digest (checksum, digest, &digest_len);
res = *(guint64 *)digest;
if (res == FUSE_ROOT_ID || res == 0)
res = FUSE_ROOT_ID + 1;
return res;
}
/* takes ownership of fd */
static XdpInode *
xdp_inode_new (XdpDomain *domain,
XdpPhysicalInode *physical)
{
XdpInode *inode = _xdp_inode_new ();
inode->domain = xdp_domain_ref (domain);
guint64 persistent_ino, try_ino;
if (physical)
{
inode->physical = xdp_physical_inode_ref (physical);
persistent_ino = generate_persistent_ino (&physical->backing_devino,
domain->doc_id,
domain->app_id);
}
G_LOCK (all_inodes);
if (physical)
try_ino = persistent_ino;
else
try_ino = next_virtual_inode++;
if (g_hash_table_contains (all_inodes, &try_ino))
try_ino++;
inode->ino = try_ino;
g_hash_table_insert (all_inodes, &inode->ino, inode);
G_UNLOCK (all_inodes);
return inode;
}
static ino_t
xdp_inode_to_ino (XdpInode *inode)
{
return inode->ino;
}
/* This is called on kernel upcalls, so it *should* be guaranteed that the inode exists due to the kernel refs */
static XdpInode *
xdp_inode_from_ino (ino_t ino)
{
XdpInode *inode;
G_LOCK (all_inodes);
inode = g_hash_table_lookup (all_inodes, &ino);
G_UNLOCK (all_inodes);
g_assert (inode != NULL);
/* Its safe to ref it here because we know it exists outside the lock due to the kernel refs */
return xdp_inode_ref (inode);
}
static XdpInode *
xdp_inode_ref (XdpInode *inode)
{
g_atomic_int_inc (&inode->ref_count);
return inode;
}
static void
xdp_inode_unref (XdpInode *inode)
{
gint old_ref;
XdpDomain *domain;
/* here we want to atomically do: if (ref_count>1) { ref_count--; return; } */
retry_atomic_decrement1:
old_ref = g_atomic_int_get (&inode->ref_count);
if (old_ref > 1)
{
if (!g_atomic_int_compare_and_exchange ((int *) &inode->ref_count, old_ref, old_ref - 1))
goto retry_atomic_decrement1;
}
else
{
if (old_ref <= 0)
{
g_warning ("Can't unref dead inode");
return;
}
/* Might be revived from domain->inodes hash by this time, so protect by lock */
G_LOCK (domain_inodes);
if (!g_atomic_int_compare_and_exchange ((int *) &inode->ref_count, old_ref, old_ref - 1))
{
G_UNLOCK (domain_inodes);
goto retry_atomic_decrement1;
}
domain = inode->domain;
if (domain->type == XDP_DOMAIN_APP)
g_hash_table_remove (domain->parent->inodes, domain->app_id);
else if (domain->type == XDP_DOMAIN_DOCUMENT)
{
if (inode->physical)
{
g_hash_table_remove (domain->inodes, inode->physical);
}
else
g_hash_table_remove (domain->parent->inodes, domain->doc_id);
}
/* Run this under domain_inodes lock to avoid race condition in ensure_docdir_inode + xdp_inode_new
* where we don't want a domain->inode lookup to fail, but then an all_inode lookup to succeed
* when looking for an ino collision
*
* Note: After the domain->inodes removal and here we don't allow resurrection, but we may
* still race with an all_inodes lookup (e.g. in xdp_fuse_lookup_id_for_inode), which *is*
* allowed and it can read the inode fields (while the lock is held) as they are still valid.
**/
G_LOCK (all_inodes);
g_hash_table_remove (all_inodes, &inode->ino);
G_UNLOCK (all_inodes);
G_UNLOCK (domain_inodes);
/* By now we have no refs outstanding and no way to get at the inode, so free it */
g_clear_pointer (&inode->domain_root_inode, xdp_inode_unref);
g_clear_pointer (&inode->physical, xdp_physical_inode_unref);
xdp_domain_unref (inode->domain);
g_free (inode);
}
}
static XdpInode *
xdp_inode_kernel_ref (XdpInode *inode)
{
int old;
old = g_atomic_int_add (&inode->kernel_ref_count, 1);
if (old == 0)
xdp_inode_ref (inode);
return inode;
}
static void
xdp_inode_kernel_unref (XdpInode *inode, unsigned long count)
{
gint old_ref, new_ref;
retry_atomic_decrement1:
old_ref = g_atomic_int_get (&inode->kernel_ref_count);
if (old_ref < count)
{
g_warning ("Can't kernel_unref inode with no kernel refs");
return;
}
new_ref = old_ref - count;
if (!g_atomic_int_compare_and_exchange (&inode->kernel_ref_count, old_ref, new_ref))
goto retry_atomic_decrement1;
if (new_ref == 0)
xdp_inode_unref (inode);
}
static int
verify_doc_dir_devino (int dirfd, XdpDomain *doc_domain)
{
struct stat buf;
if (fstat (dirfd, &buf) != 0)
return -errno;
if (buf.st_ino != doc_domain->doc_dir_inode ||
buf.st_dev != doc_domain->doc_dir_device)
return -ENOENT;
return 0;
}
/* Only for toplevel dirs, not this is a bit weird for toplevel dir
inodes as it returns the dir itself which isn't really the dirfd
for that (nonphysical) inode */
static int
xdp_nonphysical_document_inode_opendir (XdpInode *inode)
{
XdpDomain *domain = inode->domain;
g_autofd int dirfd = -1;
int res;
g_assert (domain->type == XDP_DOMAIN_DOCUMENT);
g_assert (inode->physical == NULL);
dirfd = open (domain->doc_path, O_PATH | O_DIRECTORY);
if (dirfd < 0)
return -errno;
res = verify_doc_dir_devino (dirfd, domain);
if (res != 0)
return res;
return g_steal_fd (&dirfd);
}
static int
xdp_document_inode_ensure_dirfd (XdpInode *inode,
int *close_fd_out)
{
int close_fd;
g_assert (inode->domain->type == XDP_DOMAIN_DOCUMENT);
*close_fd_out = -1;
if (inode->physical)
return inode->physical->fd;
else
{
if (xdp_document_domain_is_dir (inode->domain))
{
/* There is no dirfd for the toplevel dirs, happens for example
if renaming into toplevel, so just return EPERM */
return -EPERM;
}
close_fd = xdp_nonphysical_document_inode_opendir (inode);
if (close_fd < 0)
return close_fd;
*close_fd_out = close_fd;
return close_fd;
}
}
static gboolean
open_flags_has_write (int open_flags)
{
return
(open_flags & O_ACCMODE) == O_WRONLY ||
(open_flags & O_ACCMODE) == O_RDWR ||
open_flags & O_TRUNC;
}
static void
gen_temp_name (gchar *tmpl)
{
g_return_if_fail (tmpl != NULL);
const size_t len = strlen (tmpl);
g_return_if_fail (len >= 6);
static const char letters[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
static const int NLETTERS = sizeof (letters) - 1;
char *XXXXXX = tmpl + (len - 6);
for (int i = 0; i < 6; i++)
XXXXXX[i] = letters[g_random_int_range(0, NLETTERS)];
}
static int
open_temp_at (int dirfd,
const char *orig_name,
char **name_out,
mode_t mode)
{
g_autofree char *tmp = g_strconcat (".xdp-", orig_name, "-XXXXXX", NULL);
const size_t count_max = 100;
int errsv;
int fd;
for (size_t count = 0; count < count_max; count++)
{
gen_temp_name (tmp);
fd = openat (dirfd, tmp, O_CREAT | O_EXCL | O_NOFOLLOW | O_NOCTTY | O_RDWR, mode);
errsv = errno;
if (fd < 0)
{
if (errsv == EEXIST)
continue;
else
return -errsv;
}
*name_out = g_steal_pointer (&tmp);
return fd;
}
return -EEXIST;
}
/* allocates tempfile for existing file,
Called with tempfile lock held, sets errno */
static int
get_tempfile_for (XdpInode *parent,
XdpDomain *domain,
const char *name,
int dirfd,
const char *tmpname,
XdpTempfile **tempfile_out)
{
g_autoptr(XdpTempfile) tempfile = NULL;
g_autoptr(XdpInode) inode = NULL;
g_autofd int o_path_fd = -1;
int res;
if (tempfile_out != NULL)
*tempfile_out = NULL;
o_path_fd = openat (dirfd, tmpname, O_PATH, 0);
if (o_path_fd == -1)
return -errno;
res = ensure_docdir_inode (parent, g_steal_fd (&o_path_fd), NULL, &inode); /* passed ownership of o_path_fd */
if (res != 0)
return res;
tempfile = xdp_tempfile_new (inode, name, tmpname);
/* This is atomic, because we're called with the lock held */
g_hash_table_replace (domain->tempfiles, tempfile->name, xdp_tempfile_ref (tempfile));
queue_invalidate_dentry (parent, tempfile->name);
if (tempfile_out)
*tempfile_out = g_steal_pointer (&tempfile);
return 0;
}
/* Creates a new file on disk,
Called with tempfile lock held, sets errno */
static int
create_tempfile (XdpInode *parent,
XdpDomain *domain,
const char *name,
int dirfd,
mode_t mode,
XdpTempfile **tempfile_out)
{
g_autoptr(XdpInode) inode = NULL;
g_autofree char *real_fd_path = NULL;
g_autofd int real_fd = -1;
g_autofd int o_path_fd = -1;
g_autoptr(XdpTempfile) tempfile = NULL;
g_autofree char *tmpname = NULL;
int res;
if (tempfile_out != NULL)
*tempfile_out = NULL;
real_fd = open_temp_at (dirfd, name, &tmpname, mode);
if (real_fd < 0)
return real_fd;
real_fd_path = fd_to_path (real_fd);
o_path_fd = open (real_fd_path, O_PATH, 0);
if (o_path_fd == -1)
return -errno;
/* We can close the tmpfd early */
close (g_steal_fd (&real_fd));
res = ensure_docdir_inode (parent, g_steal_fd (&o_path_fd), NULL, &inode); /* passed ownership of o_path_fd */
if (res != 0)
return res;
tempfile = xdp_tempfile_new (inode, name, tmpname);
/* This is atomic, because we're called with the lock held */
g_hash_table_replace (domain->tempfiles, tempfile->name, xdp_tempfile_ref (tempfile));
queue_invalidate_dentry (parent, tempfile->name);
if (tempfile_out)
*tempfile_out = g_steal_pointer (&tempfile);
return 0;
}
static int
xdp_document_inode_open_child_fd (XdpInode *inode,
const char *name,
int open_flags,
mode_t mode)
{
XdpDomain *domain = inode->domain;
XdpTempfile *tempfile_lookup = NULL;
g_autoptr(XdpTempfile) tempfile = NULL;
int tempfile_res = 0;
g_autofd int fd = -1;
g_assert (domain->type == XDP_DOMAIN_DOCUMENT);
if (!xdp_document_domain_can_write (domain) &&
(open_flags_has_write (open_flags) ||
(open_flags & O_CREAT) != 0))
return -EACCES;
if (inode->physical)
{
fd = openat (inode->physical->fd, name, open_flags, mode);
if (fd == -1)
return -errno;
return g_steal_fd (&fd);
}
else
{
g_autofd int dirfd = -1;
if (xdp_document_domain_is_dir (domain))
{
if (strcmp (name, domain->doc_file) == 0)
{
/* Ensure toplevel dir exist and is right */
dirfd = xdp_nonphysical_document_inode_opendir (inode);
if (dirfd < 0)
return dirfd;
fd = openat (dirfd, ".", open_flags, mode);
if (fd == -1)
return -errno;
return g_steal_fd (&fd);
}
}
else
{
/* Ensure parent dir exist and is right */
dirfd = xdp_nonphysical_document_inode_opendir (inode);
if (dirfd < 0)
return dirfd;
if (strcmp (name, domain->doc_file) == 0)
{
fd = openat (dirfd, name, open_flags, mode);
if (fd == -1)
return -errno;
return g_steal_fd (&fd);
}
/* Not main file, maybe a temporary file? */
g_mutex_lock (&domain->tempfile_mutex);
tempfile_lookup = g_hash_table_lookup (domain->tempfiles, name);
if (tempfile_lookup)
{
if ((open_flags & O_CREAT) && (open_flags & O_EXCL))
tempfile_res = -EEXIST;
else
tempfile = xdp_tempfile_ref (tempfile_lookup);
}
else if (open_flags & O_CREAT)
{
tempfile_res = create_tempfile (inode, domain, name, dirfd, mode, &tempfile);
}
g_mutex_unlock (&domain->tempfile_mutex);
if (tempfile)
{
g_autofree char *fd_path = fd_to_path (tempfile->inode->physical->fd);
fd = open (fd_path, open_flags & ~(O_CREAT|O_EXCL|O_NOFOLLOW), mode);
if (fd == -1)
return -errno;
return g_steal_fd (&fd);
}
else
{
if (tempfile_res != 0)
return tempfile_res;
}
}
}
return -ENOENT;
}
/* Returns /proc/self/fds/$fd path for O_PATH fd or toplevel path */
static char *
xdp_document_inode_get_self_as_path (XdpInode *inode)
{
g_assert (inode->domain->type == XDP_DOMAIN_DOCUMENT);
if (inode->physical)
return fd_to_path (inode->physical->fd);
else
{
if (xdp_document_domain_is_dir (inode->domain))
return NULL;
return g_strdup (inode->domain->doc_path);
}
}
static void
tweak_statbuf_for_document_inode (XdpInode *inode,
struct stat *buf)
{
XdpDomain *domain = inode->domain;
g_assert (domain->type == XDP_DOMAIN_DOCUMENT);
buf->st_ino = xdp_inode_to_ino (inode);
/* Remove setuid/setgid/sticky flags */
buf->st_mode &= ~(S_ISUID|S_ISGID|S_ISVTX);
if (!xdp_document_domain_can_write (domain))
buf->st_mode &= ~(0222);
}
static void
xdp_reply_err (const char *op,
fuse_req_t req,
int err)
{
if (err != 0)
{
const char *errname = NULL;
switch (err)
{
case ESTALE:
errname = "ESTALE";
break;
case EEXIST:
errname = "EEXIST";
break;
case ENOENT:
errname = "ENOENT";
break;
case EPERM:
errname = "EPERM";
break;
case EACCES:
errname = "EACCES";
break;
case EINVAL:
errname = "EINVAL";
break;
default:
errname = NULL;
}
if (errname != NULL)
g_debug ("%s -> error %s", op, errname);
else
g_debug ("%s -> error %d", op, err);
}
fuse_reply_err (req, err);
}
static inline void
xdp_reply_ok (const char *op,
fuse_req_t req)
{
xdp_reply_err (op, req, 0);
}
typedef enum {
CHECK_CAN_WRITE = 1 << 0,
CHECK_IS_DIRECTORY = 1 << 1,
CHECK_IS_PHYSICAL = 1 << 2,
CHECK_IS_PHYSICAL_IF_DIR = 1 << 3,
} XdpDocumentChecks;
static gboolean
xdp_document_inode_checks (const char *op,
fuse_req_t req,
XdpInode *inode,
XdpDocumentChecks checks)
{
XdpDomain *domain = inode->domain;
gboolean check_is_directory = (checks & CHECK_IS_DIRECTORY) != 0;
gboolean check_can_write = (checks & CHECK_CAN_WRITE) != 0;
gboolean check_is_physical = (checks & CHECK_IS_PHYSICAL) != 0;
gboolean check_is_physical_if_dir = (checks & CHECK_IS_PHYSICAL_IF_DIR) != 0;
if (domain->type != XDP_DOMAIN_DOCUMENT)
{
xdp_reply_err (op, req, EPERM);
return FALSE;
}
/* We allowed the inode lookup to succeed, but maybe the permissions changed since then */
if (!xdp_document_domain_can_see (domain))
{
xdp_reply_err (op, req, EACCES);
return FALSE;
}
if (check_is_directory && !xdp_document_domain_is_dir (domain))
{
xdp_reply_err (op, req, EPERM);
return FALSE;
}
if (check_can_write && !xdp_document_domain_can_write (domain))
{
xdp_reply_err (op, req, EACCES);
return FALSE;
}
if (check_is_physical_if_dir && xdp_document_domain_is_dir (domain))
check_is_physical = TRUE;
if (check_is_physical && inode->physical == NULL)
{
xdp_reply_err (op, req, EPERM);
return FALSE;
}
return TRUE;
}
static void
stat_virtual_inode (XdpInode *inode,
struct stat *buf)
{
memset (buf, 0, sizeof (struct stat));
buf->st_ino = xdp_inode_to_ino (inode);
buf->st_uid = my_uid;
buf->st_gid = my_gid;
switch (inode->domain->type)
{
case XDP_DOMAIN_ROOT:
case XDP_DOMAIN_BY_APP:
case XDP_DOMAIN_APP:
buf->st_mode = S_IFDIR | NON_DOC_DIR_PERMS;
buf->st_nlink = 2;
break;
case XDP_DOMAIN_DOCUMENT:
if (xdp_document_domain_is_dir (inode->domain))
buf->st_mode = S_IFDIR | DOC_DIR_PERMS_DIR;
else
buf->st_mode = S_IFDIR | DOC_DIR_PERMS_FILE;
buf->st_nlink = 2;
/* Remove perms if not writable */
if (inode->domain->app_id != NULL)
{
g_autoptr(PermissionDbEntry) entry = xdp_lookup_doc (inode->domain->doc_id);
if (entry == NULL || !app_can_write_doc (entry, inode->domain->app_id))
buf->st_mode &= ~(0222);
}
break;
default:
g_assert_not_reached ();
break;
}
}
static void
xdp_fuse_getattr (fuse_req_t req,
fuse_ino_t ino,
struct fuse_file_info *fi)
{
g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
XdpDomain *domain = inode->domain;
double attr_valid_time = 0.0;/* Time in secs for attribute validation */
struct stat buf;
int res;
const char *op = "GETATTR";
g_debug ("GETATTR %" G_GINT64_MODIFIER "x", ino);
if (xdp_domain_is_virtual_type (domain))
{
stat_virtual_inode (inode, &buf);
fuse_reply_attr (req, &buf, attr_valid_time);
return;
}
g_assert (domain->type == XDP_DOMAIN_DOCUMENT);
if (inode->physical)
{
res = fstatat (inode->physical->fd, "", &buf, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW);
}
else
{
stat_virtual_inode (inode, &buf);
res = 0;
}
if (res == -1)
return xdp_reply_err (op, req, errno);
tweak_statbuf_for_document_inode (inode, &buf);
fuse_reply_attr (req, &buf, attr_valid_time);
}
static void
xdp_fuse_setattr (fuse_req_t req,
fuse_ino_t ino,
struct stat *attr,
int to_set,
struct fuse_file_info *fi)
{
g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
g_autofree char *to_set_string = setattr_flags_to_string (to_set);
struct stat buf;
double attr_valid_time = 0.0;/* Time in secs for attribute validation */
int res;
const char *op = "SETATTR";
g_debug ("SETATTR %" G_GINT64_MODIFIER "x %s", ino, to_set_string);
if (!xdp_document_inode_checks (op, req, inode,
CHECK_CAN_WRITE | CHECK_IS_PHYSICAL))
return;
/* Truncate */
if (to_set & FUSE_SET_ATTR_SIZE)
{
g_autofree char *path = NULL;
XdpFile *file = (XdpFile *)fi->fh;
if (file)
{
res = ftruncate (file->fd, attr->st_size);
if (res == -1)
res = -errno;
}
else if (inode->physical)
{
path = fd_to_path (inode->physical->fd);
res = truncate (path, attr->st_size);
if (res == -1)
res = -errno;
}
else
{
res = -EISDIR;
}
if (res != 0)
return xdp_reply_err (op, req, -res);
}
if (to_set & (FUSE_SET_ATTR_ATIME | FUSE_SET_ATTR_MTIME))
{
struct timespec times[2] = { {0, UTIME_OMIT}, {0, UTIME_OMIT} }; /* 0 = atime, 1 = mtime */
g_autofree char *path = NULL;
if (to_set & FUSE_SET_ATTR_ATIME_NOW)
times[0].tv_nsec = UTIME_NOW;
else if (to_set & FUSE_SET_ATTR_ATIME)
times[0] = attr->st_atim;
if (to_set & FUSE_SET_ATTR_MTIME_NOW)
times[1].tv_nsec = UTIME_NOW;
else if (to_set & FUSE_SET_ATTR_MTIME)
times[1] = attr->st_mtim;
if (inode->physical)
{
path = fd_to_path (inode->physical->fd);
res = utimensat (AT_FDCWD, path, times, 0);
}
else
res = utimensat (AT_FDCWD, inode->domain->doc_path, times, 0); /* follow symlink here */
if (res != 0)
return xdp_reply_err (op, req, errno);
}
if (to_set & (FUSE_SET_ATTR_UID | FUSE_SET_ATTR_GID))
{
g_autofree char *path = NULL;
uid_t uid = -1;
gid_t gid = -1;
if (to_set & FUSE_SET_ATTR_UID)
uid = attr->st_uid;
if (to_set & FUSE_SET_ATTR_GID)
gid = attr->st_gid;
if (inode->physical)
{
path = fd_to_path (inode->physical->fd);
res = chown (path, uid, gid);
if (res == -1)
res = -errno;
}
else
{
res = -EACCES;
}
if (res != 0)
return xdp_reply_err (op, req, -res);
}
if (to_set & (FUSE_SET_ATTR_MODE))
{
g_autofree char *path = NULL;
if (inode->physical)
{
path = fd_to_path (inode->physical->fd);
res = chmod (path, attr->st_mode);
if (res == -1)
res = -errno;
}
else
{
res = -EACCES;
}
if (res != 0)
return xdp_reply_err (op, req, -res);
}
if (inode->physical)
res = fstatat (inode->physical->fd, "", &buf, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW);
else
res = stat (inode->domain->doc_path, &buf); /* Follow symlinks here */
if (res != 0)
return xdp_reply_err (op, req, errno);
tweak_statbuf_for_document_inode (inode, &buf);
fuse_reply_attr (req, &buf, attr_valid_time);
}
static void
prepare_reply_entry (XdpInode *inode,
struct stat *buf,
struct fuse_entry_param *e)
{
xdp_inode_kernel_ref (inode); /* Ref given to the kernel, returned in xdp_forget() */
e->ino = xdp_inode_to_ino (inode);
e->generation = 1;
e->attr = *buf;
e->attr_timeout = 0.0; /* attribute timeout */
e->entry_timeout = 0.0; /* dentry timeout */
}
static void
prepare_reply_virtual_entry (XdpInode *inode,
struct fuse_entry_param *e)
{
stat_virtual_inode (inode, &e->attr);
xdp_inode_kernel_ref (inode); /* Ref given to the kernel, returned in xdp_forget() */
e->ino = xdp_inode_to_ino (inode);
e->generation = 1;
/* Cache virtual dirs */
e->attr_timeout = 60.0; /* attribute timeout */
e->entry_timeout = 60.0; /* dentry timeout */
}
static void
abort_reply_entry (struct fuse_entry_param *e)
{
XdpInode *inode = xdp_inode_from_ino (e->ino);
xdp_inode_kernel_unref (inode, 1);
}
static int
ensure_docdir_inode (XdpInode *parent,
int o_path_fd_in,
struct fuse_entry_param *e,
XdpInode **inode_out)
{
XdpDomain *domain = parent->domain;
g_autoptr(XdpPhysicalInode) physical = NULL;
g_autoptr(XdpInode) inode = NULL;
g_autofd int o_path_fd = -1;
struct stat buf;
int res;
/* Take ownership */
o_path_fd = g_steal_fd (&o_path_fd_in);
res = fstatat (o_path_fd, "", &buf, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW);
if (res == -1)
return -errno;
/* non-directory documents only support regular files */
if (!xdp_document_domain_is_dir (domain) && !S_ISREG(buf.st_mode))
return -ENOENT;
physical = ensure_physical_inode (buf.st_dev, buf.st_ino, g_steal_fd (&o_path_fd)); /* passed ownership of fd */
G_LOCK (domain_inodes);
inode = g_hash_table_lookup (domain->inodes, physical);
if (inode != NULL)
inode = xdp_inode_ref (inode);
else
{
inode = xdp_inode_new (domain, physical);
if (parent->domain_root_inode)
inode->domain_root_inode = xdp_inode_ref (parent->domain_root_inode);
else
inode->domain_root_inode = xdp_inode_ref (parent);
g_hash_table_insert (domain->inodes, physical, inode);
}
G_UNLOCK (domain_inodes);
if (e)
{
tweak_statbuf_for_document_inode (inode, &buf);
prepare_reply_entry (inode, &buf, e);
}
if (inode_out)
*inode_out = g_steal_pointer (&inode);
return 0;
}
static int
ensure_docdir_inode_by_name (XdpInode *parent,
int dirfd,
const char *name,
struct fuse_entry_param *e)
{
int o_path_fd;
o_path_fd = openat (dirfd, name, O_PATH|O_NOFOLLOW, 0);
if (o_path_fd == -1)
return -errno;
return ensure_docdir_inode (parent, o_path_fd, e, NULL); /* Takes ownership of o_path_fd */
}
static XdpInode *
ensure_by_app_inode (XdpInode *by_app_inode,
const char *app_id)
{
XdpDomain *by_app_domain = by_app_inode->domain;
g_autoptr(XdpInode) inode = NULL;
if (!xdp_is_valid_app_id (app_id))
return NULL;
G_LOCK (domain_inodes);
inode = g_hash_table_lookup (by_app_domain->inodes, app_id);
if (inode != NULL)
inode = xdp_inode_ref (inode);
else
{
g_autoptr(XdpDomain) app_domain = xdp_domain_new_app (by_app_inode, app_id);
inode = xdp_inode_new (app_domain, NULL);
g_hash_table_insert (by_app_domain->inodes, app_domain->app_id, inode);
}
G_UNLOCK (domain_inodes);
return g_steal_pointer (&inode);
}
static XdpInode *
ensure_doc_inode (XdpInode *parent,
const char *doc_id)
{
g_autoptr(PermissionDbEntry) doc_entry = NULL;
g_autoptr(XdpInode) inode = NULL;
XdpDomain *parent_domain = parent->domain;
doc_entry = xdp_lookup_doc (doc_id);
if (doc_entry == NULL ||
(parent_domain->app_id &&
!app_can_see_doc (doc_entry, parent_domain->app_id)))
return NULL;
G_LOCK (domain_inodes);
inode = g_hash_table_lookup (parent_domain->inodes, doc_id);
if (inode != NULL)
inode = xdp_inode_ref (inode);
else
{
g_autoptr(XdpDomain) doc_domain = xdp_domain_new_document (parent_domain, doc_id, doc_entry);
doc_domain->parent_inode = xdp_inode_ref (parent);
inode = xdp_inode_new (doc_domain, NULL);
g_hash_table_insert (parent_domain->inodes, doc_domain->doc_id, inode);
}
G_UNLOCK (domain_inodes);
return g_steal_pointer (&inode);
}
static gboolean
invalidate_dentry_cb (gpointer user_data)
{
GList *to_invalidate = NULL;
{
XDP_AUTOLOCK (invalidate_list);
to_invalidate = g_steal_pointer (&invalidate_list);
}
to_invalidate = g_list_reverse (to_invalidate);
XDP_AUTOLOCK (session);
for (GList *l = to_invalidate; l != NULL; l = l->next)
{
XdpInvalidateData *data = l->data;
if (session)
fuse_lowlevel_notify_inval_entry (session, data->parent_ino, data->name,
strlen (data->name));
g_free (data);
}
g_list_free (to_invalidate);
return FALSE;
}
/* Queue an inval_dentry, thereby freeing unused inodes in the dcache
* which will free up a bunch of O_PATH fds in the fuse implementation.
*/
static void
queue_invalidate_dentry (XdpInode *parent,
const char *name)
{
XDP_AUTOLOCK (invalidate_list);
for (GList *l = invalidate_list; l != NULL; l = l->next)
{
XdpInvalidateData *data = l->data;
if (data->parent_ino == parent->ino && strcmp (name, data->name) == 0)
return;
}
XdpInvalidateData *data = g_malloc0 (sizeof (XdpInvalidateData) + strlen (name) + 1);
data->parent_ino = parent->ino;
strcpy (data->name, name);
if (invalidate_list == NULL)
g_timeout_add (10, invalidate_dentry_cb, NULL);
invalidate_list = g_list_append (invalidate_list, data);
}
static void
xdp_fuse_lookup (fuse_req_t req,
fuse_ino_t parent_ino,
const char *name)
{
g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino);
XdpDomain *parent_domain = parent->domain;
g_autoptr(XdpInode) inode = NULL;
struct fuse_entry_param e;
int res, fd;
int open_flags = O_PATH|O_NOFOLLOW;
const char *op = "LOOKUP";
g_debug ("LOOKUP %" G_GINT64_MODIFIER "x:%s", parent_ino, name);
if (strcmp (name, ".") == 0 || strcmp (name, "..") == 0)
{
/* We don't set FUSE_CAP_EXPORT_SUPPORT, so should not get
* here. But lets make sure we never ever resolve them as that
* could be a security issue by escaping the root. */
return xdp_reply_err (op, req, ESTALE);
}
if (xdp_domain_is_virtual_type (parent_domain))
{
switch (parent_domain->type)
{
case XDP_DOMAIN_ROOT:
if (strcmp (name, BY_APP_NAME) == 0)
inode = xdp_inode_ref (by_app_inode);
else
inode = ensure_doc_inode (parent, name);
break;
case XDP_DOMAIN_BY_APP:
inode = ensure_by_app_inode (parent, name);
break;
case XDP_DOMAIN_APP:
inode = ensure_doc_inode (parent, name);
break;
default:
g_assert_not_reached ();
}
if (inode == NULL)
return xdp_reply_err (op, req, ENOENT);
prepare_reply_virtual_entry (inode, &e);
}
else
{
g_assert (parent_domain->type == XDP_DOMAIN_DOCUMENT);
fd = xdp_document_inode_open_child_fd (parent, name, open_flags, 0);
if (fd < 0)
return xdp_reply_err (op, req, -fd);
res = ensure_docdir_inode (parent, fd, &e, NULL); /* Takes ownership of fd */
if (res != 0)
return xdp_reply_err (op, req, -res);
queue_invalidate_dentry (parent, name);
}
g_debug ("LOOKUP %" G_GINT64_MODIFIER "x:%s => %" G_GINT64_MODIFIER "x", parent_ino, name, e.ino);
if (fuse_reply_entry (req, &e) == -ENOENT)
abort_reply_entry (&e);
}
static XdpFile *
xdp_file_new (int fd)
{
XdpFile *file = g_new0 (XdpFile, 1);
file->fd = fd;
XDP_AUTOLOCK (open_files);
open_files = g_list_prepend (open_files, file);
file->link = open_files;
return file;
}
static void
xdp_file_free (XdpFile *file)
{
GList *link = g_steal_pointer (&file->link);
XDP_AUTOLOCK (open_files);
open_files = g_list_delete_link (open_files, link);
close (file->fd);
g_free (file);
}
static void
xdp_fuse_open (fuse_req_t req,
fuse_ino_t ino,
struct fuse_file_info *fi)
{
g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
int open_flags = fi->flags;
g_autofree char *open_flags_string = open_flags_to_string (open_flags);
int fd;
g_autofree char *path = NULL;
XdpFile *file = NULL;
XdpDocumentChecks checks;
const char *op = "OPEN";
g_debug ("OPEN %" G_GINT64_MODIFIER "x %s", ino, open_flags_string);
checks = CHECK_IS_PHYSICAL;
if (open_flags_has_write (open_flags))
checks |= CHECK_CAN_WRITE;
/* Note: open_flags guaranteed to exclude O_CREAT, O_EXCL */
if (!xdp_document_inode_checks (op, req, inode, checks))
return;
path = fd_to_path (inode->physical->fd);
/*
* `path` is a path to the fd entry in `/proc`, which is a symlink
* to the actual file. Opening it directly with `O_NOFOLLOW` will
* fail. So we should resolve it first then we can honour the no
* follow flag.
*/
if (open_flags & O_NOFOLLOW)
{
char resolved_path[PATH_MAX] = { 0, };
ssize_t res;
res = readlink (path, resolved_path, sizeof (resolved_path));
if (res == sizeof (resolved_path))
return xdp_reply_err (op, req, ENAMETOOLONG);
if (res < 0)
return xdp_reply_err (op, req, errno);
g_clear_pointer (&path, g_free);
path = g_strdup (resolved_path);
}
fd = open (path, open_flags, 0);
if (fd == -1)
return xdp_reply_err (op, req, errno);
file = xdp_file_new (fd);
fi->fh = (gsize)file;
if (fuse_reply_open (req, fi) == -ENOENT)
{
/* The open syscall was interrupted, so it must be cancelled */
xdp_file_free (file);
}
}
static void
xdp_fuse_create (fuse_req_t req,
fuse_ino_t parent_ino,
const char *filename,
mode_t mode,
struct fuse_file_info *fi)
{
g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino);
int open_flags = fi->flags;
g_autofree char *open_flags_string = open_flags_to_string (open_flags);
struct fuse_entry_param e;
int res;
g_autofd int fd = -1;
g_autofd int o_path_fd = -1;
g_autofree char *fd_path = NULL;
XdpFile *file = NULL;
const char *op = "CREATE";
g_debug ("CREATE %" G_GINT64_MODIFIER "x %s %s, 0%o", parent_ino, filename, open_flags_string, mode);
if (!xdp_document_inode_checks (op, req, parent,
CHECK_CAN_WRITE |
CHECK_IS_PHYSICAL_IF_DIR))
return;
fd = xdp_document_inode_open_child_fd (parent, filename, open_flags, mode);
if (fd < 0)
return xdp_reply_err (op, req, -fd);
fd_path = fd_to_path (fd);
o_path_fd = open (fd_path, O_PATH, 0);
if (o_path_fd < 0)
return xdp_reply_err (op, req, errno);
res = ensure_docdir_inode (parent, g_steal_fd (&o_path_fd), &e, NULL); /* Takes ownership of o_path_fd */
if (res != 0)
return xdp_reply_err (op, req, -res);
file = xdp_file_new (g_steal_fd (&fd)); /* Takes ownership of fd */
fi->fh = (gsize)file;
if (fuse_reply_create (req, &e, fi) == -ENOENT)
{
/* The open syscall was interrupted, so it must be cancelled */
xdp_file_free (file);
abort_reply_entry (&e);
}
queue_invalidate_dentry (parent, filename);
}
static void
xdp_fuse_read (fuse_req_t req,
fuse_ino_t ino,
size_t size,
off_t off,
struct fuse_file_info *fi)
{
struct fuse_bufvec buf = FUSE_BUFVEC_INIT(size);
XdpFile *file = (XdpFile *)fi->fh;
enum fuse_buf_copy_flags reply_flags = FUSE_BUF_SPLICE_MOVE;
XdpFuseOptions *fuse_opts = fuse_req_userdata (req);
g_debug ("READ %" G_GINT64_MODIFIER "x size %" G_GSIZE_FORMAT " off %" G_GOFFSET_FORMAT, ino, size, (goffset)off);
buf.buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK;
buf.buf[0].fd = file->fd;
buf.buf[0].pos = off;
if (!fuse_opts->use_splice)
reply_flags = FUSE_BUF_NO_SPLICE;
fuse_reply_data (req, &buf, reply_flags);
}
static void
xdp_fuse_write (fuse_req_t req,
fuse_ino_t ino,
const char *buf,
size_t size,
off_t off,
struct fuse_file_info *fi)
{
XdpFile *file = (XdpFile *)fi->fh;
ssize_t res;
const char *op = "WRITE";
g_debug ("WRITE %" G_GINT64_MODIFIER "x size %" G_GSIZE_FORMAT " off %" G_GOFFSET_FORMAT, ino, size, (goffset)off);
res = pwrite (file->fd, buf, size, off);
if (res >= 0)
fuse_reply_write (req, res);
else
xdp_reply_err (op, req, errno);
}
static void
xdp_fuse_write_buf (fuse_req_t req,
fuse_ino_t ino,
struct fuse_bufvec *bufv,
off_t off,
struct fuse_file_info *fi)
{
XdpFile *file = (XdpFile *)fi->fh;
struct fuse_bufvec dst = FUSE_BUFVEC_INIT(fuse_buf_size(bufv));
ssize_t res;
const char *op = "WRITEBUF";
enum fuse_buf_copy_flags copy_flags = FUSE_BUF_SPLICE_NONBLOCK;
XdpFuseOptions *fuse_opts = fuse_req_userdata (req);
g_debug ("WRITEBUF %" G_GINT64_MODIFIER "x off %" G_GOFFSET_FORMAT, ino, (goffset)off);
dst.buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK;
dst.buf[0].fd = file->fd;
dst.buf[0].pos = off;
if (!fuse_opts->use_splice)
copy_flags = FUSE_BUF_NO_SPLICE;
res = fuse_buf_copy (&dst, bufv, copy_flags);
if (res >= 0)
fuse_reply_write (req, res);
else
xdp_reply_err (op, req, errno);
}
static void
xdp_fuse_fsync (fuse_req_t req,
fuse_ino_t ino,
int datasync,
struct fuse_file_info *fi)
{
XdpFile *file = (XdpFile *)fi->fh;
int res;
const char *op = "FSYNC";
g_debug ("FSYNC %" G_GINT64_MODIFIER "x", ino);
if (datasync)
res = fdatasync (file->fd);
else
res = fsync (file->fd);
if (res == 0)
xdp_reply_ok (op, req);
else
xdp_reply_err (op, req, errno);
}
static void
xdp_fuse_fallocate (fuse_req_t req,
fuse_ino_t ino,
int mode,
off_t offset,
off_t length,
struct fuse_file_info *fi)
{
XdpFile *file = (XdpFile *)fi->fh;
int res;
const char *op = "FALLOCATE";
g_debug ("FALLOCATE %" G_GINT64_MODIFIER "x", ino);
#ifdef __linux__
res = fallocate (file->fd, mode, offset, length);
#else
res = posix_fallocate (file->fd, offset, length);
#endif
if (res == 0)
xdp_reply_ok (op, req);
else
xdp_reply_err (op, req, errno);
}
static void
xdp_fuse_flush (fuse_req_t req,
fuse_ino_t ino,
struct fuse_file_info *fi)
{
const char *op = "FLUSH";
g_debug ("FLUSH %" G_GINT64_MODIFIER "x", ino);
xdp_reply_ok (op, req);
}
static void
xdp_fuse_release (fuse_req_t req,
fuse_ino_t ino,
struct fuse_file_info *fi)
{
XdpFile *file = (XdpFile *)fi->fh;
const char *op = "RELEASE";
g_debug ("RELEASE %" G_GINT64_MODIFIER "x", ino);
xdp_file_free (file);
xdp_reply_ok (op, req);
}
static void
forget_one (fuse_ino_t ino,
unsigned long nlookup)
{
g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
g_debug ("FORGET %" G_GINT64_MODIFIER "x %ld", ino, nlookup);
xdp_inode_kernel_unref (inode, nlookup);
}
static void
xdp_fuse_forget (fuse_req_t req,
fuse_ino_t ino,
uint64_t nlookup)
{
forget_one (ino, nlookup);
fuse_reply_none (req);
}
static void
xdp_fuse_forget_multi (fuse_req_t req,
size_t count,
struct fuse_forget_data *forgets)
{
size_t i;
g_debug ("FORGET_MULTI %" G_GSIZE_FORMAT, count);
for (i = 0; i < count; i++)
forget_one (forgets[i].ino, forgets[i].nlookup);
fuse_reply_none (req);
}
static void
xdp_dir_free (XdpDir *d)
{
if (d->dir)
closedir (d->dir);
g_free (d->dirbuf);
g_free (d);
}
static void
xdp_dir_add (XdpDir *d,
fuse_req_t req,
const char *name,
mode_t mode)
{
struct stat stbuf;
size_t oldsize = d->dirbuf_size;
d->dirbuf_size += fuse_add_direntry (req, NULL, 0, name, NULL, 0);
d->dirbuf = (char *) g_realloc (d->dirbuf, d->dirbuf_size);
memset (&stbuf, 0, sizeof (stbuf));
stbuf.st_ino = FUSE_UNKNOWN_INO;
stbuf.st_mode = mode;
fuse_add_direntry (req, d->dirbuf + oldsize,
d->dirbuf_size - oldsize,
name, &stbuf,
d->dirbuf_size);
}
static XdpDir *
xdp_dir_new_physical (DIR *dir)
{
XdpDir *d = g_new0 (XdpDir, 1);
d->dir = dir;
d->offset = 0;
d->entry = NULL;
return d;
}
static XdpDir *
xdp_dir_new_buffered (fuse_req_t req)
{
XdpDir *d = g_new0 (XdpDir, 1);
xdp_dir_add (d, req, ".", S_IFDIR);
xdp_dir_add (d, req, "..", S_IFDIR);
return d;
}
static void
xdp_dir_add_docs (XdpDir *d,
fuse_req_t req,
const char *for_app_id)
{
g_auto(GStrv) docs = NULL;
int i;
docs = xdp_list_docs ();
for (i = 0; docs[i] != NULL; i++)
{
if (for_app_id)
{
g_autoptr(PermissionDbEntry) entry = xdp_lookup_doc (docs[i]);
if (entry == NULL ||
!app_can_see_doc (entry, for_app_id))
continue;
}
xdp_dir_add (d, req, docs[i], S_IFDIR);
}
}
static void
xdp_dir_add_apps (XdpDir *d,
XdpDomain *domain,
fuse_req_t req,
const char *for_app_id)
{
g_auto(GStrv) apps = NULL;
g_auto(GStrv) names = NULL;
int i;
/* First all pre-used apps as these can be created on demand */
names = xdp_domain_get_inode_keys_as_string (domain);
for (i = 0; names[i] != NULL; i++)
xdp_dir_add (d, req, names[i], S_IFDIR);
/* Then all in the db (that don't already have inodes) */
apps = xdp_list_apps ();
for (i = 0; apps[i] != NULL; i++)
{
const char *app = apps[i];
if (!g_strv_contains ((const gchar * const *)names, app))
xdp_dir_add (d, req, app, S_IFDIR);
}
}
static void
xdp_fuse_opendir (fuse_req_t req,
fuse_ino_t ino,
struct fuse_file_info *fi)
{
g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
XdpDomain *domain = inode->domain;
XdpDir *d = NULL;
const char *op = "OPENDIR";
g_debug ("OPENDIR %" G_GINT64_MODIFIER "x domain %d", ino, inode->domain->type);
if (xdp_domain_is_virtual_type (domain))
{
d = xdp_dir_new_buffered (req);
switch (domain->type)
{
case XDP_DOMAIN_ROOT:
xdp_dir_add (d, req, BY_APP_NAME, S_IFDIR);
xdp_dir_add_docs (d, req, NULL);
break;
case XDP_DOMAIN_APP:
xdp_dir_add_docs (d, req, domain->app_id);
break;
case XDP_DOMAIN_BY_APP:
xdp_dir_add_apps (d, inode->domain, req, NULL);
break;
default:
g_assert_not_reached ();
}
}
else
{
g_assert (domain->type == XDP_DOMAIN_DOCUMENT);
if (xdp_document_domain_is_dir (domain))
{
if (inode->physical)
{
DIR *dir;
int fd;
fd = openat (inode->physical->fd, ".", O_RDONLY | O_DIRECTORY, 0);
if (fd < 0)
return xdp_reply_err (op, req, errno);
dir = fdopendir (fd);
if (dir == NULL)
{
xdp_reply_err (op, req, errno);
close (fd);
return;
}
d = xdp_dir_new_physical (dir);
}
else /* Nonphysical, i.e. toplevel */
{
struct stat buf;
d = xdp_dir_new_buffered (req);
if (stat (domain->doc_path, &buf) == 0 &&
buf.st_ino == domain->doc_dir_inode &&
buf.st_dev == domain->doc_dir_device)
{
xdp_dir_add (d, req, domain->doc_file, buf.st_mode);
}
}
}
else
{
g_autofree char *main_path = g_build_filename (domain->doc_path, domain->doc_file, NULL);
struct stat buf;
GHashTableIter iter;
gpointer key, value;
d = xdp_dir_new_buffered (req);
if (stat (main_path, &buf) == 0)
xdp_dir_add (d, req, domain->doc_file, buf.st_mode);
g_mutex_lock (&domain->tempfile_mutex);
g_hash_table_iter_init (&iter, domain->tempfiles);
while (g_hash_table_iter_next (&iter, &key, &value))
{
const char *tempname = key;
xdp_dir_add (d, req, tempname, S_IFREG);
}
g_mutex_unlock (&domain->tempfile_mutex);
}
}
fi->fh = (gsize)d;
if (fuse_reply_open (req, fi) == -ENOENT)
{
/* The opendir syscall was interrupted, so it must be cancelled */
xdp_dir_free (d);
}
}
static void
xdp_fuse_readdir (fuse_req_t req,
fuse_ino_t ino,
size_t size,
off_t off,
struct fuse_file_info *fi)
{
XdpDir *d = (XdpDir *)fi->fh;
const char *op = "READDIR";
g_debug ("READDIR %" G_GINT64_MODIFIER "x %" G_GSIZE_FORMAT " %" G_GOFFSET_FORMAT, ino, size, (goffset)off);
if (d->dir)
{
g_autofree char *buf = g_try_malloc (size);
size_t rem;
char *p;
if (buf == NULL)
{
xdp_reply_err (op, req, ENOMEM);
return;
}
/* If offset is not same, need to seek it */
if (off != d->offset)
{
seekdir (d->dir, off);
d->entry = NULL;
d->offset = off;
}
p = buf;
rem = size;
while (TRUE)
{
size_t entsize;
off_t nextoff;
if (!d->entry)
{
errno = 0;
d->entry = readdir (d->dir);
if (!d->entry)
{
if (errno && rem == size)
{
xdp_reply_err (op, req, errno);
return;
}
break;
}
}
nextoff = telldir (d->dir);
struct stat st = {
.st_ino = FUSE_UNKNOWN_INO,
.st_mode = d->entry->d_type << 12,
};
entsize = fuse_add_direntry (req, p, rem,
d->entry->d_name, &st, nextoff);
/* The above function returns the size of the entry size even though
* the copy failed due to smaller buf size, so I'm checking after this
* function and breaking out in case we exceed the size.
*/
if (entsize > rem)
break;
p += entsize;
rem -= entsize;
d->entry = NULL;
d->offset = nextoff;
}
fuse_reply_buf(req, buf, size - rem);
}
else
{
if (off < d->dirbuf_size)
{
gsize reply_size = MIN (d->dirbuf_size - off, size);
g_autofree char *buf = g_memdup2 (d->dirbuf + off, reply_size);
fuse_reply_buf (req, buf, reply_size);
}
else
{
fuse_reply_buf (req, NULL, 0);
}
}
}
static void
xdp_fuse_releasedir (fuse_req_t req,
fuse_ino_t ino,
struct fuse_file_info *fi)
{
XdpDir *d = (XdpDir *)fi->fh;
const char *op = "RELEASEDIR";
g_debug ("RELEASEDIR %" G_GINT64_MODIFIER "x", ino);
xdp_dir_free (d);
xdp_reply_ok (op, req);
}
static void
xdp_fuse_fsyncdir (fuse_req_t req,
fuse_ino_t ino,
int datasync,
struct fuse_file_info *fi)
{
XdpDir *dir = (XdpDir *)fi->fh;
int fd, res;
const char *op = "FSYNCDIR";
g_debug ("FSYNCDIR %" G_GINT64_MODIFIER "x", ino);
if (dir->dir)
{
fd = dirfd (dir->dir);
if (datasync)
res = fdatasync (fd);
else
res = fsync (fd);
}
else
res = 0;
if (res == 0)
xdp_reply_ok (op, req);
else
xdp_reply_err (op, req, errno);
}
static void
xdp_fuse_mkdir (fuse_req_t req,
fuse_ino_t parent_ino,
const char *name,
mode_t mode)
{
g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino);
struct fuse_entry_param e;
int res;
g_autofd int close_fd = -1;
int dirfd;
const char *op = "MKDIR";
g_debug ("MKDIR %" G_GINT64_MODIFIER "x %s", parent_ino, name);
if (!xdp_document_inode_checks (op, req, parent,
CHECK_CAN_WRITE |
CHECK_IS_DIRECTORY |
CHECK_IS_PHYSICAL))
return;
dirfd = xdp_document_inode_ensure_dirfd (parent, &close_fd);
if (dirfd < 0)
return xdp_reply_err (op, req, -dirfd);
res = mkdirat (dirfd, name, mode);
if (res != 0)
return xdp_reply_err (op, req, errno);
res = ensure_docdir_inode_by_name (parent, dirfd, name, &e); /* Takes ownership of o_path_fd */
if (res != 0)
return xdp_reply_err (op, req, -res);
if (fuse_reply_entry (req, &e) == -ENOENT)
abort_reply_entry (&e);
}
static void
xdp_fuse_unlink (fuse_req_t req,
fuse_ino_t parent_ino,
const char *filename)
{
g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino);
XdpDomain *parent_domain = parent->domain;
int res = -1;
const char * op = "UNLINK";
g_debug ("UNLINK %" G_GINT64_MODIFIER "x %s", parent_ino, filename);
if (!xdp_document_inode_checks (op, req, parent,
CHECK_CAN_WRITE |
CHECK_IS_PHYSICAL_IF_DIR))
return;
if (parent->physical)
{
res = unlinkat (parent->physical->fd, filename, 0);
if (res != 0)
return xdp_reply_err (op, req, errno);
}
else
{
g_autofd int dirfd = -1;
/* Only reached for non-directory inodes */
dirfd = xdp_nonphysical_document_inode_opendir (parent);
if (dirfd < 0)
xdp_reply_err (op, req, -dirfd);
if (strcmp (filename, parent_domain->doc_file) == 0)
{
res = unlinkat (dirfd, filename, 0);
if (res != 0)
return xdp_reply_err (op, req, errno);
}
else
{
gboolean removed = FALSE;
/* Not directory and not main file, maybe a temporary file? */
g_mutex_lock (&parent_domain->tempfile_mutex);
removed = g_hash_table_remove (parent_domain->tempfiles, filename);
g_mutex_unlock (&parent_domain->tempfile_mutex);
if (!removed)
return xdp_reply_err (op, req, ENOENT);
}
}
xdp_reply_ok (op, req);
}
static int
try_renameat (int olddirfd,
const char *oldpath,
int newdirfd,
const char *newpath,
unsigned int flags)
{
#if HAVE_RENAMEAT2
return renameat2 (olddirfd, oldpath, newdirfd, newpath, flags);
#else
if (flags)
{
g_warning ("renameat2 is not supported by this system and rename flags are set");
errno = EINVAL;
return -1;
}
return renameat (olddirfd, oldpath, newdirfd, newpath);
#endif
}
static void
xdp_fuse_rename (fuse_req_t req,
fuse_ino_t parent_ino,
const char *name,
fuse_ino_t newparent_ino,
const char *newname,
unsigned int flags)
{
g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino);
g_autoptr(XdpInode) newparent = xdp_inode_from_ino (newparent_ino);
g_autofree char *rename_flags_string = renameat2_flags_to_string (flags);
XdpDomain *domain;
int res, errsv;
int olddirfd, newdirfd, dirfd;
g_autofd int close_fd1 = -1;
g_autofd int close_fd2 = -1;
const char *op = "RENAME";
g_debug ("RENAME %" G_GINT64_MODIFIER "x %s -> %" G_GINT64_MODIFIER "x %s (flags: %s)", parent_ino, name,
newparent_ino, newname, rename_flags_string);
if (!xdp_document_inode_checks (op, req, parent,
CHECK_CAN_WRITE |
CHECK_IS_PHYSICAL_IF_DIR))
return;
/* Don't allow cross-domain renames */
if (parent->domain != newparent->domain)
return xdp_reply_err (op, req, EXDEV);
domain = parent->domain;
if (xdp_document_domain_is_dir (domain))
{
olddirfd = xdp_document_inode_ensure_dirfd (parent, &close_fd1);
if (olddirfd < 0)
return xdp_reply_err (op, req, -olddirfd);
newdirfd = xdp_document_inode_ensure_dirfd (newparent, &close_fd2);
if (newdirfd < 0)
return xdp_reply_err (op, req, -newdirfd);
res = try_renameat (olddirfd, name, newdirfd, newname, flags);
if (res != 0)
return xdp_reply_err (op, req, errno);
xdp_reply_ok (op, req);
}
else
{
/* For non-directories, only allow renames in toplevel (nonphysical) dir */
if (parent != newparent || parent->physical != NULL)
return xdp_reply_err (op, req, EACCES);
/* Early exit for same file */
if (strcmp (name, newname) == 0)
return xdp_reply_ok (op, req);
dirfd = xdp_nonphysical_document_inode_opendir (parent);
if (dirfd < 0)
return xdp_reply_err (op, req, -dirfd);
close_fd1 = dirfd;
if (strcmp (name, domain->doc_file) == 0)
{
/* Source is (maybe) main file, destination is tempfile */
g_autofree char *tmpname = NULL;
int tmp_fd;
/* Just use this to get an exclusive name, we will later replace its content */
tmp_fd = open_temp_at (dirfd, newname, &tmpname, 0600);
if (tmp_fd < 0)
return xdp_reply_err (op, req, -tmp_fd);
close (tmp_fd);
g_mutex_lock (&domain->tempfile_mutex);
res = try_renameat (dirfd, name, dirfd, tmpname, flags);
if (res == -1)
{
res = -errno;
/* Remove the temporary file if the move failed */
(void) unlinkat (dirfd, tmpname, 0);
}
else
{
res = get_tempfile_for (parent, domain, newname, dirfd, tmpname, NULL);
}
g_mutex_unlock (&domain->tempfile_mutex);
if (res != 0)
return xdp_reply_err (op, req, -res);
xdp_reply_ok (op, req);
}
else if (strcmp (newname, domain->doc_file) == 0)
{
gpointer stolen_value;
/* source is (maybe) tempfile, Destination is main file */
g_mutex_lock (&domain->tempfile_mutex);
if (g_hash_table_steal_extended (domain->tempfiles, name,
NULL, &stolen_value))
{
XdpTempfile *tempfile = stolen_value;
res = try_renameat (dirfd, tempfile->tempname, dirfd, newname, flags);
errsv = errno;
if (res == -1) /* Revert tempfile steal */
g_hash_table_replace (domain->tempfiles, tempfile->name, tempfile);
else
{
/* Steal the old tempname so we don't unlink it */
g_free (g_steal_pointer (&tempfile->tempname));
xdp_tempfile_unref (tempfile);
}
}
else
{
res = -1;
errsv = ENOENT;
}
g_mutex_unlock (&domain->tempfile_mutex);
if (res != 0)
return xdp_reply_err (op, req, errsv);
xdp_reply_ok (op, req);
}
else
{
/* Source and destinations are both tempfiles, no need to change anything on disk */
gboolean found_tempfile = FALSE;
gpointer stolen_value;
/* Renaming temp file to temp file */
g_mutex_lock (&domain->tempfile_mutex);
if (g_hash_table_steal_extended (domain->tempfiles, name,
NULL, &stolen_value))
{
XdpTempfile *tempfile = stolen_value;
found_tempfile = TRUE;
g_free (tempfile->name);
tempfile->name = g_strdup (newname);
/* This destroys any pre-existing tempfile with this name */
g_hash_table_replace (domain->tempfiles, tempfile->name, tempfile);
}
g_mutex_unlock (&domain->tempfile_mutex);
if (!found_tempfile)
return xdp_reply_err (op, req, ENOENT);
xdp_reply_ok (op, req);
}
}
}
static void
xdp_fuse_access (fuse_req_t req,
fuse_ino_t ino,
int mask)
{
g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
g_autofree char *path = NULL;
int res;
const char *op = "ACCESS";
g_debug ("ACCESS %" G_GINT64_MODIFIER "x", ino);
if (inode->domain->type != XDP_DOMAIN_DOCUMENT)
{
if (mask & W_OK)
return xdp_reply_err (op, req, EPERM);
else
return xdp_reply_ok (op, req);
}
if ((mask & W_OK) != 0 &&
!xdp_document_domain_can_write (inode->domain))
return xdp_reply_err (op, req, EPERM);
if (inode->physical)
{
path = fd_to_path (inode->physical->fd);
res = access (path, mask);
}
else
{
if (xdp_document_domain_is_dir (inode->domain))
{
if (mask & W_OK)
res = EPERM;
else
res = 0;
}
else
res = access (inode->domain->doc_path, mask);
}
if (res == -1)
xdp_reply_err (op, req, errno);
else
xdp_reply_ok (op, req);
}
static void
xdp_fuse_rmdir (fuse_req_t req,
fuse_ino_t parent_ino,
const char *filename)
{
g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino);
g_autofd int close_fd = -1;
int dirfd;
int res;
const char *op = "RMDIR";
g_debug ("RMDIR %" G_GINT64_MODIFIER "x %s", parent_ino, filename);
if (!xdp_document_inode_checks (op, req, parent,
CHECK_CAN_WRITE |
CHECK_IS_DIRECTORY |
CHECK_IS_PHYSICAL))
return;
dirfd = xdp_document_inode_ensure_dirfd (parent, &close_fd);
if (dirfd < 0)
return xdp_reply_err (op, req, -dirfd);
res = unlinkat (dirfd, filename, AT_REMOVEDIR);
if (res != 0)
return xdp_reply_err (op, req, errno);
xdp_reply_ok (op, req);
}
static void
xdp_fuse_readlink (fuse_req_t req,
fuse_ino_t ino)
{
g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
char linkname[PATH_MAX + 1];
const char *op = "READLINK";
ssize_t res;
g_debug ("READLINK %" G_GINT64_MODIFIER "x", ino);
if (!xdp_document_inode_checks (op, req, inode,
CHECK_IS_DIRECTORY |
CHECK_IS_PHYSICAL))
return;
if (inode->physical == NULL)
return xdp_reply_err (op, req, EINVAL);
res = readlinkat (inode->physical->fd, "", linkname, sizeof(linkname));
if (res < 0)
return xdp_reply_err (op, req, errno);
linkname[res] = '\0';
fuse_reply_readlink (req, linkname);
}
static void
xdp_fuse_symlink (fuse_req_t req,
const char *link,
fuse_ino_t parent_ino,
const char *name)
{
g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino);
g_autofd int close_fd = -1;
struct fuse_entry_param e;
const char * op = "SYMLINK";
int dirfd;
int res;
g_debug ("SYMLINK %s %" G_GINT64_MODIFIER "x %s", link, parent_ino, name);
if (!xdp_document_inode_checks (op, req, parent,
CHECK_CAN_WRITE |
CHECK_IS_DIRECTORY |
CHECK_IS_PHYSICAL))
return;
dirfd = xdp_document_inode_ensure_dirfd (parent, &close_fd);
if (dirfd < 0)
return xdp_reply_err (op, req, -dirfd);
res = symlinkat (link, dirfd, name);
if (res != 0)
return xdp_reply_err (op, req, errno);
res = ensure_docdir_inode_by_name (parent, dirfd, name, &e); /* Takes ownership of o_path_fd */
if (res != 0)
return xdp_reply_err (op, req, -res);
if (fuse_reply_entry (req, &e) == -ENOENT)
abort_reply_entry (&e);
}
static void
xdp_fuse_link (fuse_req_t req,
fuse_ino_t ino,
fuse_ino_t newparent_ino,
const char *newname)
{
g_autoptr(XdpInode) newparent = xdp_inode_from_ino (newparent_ino);
g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
g_autofree char *proc_path = NULL;
g_autofd int close_fd = -1;
struct fuse_entry_param e;
const char * op = "LINK";
int newparent_dirfd;
int res;
g_debug ("LINK %" G_GINT64_MODIFIER "x %" G_GINT64_MODIFIER "x %s", ino, newparent_ino, newname);
/* hardlinks only supported in docdirs, and only physical files */
if (!xdp_document_inode_checks (op, req, inode,
CHECK_CAN_WRITE |
CHECK_IS_DIRECTORY |
CHECK_IS_PHYSICAL))
return;
/* Don't allow linking between domains */
if (inode->domain != newparent->domain)
return xdp_reply_err (op, req, EXDEV);
proc_path = fd_to_path (inode->physical->fd);
newparent_dirfd = xdp_document_inode_ensure_dirfd (newparent, &close_fd);
if (newparent_dirfd < 0)
return xdp_reply_err (op, req, -newparent_dirfd);
res = linkat (AT_FDCWD, proc_path, newparent_dirfd, newname, AT_SYMLINK_FOLLOW);
if (res != 0)
return xdp_reply_err (op, req, errno);
res = ensure_docdir_inode_by_name (inode, newparent_dirfd, newname, &e); /* Takes ownership of o_path_fd */
if (res != 0)
return xdp_reply_err (op, req, -res);
if (fuse_reply_entry (req, &e) == -ENOENT)
abort_reply_entry (&e);
}
static void
xdp_fuse_statfs (fuse_req_t req,
fuse_ino_t ino)
{
g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
struct statvfs buf;
int res;
const char *op = "STATFS";
g_debug ("STATFS %" G_GINT64_MODIFIER "x", ino);
if (!xdp_document_inode_checks (op, req, inode, 0))
return;
if (inode->physical)
res = fstatvfs (inode->physical->fd, &buf);
else
res = statvfs (inode->domain->doc_path, &buf);
if (!res)
fuse_reply_statfs (req, &buf);
else
xdp_reply_err (op, req, errno);
}
static gboolean
xdp_fuse_get_real_path (XdpPhysicalInode *physical,
char **real_path_out)
{
g_autofree char *fd_path = fd_to_path (physical->fd);
char path_buffer[PATH_MAX + 1];
DevIno file_devino = physical->backing_devino;
ssize_t symlink_size;
struct stat buf;
/* Try to extract a real path to the file
* (and verify it goes to the same place as the fd) */
symlink_size = readlink (fd_path, path_buffer, PATH_MAX);
if (symlink_size < 1)
return FALSE;
path_buffer[symlink_size] = 0;
if (lstat (path_buffer, &buf) != 0 ||
buf.st_dev != file_devino.dev ||
buf.st_ino != file_devino.ino)
return FALSE;
*real_path_out = g_strdup (path_buffer);
return TRUE;
}
static ssize_t
xdp_fuse_set_host_path_xattr (XdpInode *inode,
const char *value,
size_t size)
{
errno = EPERM;
return -1;
}
static ssize_t
xdp_fuse_get_host_path_xattr (XdpInode *inode,
char *buf,
size_t size)
{
const char *path = NULL;
size_t path_size;
g_autofree char *real_path = NULL;
path = inode->domain->doc_path;
if (!path)
{
errno = ENODATA;
return -1;
}
if (inode->physical)
{
if (!xdp_fuse_get_real_path (inode->physical, &real_path))
{
errno = ENODATA;
return -1;
}
if (!g_str_has_prefix (real_path, path))
{
errno = ENODATA;
return -1;
}
path = real_path;
}
path_size = strlen (path);
if (size == 0)
return path_size;
if (size < path_size)
{
errno = ERANGE;
return -1;
}
memcpy (buf, path, path_size);
return path_size;
}
static ssize_t
xdp_fuse_remove_host_path_xattr (XdpInode *inode)
{
errno = EPERM;
return -1;
}
static void
xdp_fuse_setxattr (fuse_req_t req,
fuse_ino_t ino,
const char *name,
const char *value,
size_t size,
int flags)
{
g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
ssize_t res;
g_autofree char *path = NULL;
const char *op = "SETXATTR";
g_debug ("SETXATTR %" G_GINT64_MODIFIER "x %s", ino, name);
if (!xdp_document_inode_checks (op, req, inode,
CHECK_CAN_WRITE |
CHECK_IS_DIRECTORY |
CHECK_IS_PHYSICAL))
return;
if (g_strcmp0 (name, XDP_XATTR_HOST_PATH) == 0)
{
res = xdp_fuse_set_host_path_xattr (inode, value, size);
}
else
{
path = fd_to_path (inode->physical->fd);
#if defined(HAVE_SYS_XATTR_H)
res = setxattr (path, name, value, size, flags);
#elif defined(HAVE_SYS_EXTATTR_H)
res = extattr_set_file (path, EXTATTR_NAMESPACE_USER, name, value, size);
#else
#error "Not implemented for your platform"
#endif
}
if (res < 0)
return xdp_reply_err (op, req, errno);
xdp_reply_ok (op, req);
}
static void
xdp_fuse_getxattr (fuse_req_t req,
fuse_ino_t ino,
const char *name,
size_t size)
{
g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
ssize_t res;
g_autofree char *buf = NULL;
g_autofree char *path = NULL;
const char *op = "GETXATTR";
g_debug ("GETXATTR %" G_GINT64_MODIFIER "x %s %" G_GSIZE_FORMAT, ino, name, size);
if (inode->domain->type != XDP_DOMAIN_DOCUMENT)
return xdp_reply_err (op, req, ENODATA);
if (size != 0)
buf = g_malloc (size);
path = xdp_document_inode_get_self_as_path (inode);
if (path == NULL)
return xdp_reply_err (op, req, ENODATA);
if (g_strcmp0 (name, XDP_XATTR_HOST_PATH) == 0)
{
res = xdp_fuse_get_host_path_xattr (inode, buf, size);
}
else
{
#if defined(HAVE_SYS_XATTR_H)
res = getxattr (path, name, buf, size);
#elif defined(HAVE_SYS_EXTATTR_H)
res = extattr_get_file (path, EXTATTR_NAMESPACE_USER, name, buf, size);
#else
#error "Not implemented for your platform"
#endif
}
if (res < 0)
return xdp_reply_err (op, req, errno);
if (size == 0)
fuse_reply_xattr (req, res);
else
fuse_reply_buf (req, buf, res);
}
static ssize_t
xdp_fuse_listxattr_xdp_attrs (XdpInode *inode,
ssize_t res,
char *buf,
size_t size)
{
size_t attr_size = sizeof (XDP_XATTR_ATTRIBUTE_NAME_LIST);
if (size == 0)
return res + attr_size;
if (size < res + attr_size)
{
errno = ERANGE;
return -1;
}
memcpy (buf + res, XDP_XATTR_ATTRIBUTE_NAME_LIST, attr_size);
return res + attr_size;
}
static void
xdp_fuse_listxattr (fuse_req_t req,
fuse_ino_t ino,
size_t size)
{
g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
g_autofree char *path = NULL;
g_autofree char *buf = NULL;
const char *op = "LISTXATTR";
ssize_t res;
g_debug ("LISTXATTR %" G_GINT64_MODIFIER "x %" G_GSIZE_FORMAT, ino, size);
if (inode->domain->type != XDP_DOMAIN_DOCUMENT)
return xdp_reply_err (op, req, ENOTSUP);
if (size != 0)
buf = g_malloc (size);
path = xdp_document_inode_get_self_as_path (inode);
if (path == NULL)
{
res = 0;
}
else
{
#if defined(HAVE_SYS_XATTR_H)
res = listxattr (path, buf, size);
#elif defined(HAVE_SYS_EXTATTR_H)
res = extattr_list_file (path, EXTATTR_NAMESPACE_USER, buf, size);
#else
#error "Not implemented for your platform"
#endif
if (res >= 0)
res = xdp_fuse_listxattr_xdp_attrs (inode, res, buf, size);
}
if (res < 0)
return xdp_reply_err (op, req, errno);
if (size == 0)
fuse_reply_xattr (req, res);
else
fuse_reply_buf (req, buf, res);
}
static void
xdp_fuse_removexattr (fuse_req_t req,
fuse_ino_t ino,
const char *name)
{
g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
g_autofree char *path = NULL;
ssize_t res;
const char *op = "REMOVEXATTR";
g_debug ("REMOVEXATTR %" G_GINT64_MODIFIER "x %s", ino, name);
if (!xdp_document_inode_checks (op, req, inode,
CHECK_CAN_WRITE |
CHECK_IS_DIRECTORY |
CHECK_IS_PHYSICAL))
return;
if (g_strcmp0 (name, XDP_XATTR_HOST_PATH) == 0)
{
res = xdp_fuse_remove_host_path_xattr (inode);
}
else
{
path = fd_to_path (inode->physical->fd);
#if defined(HAVE_SYS_XATTR_H)
res = removexattr (path, name);
#elif defined(HAVE_SYS_EXTATTR_H)
res = extattr_delete_file (path, EXTATTR_NAMESPACE_USER, name);
#else
#error "Not implemented for your platform"
#endif
}
if (res < 0)
xdp_reply_err (op, req, errno);
else
xdp_reply_ok (op, req);
}
static void
xdp_fuse_getlk (fuse_req_t req,
fuse_ino_t ino,
struct fuse_file_info *fi,
struct flock *lock)
{
const char *op = "GETLK";
XdpFile *file = (XdpFile *)fi->fh;
int res;
g_debug ("GETLK %" G_GINT64_MODIFIER "x", ino);
res = fcntl (file->fd, F_GETLK, lock);
if (res < 0)
return xdp_reply_err (op, req, errno);
return xdp_reply_ok (op, req);
}
static void
xdp_fuse_setlk (fuse_req_t req,
fuse_ino_t ino,
struct fuse_file_info *fi,
struct flock *lock,
int sleep)
{
const char *op = "SETLK";
XdpFile *file = (XdpFile *)fi->fh;
int res;
g_debug ("SETLK %" G_GINT64_MODIFIER "x", ino);
res = fcntl (file->fd, F_SETLK, lock);
if (res < 0)
return xdp_reply_err (op, req, errno);
return xdp_reply_ok (op, req);
}
static void
xdp_fuse_flock (fuse_req_t req,
fuse_ino_t ino,
struct fuse_file_info *fi,
int lock_op)
{
const char *op = "FLOCK";
g_debug ("FLOCK %" G_GINT64_MODIFIER "x", ino);
xdp_reply_err (op, req, ENOSYS);
}
static void
xdp_fuse_init_cb (void *userdata,
struct fuse_conn_info *conn)
{
XdpFuseOptions *fuse_opts = userdata;
g_debug ("INIT");
/* atomic_o_trunc: We handle O_TRUNC in create() */
conn->want |= FUSE_CAP_ATOMIC_O_TRUNC;
if (fuse_opts->use_splice)
{
/* splice_read: use splice() to read from fuse pipe */
conn->want |= FUSE_CAP_SPLICE_READ;
/* splice_write: use splice() to write to fuse pipe */
conn->want |= FUSE_CAP_SPLICE_WRITE;
/* splice_move: move buffers from writing app to kernel during splice write */
conn->want |= FUSE_CAP_SPLICE_MOVE;
}
}
extern gboolean on_fuse_unmount (void *);
static void
xdp_fuse_destroy_cb (void *userdata)
{
XdpFuseOptions *fuse_opts = userdata;
g_debug ("DESTROY");
/* Ensure we call this on the main thread */
g_idle_add ((GSourceFunc) on_fuse_unmount, NULL);
g_clear_pointer (&fuse_opts, g_free);
}
static struct fuse_lowlevel_ops xdp_fuse_oper = {
.init = xdp_fuse_init_cb,
.destroy = xdp_fuse_destroy_cb,
.lookup = xdp_fuse_lookup,
.getattr = xdp_fuse_getattr,
.setattr = xdp_fuse_setattr,
.readdir = xdp_fuse_readdir,
.open = xdp_fuse_open,
.read = xdp_fuse_read,
.write = xdp_fuse_write,
.write_buf = xdp_fuse_write_buf,
.fsync = xdp_fuse_fsync,
.forget = xdp_fuse_forget,
.forget_multi = xdp_fuse_forget_multi,
.releasedir = xdp_fuse_releasedir,
.release = xdp_fuse_release,
.opendir = xdp_fuse_opendir,
.fsyncdir = xdp_fuse_fsyncdir,
.create = xdp_fuse_create,
.unlink = xdp_fuse_unlink,
.rename = xdp_fuse_rename,
.access = xdp_fuse_access,
.readlink = xdp_fuse_readlink,
.rmdir = xdp_fuse_rmdir,
.mkdir = xdp_fuse_mkdir,
.symlink = xdp_fuse_symlink,
.link = xdp_fuse_link,
.flush = xdp_fuse_flush,
.statfs = xdp_fuse_statfs,
.setxattr = xdp_fuse_setxattr,
.getxattr = xdp_fuse_getxattr,
.listxattr = xdp_fuse_listxattr,
.removexattr = xdp_fuse_removexattr,
.getlk = xdp_fuse_getlk,
.setlk = xdp_fuse_setlk,
.flock = xdp_fuse_flock,
.fallocate = xdp_fuse_fallocate,
};
typedef struct {
GMutex lock;
GCond cond;
GError *error;
} XdpFuseThreadData;
static void
xdp_fuse_mainloop (struct fuse_session *se,
struct fuse_loop_config *loop_config)
{
const char *status;
fuse_session_loop_mt (se, loop_config);
status = getenv ("TEST_DOCUMENT_PORTAL_FUSE_STATUS");
if (status)
{
GError *error = NULL;
g_autoptr(GString) s = g_string_new ("");
g_string_append (s, "ok");
g_file_set_contents (status, s->str, -1, &error);
g_assert_no_error (error);
}
}
typedef struct fuse_args XdpAutoFuseArgs;
G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (XdpAutoFuseArgs, fuse_opt_free_args);
static gpointer
xdp_fuse_thread (gpointer data)
{
/* Options:
* auto_unmount: Tell fusermount to auto unmount if we die.
*/
static char *fusermount_argv[] = {
"xdp-fuse", "-osubtype=portal,fsname=portal,auto_unmount",
};
g_auto(XdpAutoFuseArgs) args =
FUSE_ARGS_INIT (G_N_ELEMENTS (fusermount_argv), fusermount_argv);
g_autoptr(GMutexLocker) session_locker = NULL;
g_autoptr(GMutexLocker) locker = NULL;
struct fuse_cmdline_opts opts = {0};
struct fuse_loop_config loop_config = {0};
XdpFuseThreadData *thread_data = data;
XdpFuseOptions *fuse_opts = NULL;
struct fuse_session *se;
const char *path;
locker = g_mutex_locker_new (&thread_data->lock);
fuse_pthread = pthread_self ();
g_cond_signal (&thread_data->cond);
if (fuse_parse_cmdline (&args, &opts) != 0)
{
g_set_error (&thread_data->error, XDG_DESKTOP_PORTAL_ERROR,
XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
"Impossible to parse command line");
return NULL;
}
fuse_opts = g_new0 (XdpFuseOptions, 1);
#ifdef WITH_SPLICE
fuse_opts->use_splice = TRUE;
#endif
se = fuse_session_new (&args, &xdp_fuse_oper,
sizeof (xdp_fuse_oper), fuse_opts);
if (se == NULL)
{
g_set_error (&thread_data->error, XDG_DESKTOP_PORTAL_ERROR,
XDG_DESKTOP_PORTAL_ERROR_FAILED,
"Can't create fuse session");
g_clear_pointer (&fuse_opts, g_free);
return NULL;
}
path = xdp_fuse_get_mountpoint ();
if (fuse_session_mount (se, path) != 0)
{
fuse_session_destroy (se);
g_set_error (&thread_data->error, XDG_DESKTOP_PORTAL_ERROR,
XDG_DESKTOP_PORTAL_ERROR_FAILED,
"Can't mount path %s", path);
return NULL;
}
session = se;
thread_data = NULL;
g_clear_pointer (&locker, g_mutex_locker_free);
loop_config.clone_fd = opts.clone_fd;
loop_config.max_idle_threads = opts.max_idle_threads;
thread_data = NULL;
session_locker = g_mutex_locker_new (&G_LOCK_NAME (session));
g_clear_pointer (&session_locker, g_mutex_locker_free);
xdp_fuse_mainloop (session, &loop_config);
session_locker = g_mutex_locker_new (&G_LOCK_NAME (session));
fuse_session_unmount (se);
fuse_session_destroy (se);
session = NULL;
return NULL;
}
gboolean
xdp_fuse_init (GError **error)
{
g_autoptr(XdpDomain) by_app_domain = NULL;
g_autoptr(XdpDomain) root_domain = NULL;
XdpFuseThreadData thread_data = {0};
struct statfs stfs;
struct rlimit rl;
struct stat st;
const char *path;
int statfs_res;
my_uid = getuid ();
my_gid = getgid ();
all_inodes = g_hash_table_new_full (g_int64_hash, g_int64_equal, NULL, NULL);
g_assert (open_files == NULL);
root_domain = xdp_domain_new_root ();
root_inode = xdp_inode_new (root_domain, NULL);
by_app_domain = xdp_domain_new_by_app (root_inode);
by_app_inode = xdp_inode_new (by_app_domain, NULL);
physical_inodes =
g_hash_table_new_full (devino_hash, devino_equal, NULL, NULL);
/* Bump nr of filedescriptor limit to max */
if (getrlimit (RLIMIT_NOFILE , &rl) == 0 &&
rl.rlim_cur != rl.rlim_max)
{
rl.rlim_cur = rl.rlim_max;
setrlimit (RLIMIT_NOFILE, &rl);
}
path = xdp_fuse_get_mountpoint ();
if ((stat (path, &st) == -1 && errno == ENOTCONN) ||
(((statfs_res = statfs (path, &stfs)) == -1 && errno == ENOTCONN) ||
(statfs_res == 0 && stfs.f_type == 0x65735546 /* fuse */)))
{
int count;
char *umount_argv[] = { "fusermount3", "-u", "-z", (char *) path, NULL };
g_spawn_sync (NULL, umount_argv, NULL, G_SPAWN_SEARCH_PATH,
NULL, NULL, NULL, NULL, NULL, NULL);
g_usleep (10000); /* 10ms */
count = 0;
while (stat (path, &st) == -1 && count < 10)
g_usleep (10000); /* 10ms */
}
if (g_mkdir_with_parents (path, 0700))
{
g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED,
"Unable to create dir %s", path);
return FALSE;
}
g_mutex_init (&thread_data.lock);
g_cond_init (&thread_data.cond);
g_mutex_lock (&thread_data.lock);
XDP_AUTOLOCK (session);
fuse_thread = g_thread_new ("fuse mainloop", xdp_fuse_thread, &thread_data);
while (session == NULL && thread_data.error == NULL)
g_cond_wait (&thread_data.cond, &thread_data.lock);
g_mutex_unlock (&thread_data.lock);
g_cond_clear (&thread_data.cond);
g_mutex_clear (&thread_data.lock);
if (thread_data.error != NULL)
{
g_propagate_error (error, g_steal_pointer (&thread_data.error));
return FALSE;
}
g_assert (session != NULL);
XDP_AUTOLOCK (open_files);
while (open_files)
xdp_file_free (open_files->data);
return TRUE;
}
void
xdp_fuse_exit (void)
{
{
XDP_AUTOLOCK (session);
if (session)
fuse_session_exit (session);
if (fuse_pthread)
pthread_kill (fuse_pthread, SIGHUP);
}
g_clear_pointer (&fuse_thread, g_thread_join);
g_assert (session == NULL);
}
const char *
xdp_fuse_get_mountpoint (void)
{
if (mount_path == NULL)
mount_path = g_build_filename (g_get_user_runtime_dir (), "doc", NULL);
return mount_path;
}
typedef struct {
fuse_ino_t ino;
char *filename;
} Invalidate;
/* Called with domain_inodes lock held, don't block */
static void
invalidate_doc_inode (XdpInode *parent_inode,
const char *doc_id,
GArray *invalidates)
{
XdpInode *doc_inode = g_hash_table_lookup (parent_inode->domain->inodes, doc_id);
Invalidate inval;
if (doc_inode == NULL)
return;
inval.ino = xdp_inode_to_ino (doc_inode);
inval.filename = NULL;
g_array_append_val (invalidates, inval);
inval.ino = xdp_inode_to_ino (parent_inode);
inval.filename = g_strdup (doc_id);
g_array_append_val (invalidates, inval);
/* No need to invalidate doc children, we don't cache them */
}
/* Called when a apps permissions to see a document is changed,
and with null opt_app_id when the doc is created/removed */
void
xdp_fuse_invalidate_doc_app (const char *doc_id,
const char *opt_app_id)
{
g_autoptr(GArray) invalidates = NULL;
XDP_AUTOLOCK (session);
int i;
/* This can happen if fuse is not initialized yet for the very
first dbus message that activated the service */
if (session == NULL)
return;
g_debug ("invalidate %s/%s", doc_id, opt_app_id ? opt_app_id : "*");
invalidates = g_array_new (FALSE, FALSE, sizeof (Invalidate));
G_LOCK (domain_inodes);
if (opt_app_id != NULL)
{
XdpInode *app_inode = g_hash_table_lookup (by_app_inode->domain->inodes, opt_app_id);
if (app_inode)
invalidate_doc_inode (app_inode, doc_id, invalidates);
}
else
{
GHashTableIter iter;
gpointer key, value;
invalidate_doc_inode (root_inode, doc_id, invalidates);
g_hash_table_iter_init (&iter, by_app_inode->domain->inodes);
while (g_hash_table_iter_next (&iter, &key, &value))
invalidate_doc_inode ((XdpInode *)value, doc_id, invalidates);
}
G_UNLOCK (domain_inodes);
for (i = 0; i < invalidates->len; i++)
{
Invalidate *invalidate = &g_array_index (invalidates, Invalidate, i);
if (invalidate->filename)
{
fuse_lowlevel_notify_inval_entry (session, invalidate->ino,
invalidate->filename, strlen (invalidate->filename));
g_free (invalidate->filename);
}
else
fuse_lowlevel_notify_inval_inode (session, invalidate->ino, 0, 0);
}
}
char *
xdp_fuse_lookup_id_for_inode (ino_t ino,
gboolean directory,
char **real_path_out)
{
g_autoptr(XdpDomain) domain = NULL;
g_autoptr(XdpPhysicalInode) physical = NULL;
DevIno file_devino;
struct stat buf;
if (real_path_out)
*real_path_out = NULL;
G_LOCK (all_inodes);
{
XdpInode *inode = g_hash_table_lookup (all_inodes, &ino);
if (inode)
{
/* We're not allowed to resurrect the inode here, but we can get the data while in the lock */
domain = xdp_domain_ref (inode->domain);
if (inode->physical)
physical = xdp_physical_inode_ref (inode->physical);
}
}
G_UNLOCK (all_inodes);
if (domain == NULL)
return NULL;
if (domain->type != XDP_DOMAIN_DOCUMENT)
return NULL;
if (physical == NULL)
return NULL;
file_devino = physical->backing_devino;
if (!xdp_document_domain_is_dir (domain))
{
g_autofree char *main_path = g_build_filename (domain->doc_path, domain->doc_file, NULL);
/* file document */
if (directory)
return NULL;
/* Only return for main file */
if (lstat (main_path, &buf) == 0 &&
buf.st_dev == file_devino.dev &&
buf.st_ino == file_devino.ino)
return g_strdup (domain->doc_id);
}
else
{
/* directory document */
/* Only return entire doc for main dir */
if (file_devino.dev == domain->doc_dir_device &&
file_devino.ino == domain->doc_dir_inode)
return g_strdup (domain->doc_id);
/* But maybe its a subfile of the document */
if (real_path_out && xdp_fuse_get_real_path (physical, real_path_out))
return g_strdup (domain->doc_id);
}
return NULL;
}