cachefiles: Implement backing file wrangling
authorDavid Howells <dhowells@redhat.com>
Thu, 21 Oct 2021 07:50:10 +0000 (08:50 +0100)
committerDavid Howells <dhowells@redhat.com>
Fri, 7 Jan 2022 13:42:40 +0000 (13:42 +0000)
Implement the wrangling of backing files, including the following pieces:

 (1) Lookup and creation of a file on disk, using a tmpfile if the file
     isn't yet present.  The file is then opened, sized for DIO and the
     file handle is attached to the cachefiles_object struct.  The inode is
     marked to indicate that it's in use by a kernel service.

 (2) Invalidation of an object, creating a tmpfile and switching the file
     pointer in the cachefiles object.

 (3) Committing a file to disk, including setting the coherency xattr on it
     and, if necessary, creating a hard link to it.

     Note that this would be a good place to use Omar Sandoval's vfs_link()
     with AT_LINK_REPLACE[1] as I may have to unlink an old file before I
     can link a tmpfile into place.

 (4) Withdrawal of open objects when a cache is being withdrawn or a cookie
     is relinquished.  This involves committing or discarding the file.

Changes
=======
ver #2:
 - Fix logging of wrong error[1].

Signed-off-by: David Howells <dhowells@redhat.com>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
cc: linux-cachefs@redhat.com
Link: https://lore.kernel.org/r/20211203094950.GA2480@kili/
Link: https://lore.kernel.org/r/163819644097.215744.4505389616742411239.stgit@warthog.procyon.org.uk/
Link: https://lore.kernel.org/r/163906949512.143852.14222856795032602080.stgit@warthog.procyon.org.uk/
Link: https://lore.kernel.org/r/163967158526.1823006.17482695321424642675.stgit@warthog.procyon.org.uk/
Link: https://lore.kernel.org/r/164021557060.640689.16373541458119269871.stgit@warthog.procyon.org.uk/
fs/cachefiles/cache.c
fs/cachefiles/daemon.c
fs/cachefiles/interface.c
fs/cachefiles/internal.h
fs/cachefiles/namei.c

index c4b9280ca0cdfbd0eacb9bd732ade0a0a18e1e02..e2cbbc08bad9cac25ee45a0da26f650844a05bd9 100644 (file)
@@ -262,6 +262,36 @@ begin_cull:
        return ret;
 }
 
+/*
+ * Mark all the objects as being out of service and queue them all for cleanup.
+ */
+static void cachefiles_withdraw_objects(struct cachefiles_cache *cache)
+{
+       struct cachefiles_object *object;
+       unsigned int count = 0;
+
+       _enter("");
+
+       spin_lock(&cache->object_list_lock);
+
+       while (!list_empty(&cache->object_list)) {
+               object = list_first_entry(&cache->object_list,
+                                         struct cachefiles_object, cache_link);
+               cachefiles_see_object(object, cachefiles_obj_see_withdrawal);
+               list_del_init(&object->cache_link);
+               fscache_withdraw_cookie(object->cookie);
+               count++;
+               if ((count & 63) == 0) {
+                       spin_unlock(&cache->object_list_lock);
+                       cond_resched();
+                       spin_lock(&cache->object_list_lock);
+               }
+       }
+
+       spin_unlock(&cache->object_list_lock);
+       _leave(" [%u objs]", count);
+}
+
 /*
  * Withdraw volumes.
  */
@@ -326,7 +356,7 @@ void cachefiles_withdraw_cache(struct cachefiles_cache *cache)
        /* we now have to destroy all the active objects pertaining to this
         * cache - which we do by passing them off to thread pool to be
         * disposed of */
-       // PLACEHOLDER: Withdraw objects
+       cachefiles_withdraw_objects(cache);
        fscache_wait_for_objects(fscache);
 
        cachefiles_withdraw_volumes(cache);
index 985c3f3e676742478fca468971cc32e390d6e834..61e8740d01be1457a43d11cacb326d778d18abf6 100644 (file)
@@ -106,6 +106,7 @@ static int cachefiles_daemon_open(struct inode *inode, struct file *file)
        mutex_init(&cache->daemon_mutex);
        init_waitqueue_head(&cache->daemon_pollwq);
        INIT_LIST_HEAD(&cache->volumes);
+       INIT_LIST_HEAD(&cache->object_list);
        spin_lock_init(&cache->object_list_lock);
 
        /* set default caching limits
index 68bb7b6c49459284c1e0125a5a0ed125fa0362ed..e47c52c3407127e881f57e526d16151d91ba20ae 100644 (file)
@@ -99,8 +99,268 @@ void cachefiles_put_object(struct cachefiles_object *object,
        _leave("");
 }
 
+/*
+ * Adjust the size of a cache file if necessary to match the DIO size.  We keep
+ * the EOF marker a multiple of DIO blocks so that we don't fall back to doing
+ * non-DIO for a partial block straddling the EOF, but we also have to be
+ * careful of someone expanding the file and accidentally accreting the
+ * padding.
+ */
+static int cachefiles_adjust_size(struct cachefiles_object *object)
+{
+       struct iattr newattrs;
+       struct file *file = object->file;
+       uint64_t ni_size;
+       loff_t oi_size;
+       int ret;
+
+       ni_size = object->cookie->object_size;
+       ni_size = round_up(ni_size, CACHEFILES_DIO_BLOCK_SIZE);
+
+       _enter("{OBJ%x},[%llu]",
+              object->debug_id, (unsigned long long) ni_size);
+
+       if (!file)
+               return -ENOBUFS;
+
+       oi_size = i_size_read(file_inode(file));
+       if (oi_size == ni_size)
+               return 0;
+
+       inode_lock(file_inode(file));
+
+       /* if there's an extension to a partial page at the end of the backing
+        * file, we need to discard the partial page so that we pick up new
+        * data after it */
+       if (oi_size & ~PAGE_MASK && ni_size > oi_size) {
+               _debug("discard tail %llx", oi_size);
+               newattrs.ia_valid = ATTR_SIZE;
+               newattrs.ia_size = oi_size & PAGE_MASK;
+               ret = cachefiles_inject_remove_error();
+               if (ret == 0)
+                       ret = notify_change(&init_user_ns, file->f_path.dentry,
+                                           &newattrs, NULL);
+               if (ret < 0)
+                       goto truncate_failed;
+       }
+
+       newattrs.ia_valid = ATTR_SIZE;
+       newattrs.ia_size = ni_size;
+       ret = cachefiles_inject_write_error();
+       if (ret == 0)
+               ret = notify_change(&init_user_ns, file->f_path.dentry,
+                                   &newattrs, NULL);
+
+truncate_failed:
+       inode_unlock(file_inode(file));
+
+       if (ret < 0)
+               trace_cachefiles_io_error(NULL, file_inode(file), ret,
+                                         cachefiles_trace_notify_change_error);
+       if (ret == -EIO) {
+               cachefiles_io_error_obj(object, "Size set failed");
+               ret = -ENOBUFS;
+       }
+
+       _leave(" = %d", ret);
+       return ret;
+}
+
+/*
+ * Attempt to look up the nominated node in this cache
+ */
+static bool cachefiles_lookup_cookie(struct fscache_cookie *cookie)
+{
+       struct cachefiles_object *object;
+       struct cachefiles_cache *cache = cookie->volume->cache->cache_priv;
+       const struct cred *saved_cred;
+       bool success;
+
+       object = cachefiles_alloc_object(cookie);
+       if (!object)
+               goto fail;
+
+       _enter("{OBJ%x}", object->debug_id);
+
+       if (!cachefiles_cook_key(object))
+               goto fail_put;
+
+       cookie->cache_priv = object;
+
+       cachefiles_begin_secure(cache, &saved_cred);
+
+       success = cachefiles_look_up_object(object);
+       if (!success)
+               goto fail_withdraw;
+
+       cachefiles_see_object(object, cachefiles_obj_see_lookup_cookie);
+
+       spin_lock(&cache->object_list_lock);
+       list_add(&object->cache_link, &cache->object_list);
+       spin_unlock(&cache->object_list_lock);
+       cachefiles_adjust_size(object);
+
+       cachefiles_end_secure(cache, saved_cred);
+       _leave(" = t");
+       return true;
+
+fail_withdraw:
+       cachefiles_end_secure(cache, saved_cred);
+       cachefiles_see_object(object, cachefiles_obj_see_lookup_failed);
+       fscache_caching_failed(cookie);
+       _debug("failed c=%08x o=%08x", cookie->debug_id, object->debug_id);
+       /* The caller holds an access count on the cookie, so we need them to
+        * drop it before we can withdraw the object.
+        */
+       return false;
+
+fail_put:
+       cachefiles_put_object(object, cachefiles_obj_put_alloc_fail);
+fail:
+       return false;
+}
+
+/*
+ * Commit changes to the object as we drop it.
+ */
+static void cachefiles_commit_object(struct cachefiles_object *object,
+                                    struct cachefiles_cache *cache)
+{
+       bool update = false;
+
+       if (test_and_clear_bit(FSCACHE_COOKIE_LOCAL_WRITE, &object->cookie->flags))
+               update = true;
+       if (test_and_clear_bit(FSCACHE_COOKIE_NEEDS_UPDATE, &object->cookie->flags))
+               update = true;
+       if (update)
+               cachefiles_set_object_xattr(object);
+
+       if (test_bit(CACHEFILES_OBJECT_USING_TMPFILE, &object->flags))
+               cachefiles_commit_tmpfile(cache, object);
+}
+
+/*
+ * Finalise and object and close the VFS structs that we have.
+ */
+static void cachefiles_clean_up_object(struct cachefiles_object *object,
+                                      struct cachefiles_cache *cache)
+{
+       if (test_bit(FSCACHE_COOKIE_RETIRED, &object->cookie->flags)) {
+               if (!test_bit(CACHEFILES_OBJECT_USING_TMPFILE, &object->flags)) {
+                       cachefiles_see_object(object, cachefiles_obj_see_clean_delete);
+                       _debug("- inval object OBJ%x", object->debug_id);
+                       cachefiles_delete_object(object, FSCACHE_OBJECT_WAS_RETIRED);
+               } else {
+                       cachefiles_see_object(object, cachefiles_obj_see_clean_drop_tmp);
+                       _debug("- inval object OBJ%x tmpfile", object->debug_id);
+               }
+       } else {
+               cachefiles_see_object(object, cachefiles_obj_see_clean_commit);
+               cachefiles_commit_object(object, cache);
+       }
+
+       cachefiles_unmark_inode_in_use(object, object->file);
+       if (object->file) {
+               fput(object->file);
+               object->file = NULL;
+       }
+}
+
+/*
+ * Withdraw caching for a cookie.
+ */
+static void cachefiles_withdraw_cookie(struct fscache_cookie *cookie)
+{
+       struct cachefiles_object *object = cookie->cache_priv;
+       struct cachefiles_cache *cache = object->volume->cache;
+       const struct cred *saved_cred;
+
+       _enter("o=%x", object->debug_id);
+       cachefiles_see_object(object, cachefiles_obj_see_withdraw_cookie);
+
+       if (!list_empty(&object->cache_link)) {
+               spin_lock(&cache->object_list_lock);
+               cachefiles_see_object(object, cachefiles_obj_see_withdrawal);
+               list_del_init(&object->cache_link);
+               spin_unlock(&cache->object_list_lock);
+       }
+
+       if (object->file) {
+               cachefiles_begin_secure(cache, &saved_cred);
+               cachefiles_clean_up_object(object, cache);
+               cachefiles_end_secure(cache, saved_cred);
+       }
+
+       cookie->cache_priv = NULL;
+       cachefiles_put_object(object, cachefiles_obj_put_detach);
+}
+
+/*
+ * Invalidate the storage associated with a cookie.
+ */
+static bool cachefiles_invalidate_cookie(struct fscache_cookie *cookie)
+{
+       struct cachefiles_object *object = cookie->cache_priv;
+       struct file *new_file, *old_file;
+       bool old_tmpfile;
+
+       _enter("o=%x,[%llu]", object->debug_id, object->cookie->object_size);
+
+       old_tmpfile = test_bit(CACHEFILES_OBJECT_USING_TMPFILE, &object->flags);
+
+       if (!object->file) {
+               fscache_resume_after_invalidation(cookie);
+               _leave(" = t [light]");
+               return true;
+       }
+
+       new_file = cachefiles_create_tmpfile(object);
+       if (IS_ERR(new_file))
+               goto failed;
+
+       /* Substitute the VFS target */
+       _debug("sub");
+       spin_lock(&object->lock);
+
+       old_file = object->file;
+       object->file = new_file;
+       object->content_info = CACHEFILES_CONTENT_NO_DATA;
+       set_bit(CACHEFILES_OBJECT_USING_TMPFILE, &object->flags);
+       set_bit(FSCACHE_COOKIE_NEEDS_UPDATE, &object->cookie->flags);
+
+       spin_unlock(&object->lock);
+       _debug("subbed");
+
+       /* Allow I/O to take place again */
+       fscache_resume_after_invalidation(cookie);
+
+       if (old_file) {
+               if (!old_tmpfile) {
+                       struct cachefiles_volume *volume = object->volume;
+                       struct dentry *fan = volume->fanout[(u8)cookie->key_hash];
+
+                       inode_lock_nested(d_inode(fan), I_MUTEX_PARENT);
+                       cachefiles_bury_object(volume->cache, object, fan,
+                                              old_file->f_path.dentry,
+                                              FSCACHE_OBJECT_INVALIDATED);
+               }
+               fput(old_file);
+       }
+
+       _leave(" = t");
+       return true;
+
+failed:
+       _leave(" = f");
+       return false;
+}
+
 const struct fscache_cache_ops cachefiles_cache_ops = {
        .name                   = "cachefiles",
        .acquire_volume         = cachefiles_acquire_volume,
        .free_volume            = cachefiles_free_volume,
+       .lookup_cookie          = cachefiles_lookup_cookie,
+       .withdraw_cookie        = cachefiles_withdraw_cookie,
+       .invalidate_cookie      = cachefiles_invalidate_cookie,
+       .prepare_to_write       = cachefiles_prepare_to_write,
 };
index 654dbd51b96590fbcea28b55a1121b8d7d5a2cd6..d7aae04edc61b4b962c35f2538dce9c4d2badcfc 100644 (file)
@@ -16,6 +16,8 @@
 #include <linux/cred.h>
 #include <linux/security.h>
 
+#define CACHEFILES_DIO_BLOCK_SIZE 4096
+
 struct cachefiles_cache;
 struct cachefiles_object;
 
@@ -68,6 +70,7 @@ struct cachefiles_cache {
        struct dentry                   *graveyard;     /* directory into which dead objects go */
        struct file                     *cachefilesd;   /* manager daemon handle */
        struct list_head                volumes;        /* List of volume objects */
+       struct list_head                object_list;    /* List of active objects */
        spinlock_t                      object_list_lock; /* Lock for volumes and object_list */
        const struct cred               *cache_cred;    /* security override for accessing cache */
        struct mutex                    daemon_mutex;   /* command serialisation mutex */
@@ -194,6 +197,9 @@ extern int cachefiles_bury_object(struct cachefiles_cache *cache,
                                  struct dentry *dir,
                                  struct dentry *rep,
                                  enum fscache_why_object_killed why);
+extern int cachefiles_delete_object(struct cachefiles_object *object,
+                                   enum fscache_why_object_killed why);
+extern bool cachefiles_look_up_object(struct cachefiles_object *object);
 extern struct dentry *cachefiles_get_directory(struct cachefiles_cache *cache,
                                               struct dentry *dir,
                                               const char *name,
@@ -205,6 +211,9 @@ extern int cachefiles_cull(struct cachefiles_cache *cache, struct dentry *dir,
 
 extern int cachefiles_check_in_use(struct cachefiles_cache *cache,
                                   struct dentry *dir, char *filename);
+extern struct file *cachefiles_create_tmpfile(struct cachefiles_object *object);
+extern bool cachefiles_commit_tmpfile(struct cachefiles_cache *cache,
+                                     struct cachefiles_object *object);
 
 /*
  * security.c
index e87c401239f17a8e8cdcc2d181526d13c9545a49..b549e9f79c014e9e0feea98a3623739059ce782a 100644 (file)
@@ -404,6 +404,324 @@ try_again:
        return 0;
 }
 
+/*
+ * Delete a cache file.
+ */
+int cachefiles_delete_object(struct cachefiles_object *object,
+                            enum fscache_why_object_killed why)
+{
+       struct cachefiles_volume *volume = object->volume;
+       struct dentry *dentry = object->file->f_path.dentry;
+       struct dentry *fan = volume->fanout[(u8)object->cookie->key_hash];
+       int ret;
+
+       _enter(",OBJ%x{%pD}", object->debug_id, object->file);
+
+       /* Stop the dentry being negated if it's only pinned by a file struct. */
+       dget(dentry);
+
+       inode_lock_nested(d_backing_inode(fan), I_MUTEX_PARENT);
+       ret = cachefiles_unlink(volume->cache, object, fan, dentry, why);
+       inode_unlock(d_backing_inode(fan));
+       dput(dentry);
+       return ret;
+}
+
+/*
+ * Create a temporary file and leave it unattached and un-xattr'd until the
+ * time comes to discard the object from memory.
+ */
+struct file *cachefiles_create_tmpfile(struct cachefiles_object *object)
+{
+       struct cachefiles_volume *volume = object->volume;
+       struct cachefiles_cache *cache = volume->cache;
+       const struct cred *saved_cred;
+       struct dentry *fan = volume->fanout[(u8)object->cookie->key_hash];
+       struct file *file;
+       struct path path;
+       uint64_t ni_size = object->cookie->object_size;
+       long ret;
+
+       ni_size = round_up(ni_size, CACHEFILES_DIO_BLOCK_SIZE);
+
+       cachefiles_begin_secure(cache, &saved_cred);
+
+       path.mnt = cache->mnt;
+       ret = cachefiles_inject_write_error();
+       if (ret == 0)
+               path.dentry = vfs_tmpfile(&init_user_ns, fan, S_IFREG, O_RDWR);
+       else
+               path.dentry = ERR_PTR(ret);
+       if (IS_ERR(path.dentry)) {
+               trace_cachefiles_vfs_error(object, d_inode(fan), PTR_ERR(path.dentry),
+                                          cachefiles_trace_tmpfile_error);
+               if (PTR_ERR(path.dentry) == -EIO)
+                       cachefiles_io_error_obj(object, "Failed to create tmpfile");
+               file = ERR_CAST(path.dentry);
+               goto out;
+       }
+
+       trace_cachefiles_tmpfile(object, d_backing_inode(path.dentry));
+
+       if (!cachefiles_mark_inode_in_use(object, path.dentry)) {
+               file = ERR_PTR(-EBUSY);
+               goto out_dput;
+       }
+
+       if (ni_size > 0) {
+               trace_cachefiles_trunc(object, d_backing_inode(path.dentry), 0, ni_size,
+                                      cachefiles_trunc_expand_tmpfile);
+               ret = cachefiles_inject_write_error();
+               if (ret == 0)
+                       ret = vfs_truncate(&path, ni_size);
+               if (ret < 0) {
+                       trace_cachefiles_vfs_error(
+                               object, d_backing_inode(path.dentry), ret,
+                               cachefiles_trace_trunc_error);
+                       file = ERR_PTR(ret);
+                       goto out_dput;
+               }
+       }
+
+       file = open_with_fake_path(&path, O_RDWR | O_LARGEFILE | O_DIRECT,
+                                  d_backing_inode(path.dentry), cache->cache_cred);
+       if (IS_ERR(file)) {
+               trace_cachefiles_vfs_error(object, d_backing_inode(path.dentry),
+                                          PTR_ERR(file),
+                                          cachefiles_trace_open_error);
+               goto out_dput;
+       }
+       if (unlikely(!file->f_op->read_iter) ||
+           unlikely(!file->f_op->write_iter)) {
+               fput(file);
+               pr_notice("Cache does not support read_iter and write_iter\n");
+               file = ERR_PTR(-EINVAL);
+       }
+
+out_dput:
+       dput(path.dentry);
+out:
+       cachefiles_end_secure(cache, saved_cred);
+       return file;
+}
+
+/*
+ * Create a new file.
+ */
+static bool cachefiles_create_file(struct cachefiles_object *object)
+{
+       struct file *file;
+       int ret;
+
+       ret = cachefiles_has_space(object->volume->cache, 1, 0);
+       if (ret < 0)
+               return false;
+
+       file = cachefiles_create_tmpfile(object);
+       if (IS_ERR(file))
+               return false;
+
+       set_bit(FSCACHE_COOKIE_NEEDS_UPDATE, &object->cookie->flags);
+       set_bit(CACHEFILES_OBJECT_USING_TMPFILE, &object->flags);
+       _debug("create -> %pD{ino=%lu}", file, file_inode(file)->i_ino);
+       object->file = file;
+       return true;
+}
+
+/*
+ * Open an existing file, checking its attributes and replacing it if it is
+ * stale.
+ */
+static bool cachefiles_open_file(struct cachefiles_object *object,
+                                struct dentry *dentry)
+{
+       struct cachefiles_cache *cache = object->volume->cache;
+       struct file *file;
+       struct path path;
+       int ret;
+
+       _enter("%pd", dentry);
+
+       if (!cachefiles_mark_inode_in_use(object, dentry))
+               return false;
+
+       /* We need to open a file interface onto a data file now as we can't do
+        * it on demand because writeback called from do_exit() sees
+        * current->fs == NULL - which breaks d_path() called from ext4 open.
+        */
+       path.mnt = cache->mnt;
+       path.dentry = dentry;
+       file = open_with_fake_path(&path, O_RDWR | O_LARGEFILE | O_DIRECT,
+                                  d_backing_inode(dentry), cache->cache_cred);
+       if (IS_ERR(file)) {
+               trace_cachefiles_vfs_error(object, d_backing_inode(dentry),
+                                          PTR_ERR(file),
+                                          cachefiles_trace_open_error);
+               goto error;
+       }
+
+       if (unlikely(!file->f_op->read_iter) ||
+           unlikely(!file->f_op->write_iter)) {
+               pr_notice("Cache does not support read_iter and write_iter\n");
+               goto error_fput;
+       }
+       _debug("file -> %pd positive", dentry);
+
+       ret = cachefiles_check_auxdata(object, file);
+       if (ret < 0)
+               goto check_failed;
+
+       object->file = file;
+
+       /* Always update the atime on an object we've just looked up (this is
+        * used to keep track of culling, and atimes are only updated by read,
+        * write and readdir but not lookup or open).
+        */
+       touch_atime(&file->f_path);
+       dput(dentry);
+       return true;
+
+check_failed:
+       fscache_cookie_lookup_negative(object->cookie);
+       cachefiles_unmark_inode_in_use(object, file);
+       if (ret == -ESTALE) {
+               fput(file);
+               dput(dentry);
+               return cachefiles_create_file(object);
+       }
+error_fput:
+       fput(file);
+error:
+       dput(dentry);
+       return false;
+}
+
+/*
+ * walk from the parent object to the child object through the backing
+ * filesystem, creating directories as we go
+ */
+bool cachefiles_look_up_object(struct cachefiles_object *object)
+{
+       struct cachefiles_volume *volume = object->volume;
+       struct dentry *dentry, *fan = volume->fanout[(u8)object->cookie->key_hash];
+       int ret;
+
+       _enter("OBJ%x,%s,", object->debug_id, object->d_name);
+
+       /* Look up path "cache/vol/fanout/file". */
+       ret = cachefiles_inject_read_error();
+       if (ret == 0)
+               dentry = lookup_positive_unlocked(object->d_name, fan,
+                                                 object->d_name_len);
+       else
+               dentry = ERR_PTR(ret);
+       trace_cachefiles_lookup(object, dentry);
+       if (IS_ERR(dentry)) {
+               if (dentry == ERR_PTR(-ENOENT))
+                       goto new_file;
+               if (dentry == ERR_PTR(-EIO))
+                       cachefiles_io_error_obj(object, "Lookup failed");
+               return false;
+       }
+
+       if (!d_is_reg(dentry)) {
+               pr_err("%pd is not a file\n", dentry);
+               inode_lock_nested(d_inode(fan), I_MUTEX_PARENT);
+               ret = cachefiles_bury_object(volume->cache, object, fan, dentry,
+                                            FSCACHE_OBJECT_IS_WEIRD);
+               dput(dentry);
+               if (ret < 0)
+                       return false;
+               goto new_file;
+       }
+
+       if (!cachefiles_open_file(object, dentry))
+               return false;
+
+       _leave(" = t [%lu]", file_inode(object->file)->i_ino);
+       return true;
+
+new_file:
+       fscache_cookie_lookup_negative(object->cookie);
+       return cachefiles_create_file(object);
+}
+
+/*
+ * Attempt to link a temporary file into its rightful place in the cache.
+ */
+bool cachefiles_commit_tmpfile(struct cachefiles_cache *cache,
+                              struct cachefiles_object *object)
+{
+       struct cachefiles_volume *volume = object->volume;
+       struct dentry *dentry, *fan = volume->fanout[(u8)object->cookie->key_hash];
+       bool success = false;
+       int ret;
+
+       _enter(",%pD", object->file);
+
+       inode_lock_nested(d_inode(fan), I_MUTEX_PARENT);
+       ret = cachefiles_inject_read_error();
+       if (ret == 0)
+               dentry = lookup_one_len(object->d_name, fan, object->d_name_len);
+       else
+               dentry = ERR_PTR(ret);
+       if (IS_ERR(dentry)) {
+               trace_cachefiles_vfs_error(object, d_inode(fan), PTR_ERR(dentry),
+                                          cachefiles_trace_lookup_error);
+               _debug("lookup fail %ld", PTR_ERR(dentry));
+               goto out_unlock;
+       }
+
+       if (!d_is_negative(dentry)) {
+               if (d_backing_inode(dentry) == file_inode(object->file)) {
+                       success = true;
+                       goto out_dput;
+               }
+
+               ret = cachefiles_unlink(volume->cache, object, fan, dentry,
+                                       FSCACHE_OBJECT_IS_STALE);
+               if (ret < 0)
+                       goto out_dput;
+
+               dput(dentry);
+               ret = cachefiles_inject_read_error();
+               if (ret == 0)
+                       dentry = lookup_one_len(object->d_name, fan, object->d_name_len);
+               else
+                       dentry = ERR_PTR(ret);
+               if (IS_ERR(dentry)) {
+                       trace_cachefiles_vfs_error(object, d_inode(fan), PTR_ERR(dentry),
+                                                  cachefiles_trace_lookup_error);
+                       _debug("lookup fail %ld", PTR_ERR(dentry));
+                       goto out_unlock;
+               }
+       }
+
+       ret = cachefiles_inject_read_error();
+       if (ret == 0)
+               ret = vfs_link(object->file->f_path.dentry, &init_user_ns,
+                              d_inode(fan), dentry, NULL);
+       if (ret < 0) {
+               trace_cachefiles_vfs_error(object, d_inode(fan), ret,
+                                          cachefiles_trace_link_error);
+               _debug("link fail %d", ret);
+       } else {
+               trace_cachefiles_link(object, file_inode(object->file));
+               spin_lock(&object->lock);
+               /* TODO: Do we want to switch the file pointer to the new dentry? */
+               clear_bit(CACHEFILES_OBJECT_USING_TMPFILE, &object->flags);
+               spin_unlock(&object->lock);
+               success = true;
+       }
+
+out_dput:
+       dput(dentry);
+out_unlock:
+       inode_unlock(d_inode(fan));
+       _leave(" = %u", success);
+       return success;
+}
+
 /*
  * Look up an inode to be checked or culled.  Return -EBUSY if the inode is
  * marked in use.