Directory blocks are metadata, but ext2fs edits them through the
directory's file-pager mapping.  The disk cache, which the stop-time
sweep of journal_get_write_access copies from, never sees those edits,
so they reached disk outside the journal, through file-pager writes.
After a crash, a directory entry could then name an inode whose
creation was never committed.

Add journal entry points for a metadata block whose live bytes are
outside the disk cache.  journal_get_write_access_external reserves the
block before the edit, so that pager writes of it are intercepted until
commit.  The buffer has no image yet; jb_awaiting_data marks it, and the
sweep leaves it alone.  journal_mark_dirty_external copies the finished
block into the shadow.  The copy reads the mapping into a scratch buffer
with hurd_safe_copyin before it takes the journal lock, as a fault under
the lock would re-enter the journal through the pager.  An intercept
chunk taken during the edit receives the same image, because the chunk
backs reads of the block until its home write.
journal_release_access drops a reservation that never received an
image, and drops the block from the count of logged blocks that revoke
records rely on.  When a pager write was intercepted since the
reservation, that image is the page as it is, and it stays in the
transaction.  A released buffer is freed at once: the stop-time sweep
only takes buffers with needs_copy set, which an external reservation
clears, and an assertion keeps it that way.

journal_get_write_access and journal_get_write_access_external share
one locked helper, with a flag that selects the disk cache or the
caller as the source of the copy.  journal_write_payload asserts that
every buffer has an image.  Directory blocks are now intercepted too,
so the intercept pool grows from 512 to 2048 chunks: with that, no
write went past the log during large upgrades or tar -xf and rm -rf of
a Linux kernel tree.

diskfs_direnter_hard, diskfs_dirremove_hard and diskfs_dirrewrite_hard
reserve the block before the edit and hand it to the journal before the
mapping is dropped.  They work inside the RPC's journal handle, which
journal_thread_transaction returns, and which keeps the transaction open
from the reservation to the copy.  For EXTEND the block is allocated
when it is reserved, as diskfs_grow only makes room for it.
---
 ext2fs/dir.c     | 126 ++++++++++++++++++++++++++++++++++++--
 ext2fs/journal.c | 153 ++++++++++++++++++++++++++++++++++++++++++++---
 ext2fs/journal.h |  24 ++++++++
 3 files changed, 290 insertions(+), 13 deletions(-)

diff --git a/ext2fs/dir.c b/ext2fs/dir.c
index b90529967..09f5a7225 100644
--- a/ext2fs/dir.c
+++ b/ext2fs/dir.c
@@ -21,6 +21,7 @@
 
 #include "ext2fs.h"
 #include <libdiskfs/diskfs.h>
+#include "journal.h"
 
 #include <string.h>
 #include <stdio.h>
@@ -527,6 +528,69 @@ dirscanblock (vm_address_t blockaddr, struct node *dp, int 
idx,
   return 0;
 }
 
+/* Directory blocks are metadata, and their live bytes are the directory's
+   file-pager mapping, outside the disk cache.  An edit runs inside the
+   RPC's journal handle.  dirblock_get_write_access comes before the first
+   byte changes, so that pager writes of the block are intercepted until
+   commit.  dirblock_mark_dirty hands the finished block to the journal
+   before the mapping is dropped.  dirblock_release_access gives the block
+   up when the edit never happened.  A zero block number stands for an
+   edit outside the journal.  */
+
+/* Find the disk block of directory block IDX of DP into *BLK and add it
+   to the thread's transaction.  CREATE allocates the block, for a block
+   that diskfs_grow has only made room for.  */
+static error_t
+dirblock_get_write_access (struct node *dp, int idx, int create,
+                          block_t *blk)
+{
+  struct disknode *dn = diskfs_node_disknode (dp);
+  diskfs_transaction_t *txn;
+  error_t err;
+
+  *blk = 0;
+  txn = journal_thread_transaction ();
+  if (!txn)
+    return 0;
+
+  /* We must acquire a WRITE lock if we might allocate physical blocks.  */
+  if (create)
+    pthread_rwlock_wrlock (&dn->alloc_lock);
+  else
+    pthread_rwlock_rdlock (&dn->alloc_lock);
+  err = ext2_getblk (dp, idx, create, blk);
+  pthread_rwlock_unlock (&dn->alloc_lock);
+  if (err)
+    return err;
+
+  return journal_get_write_access_external (txn, *blk);
+}
+
+/* Hand the journal the edited directory block BLK, mapped at DATA.  */
+static error_t
+dirblock_mark_dirty (block_t blk, vm_address_t data)
+{
+  diskfs_transaction_t *txn;
+  error_t err;
+
+  if (!blk)
+    return 0;
+
+  txn = journal_thread_transaction ();
+  err = journal_mark_dirty_external (txn, blk, (const void *) data);
+  if (err)
+    journal_release_access (txn, blk);
+  return err;
+}
+
+/* Give up directory block BLK, which was never edited.  */
+static void
+dirblock_release_access (block_t blk)
+{
+  if (blk)
+    journal_release_access (journal_thread_transaction (), blk);
+}
+
 /* Following a lookup call for CREATE, this adds a node to a directory.
    DP is the directory to be modified; NAME is the name to be entered;
    NP is the node being linked in; DS is the cached information returned
@@ -545,6 +609,7 @@ diskfs_direnter_hard (struct node *dp, const char *name, 
struct node *np,
   size_t totfreed;
   error_t err;
   size_t oldsize = 0;
+  block_t blk = 0;
 
   assert_backtrace (ds->type == CREATE);
 
@@ -552,6 +617,18 @@ diskfs_direnter_hard (struct node *dp, const char *name, 
struct node *np,
 
   dp->dn_set_mtime = 1;
 
+  /* Every slot but EXTEND's lies in an allocated block.  EXTEND's block
+     is allocated once diskfs_grow has made room for it.  */
+  if (ds->stat != EXTEND)
+    {
+      err = dirblock_get_write_access (dp, ds->idx, 0, &blk);
+      if (err)
+       {
+         munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+         return err;
+       }
+    }
+
   /* Select a location for the new directory entry.  Each branch of this
      switch is responsible for setting NEW to point to the on-disk
      directory entry being written, and setting NEW->rec_len appropriately.  */
@@ -631,15 +708,23 @@ diskfs_direnter_hard (struct node *dp, const char *name, 
struct node *np,
            }
        }
 
+      err = dirblock_get_write_access (dp, ds->idx, 1, &blk);
+      if (err)
+       {
+         munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+         return err;
+       }
+
       new = (struct ext2_dir_entry_2 *) (ds->mapbuf + oldsize);
       err = hurd_safe_memset (new, 0, DIRBLKSIZ);
       if (err)
-       {
-        if (err == EKERN_MEMORY_ERROR)
-          err = ENOSPC;
-         munmap ((caddr_t) ds->mapbuf, ds->mapextent);
-         return err;
-       }
+       {
+         if (err == EKERN_MEMORY_ERROR)
+           err = ENOSPC;
+         dirblock_release_access (blk);
+         munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+         return err;
+       }
 
       dp->dn_stat.st_size = oldsize + DIRBLKSIZ;
       dp->dn_set_ctime = 1;
@@ -676,7 +761,10 @@ diskfs_direnter_hard (struct node *dp, const char *name, 
struct node *np,
   diskfs_node_disknode (dp)->info.i_flags &= ~EXT2_BTREE_FL;
   dp->dn_set_mtime = 1;
 
+  err = dirblock_mark_dirty (blk, ds->mapbuf + ds->idx * DIRBLKSIZ);
   munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+  if (err)
+    return err;
 
   if (ds->stat != EXTEND)
     {
@@ -737,11 +825,21 @@ diskfs_direnter_hard (struct node *dp, const char *name, 
struct node *np,
 error_t
 diskfs_dirremove_hard (struct node *dp, struct dirstat *ds)
 {
+  error_t err;
+  block_t blk;
+
   assert_backtrace (ds->type == REMOVE);
   assert_backtrace (ds->stat == HERE_TIS);
 
   assert_backtrace (!diskfs_readonly);
 
+  err = dirblock_get_write_access (dp, ds->idx, 0, &blk);
+  if (err)
+    {
+      munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+      return err;
+    }
+
   if (ds->preventry == 0)
     ds->entry->inode = htole32 (0);
   else
@@ -754,7 +852,10 @@ diskfs_dirremove_hard (struct node *dp, struct dirstat *ds)
   dp->dn_set_mtime = 1;
   diskfs_node_disknode (dp)->info.i_flags &= ~EXT2_BTREE_FL;
 
+  err = dirblock_mark_dirty (blk, ds->mapbuf + ds->idx * DIRBLKSIZ);
   munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+  if (err)
+    return err;
 
   /* If we are keeping count of this block, then keep the count up
      to date. */
@@ -777,16 +878,29 @@ diskfs_dirremove_hard (struct node *dp, struct dirstat 
*ds)
 error_t
 diskfs_dirrewrite_hard (struct node *dp, struct node *np, struct dirstat *ds)
 {
+  error_t err;
+  block_t blk;
+
   assert_backtrace (ds->type == RENAME);
   assert_backtrace (ds->stat == HERE_TIS);
 
   assert_backtrace (!diskfs_readonly);
 
+  err = dirblock_get_write_access (dp, ds->idx, 0, &blk);
+  if (err)
+    {
+      munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+      return err;
+    }
+
   ds->entry->inode = htole32 (np->cache_id);
   dp->dn_set_mtime = 1;
   diskfs_node_disknode (dp)->info.i_flags &= ~EXT2_BTREE_FL;
 
+  err = dirblock_mark_dirty (blk, ds->mapbuf + ds->idx * DIRBLKSIZ);
   munmap ((caddr_t) ds->mapbuf, ds->mapextent);
+  if (err)
+    return err;
 
   diskfs_file_update (dp, diskfs_synchronous);
 
diff --git a/ext2fs/journal.c b/ext2fs/journal.c
index 37139056e..2993a76c1 100644
--- a/ext2fs/journal.c
+++ b/ext2fs/journal.c
@@ -35,6 +35,7 @@
 #include <pthread.h>
 #include <mach.h>
 
+#include <hurd/sigpreempt.h>
 #include <hurd/store.h>
 #include <libdiskfs/diskfs.h>
 #include "ext2fs.h"
@@ -130,7 +131,7 @@
 #define IS_RUNNING_TID(j, tid) \
     ((j)->j_running_transaction && (j)->j_running_transaction->t_tid == (tid))
 
-#define JRNL_INTERCEPT_CAPACITY 512
+#define JRNL_INTERCEPT_CAPACITY 2048
 
 static pthread_t kjournald_tid;
 
@@ -158,6 +159,12 @@ static __thread int journal_thread_is_pager;
    is that thread's business.  Cleared by the outermost start. */
 static __thread int journal_thread_sync;
 
+/* Allocated in journal_create.  journal_mark_dirty_external copies the
+   caller's image here before it takes the journal lock.  */
+static void *journal_external_scratch;
+static pthread_mutex_t journal_external_scratch_lock =
+  PTHREAD_MUTEX_INITIALIZER;
+
 /**
  * Holds one modified block (4KB) that needs to be written to the journal.
  */
@@ -174,6 +181,10 @@ typedef struct journal_buffer
   char *jb_intercepted_data;
   uint8_t jb_is_flushing;      /* 1 if commit thread is actively flushing it. 
*/
   uint8_t jb_escaped;
+  uint8_t jb_awaiting_data;    /* Reserved by
+                                  journal_get_write_access_external, and
+                                  the shadow has no image of the block's
+                                  current owner yet. */
 } journal_buffer_t;
 
 /**
@@ -665,6 +676,7 @@ journal_alloc_buffer (journal_t *journal)
       jb->jb_is_written = 0;
       jb->jb_is_flushing = 0;
       jb->jb_escaped = 0;
+      jb->jb_awaiting_data = 0;
       goto out;
     }
   jb = calloc (1, sizeof (journal_buffer_t));
@@ -2033,10 +2045,13 @@ out:
 
 /**
  * Adds a modified filesystem block to the SPECIFIC transaction handle.
- * Defers the actual memory copy until the transaction stops.
+ * With HYDRATE set, defers the actual memory copy until the transaction
+ * stops.  With HYDRATE clear, the block's live bytes are outside the disk
+ * cache, and journal_mark_dirty_external supplies the copy.
  */
 static error_t
-journal_get_write_access_locked (diskfs_transaction_t *txn, block_t fs_blocknr)
+journal_get_write_access_locked (diskfs_transaction_t *txn,
+                                block_t fs_blocknr, int hydrate)
 {
   journal_buffer_t *jb;
   journal_buffer_t *new_jb;
@@ -2059,7 +2074,13 @@ journal_get_write_access_locked (diskfs_transaction_t 
*txn, block_t fs_blocknr)
       /* Even if it is already contained in the map we have still been
        * notified about the new change and therefore we must copy it
        * over once more when the time comes. */
-      jb->needs_copy = 1;
+      if (hydrate)
+       jb->jb_awaiting_data = 0;
+      else if (jb->needs_copy)
+       /* The shadow is due to be copied from the disk cache, which does
+        * not hold the caller's edit. */
+       jb->jb_awaiting_data = 1;
+      jb->needs_copy = hydrate;
       if (jb->jb_is_written)
        {
          /* The pager rushed this block previously, but VFS is dirtying it 
again.
@@ -2086,7 +2107,8 @@ journal_get_write_access_locked (diskfs_transaction_t 
*txn, block_t fs_blocknr)
     }
 
   /* Brand new block for us? We will definitively want to copy it over. */
-  new_jb->needs_copy = 1;
+  new_jb->needs_copy = hydrate;
+  new_jb->jb_awaiting_data = !hydrate;
   txn->t_outstanding_io++;
   journal_logged_ref_locked (ext2_journal, fs_blocknr);
 out:
@@ -2220,7 +2242,7 @@ journal_get_write_access (diskfs_transaction_t *txn, 
block_t fs_blocknr)
     }
 
   JOURNAL_LOCK (ext2_journal);
-  err = journal_get_write_access_locked (txn, fs_blocknr);
+  err = journal_get_write_access_locked (txn, fs_blocknr, 1);
   if (err)
     JRNL_LOG_WARN
       ("Didn't manage to get journal write access for block %u. (%s).",
@@ -2260,6 +2282,119 @@ journal_thread_transaction (void)
   return txn;
 }
 
+/* Adds FS_BLOCKNR, a metadata block whose live bytes are outside the disk
+   cache, to TXN before it is edited.  From here on pager writes of the
+   block are intercepted.  The caller supplies the edited image through
+   journal_mark_dirty_external, or gives the block up through
+   journal_release_access.  */
+error_t
+journal_get_write_access_external (diskfs_transaction_t *txn,
+                                  block_t fs_blocknr)
+{
+  error_t err;
+
+  if (!ext2_journal || !txn)
+    return 0;
+
+  JOURNAL_LOCK (ext2_journal);
+  err = journal_get_write_access_locked (txn, fs_blocknr, 0);
+  if (err)
+    JRNL_LOG_WARN
+      ("Didn't manage to get journal write access for block %u. (%s).",
+       fs_blocknr, strerror (err));
+  JOURNAL_UNLOCK (ext2_journal);
+  return err;
+}
+
+/* Copies DATA (block_size bytes) into TXN as the image of FS_BLOCKNR,
+   which journal_get_write_access_external added to it.  DATA may be a
+   mapping of the block, so it is read into the scratch buffer before the
+   journal lock is taken: a fault under the lock would re-enter the
+   journal through the pager.  Returns EIO when DATA cannot be read.  */
+error_t
+journal_mark_dirty_external (diskfs_transaction_t *txn, block_t fs_blocknr,
+                            const void *data)
+{
+  journal_buffer_t *jb;
+  error_t err;
+
+  if (!ext2_journal || !txn)
+    return 0;
+
+  pthread_mutex_lock (&journal_external_scratch_lock);
+  err = hurd_safe_copyin (journal_external_scratch, data, block_size);
+  if (err)
+    {
+      pthread_mutex_unlock (&journal_external_scratch_lock);
+      JRNL_LOG_WARN ("Cannot read the image of block %u. (%s).",
+                    fs_blocknr, strerror (err));
+      return EIO;
+    }
+
+  JOURNAL_LOCK (ext2_journal);
+  jb = journal_map_lookup (&txn->t_buffer_map, fs_blocknr);
+  if (jb)
+    {
+      /* Only the committing transaction and the checkpoint list are
+        notified of home writes, and TXN is neither while the caller's
+        handle is open.  */
+      assert_backtrace (!jb->jb_is_written);
+      memcpy (jb->jb_shadow_data, journal_external_scratch, block_size);
+      /* A pager write during the edit left a chunk behind.  The chunk
+        backs reads of this block until its home write, so it holds the
+        image that commits.  */
+      if (jb->jb_intercepted_data)
+       memcpy (jb->jb_intercepted_data, journal_external_scratch,
+               block_size);
+      jb->needs_copy = 0;
+      jb->jb_awaiting_data = 0;
+    }
+  else
+    JRNL_LOG_WARN ("mark_dirty_external called on unreserved block %u! "
+                  "Missing get_write_access_external?", fs_blocknr);
+  JOURNAL_UNLOCK (ext2_journal);
+  pthread_mutex_unlock (&journal_external_scratch_lock);
+  return 0;
+}
+
+/* Gives up FS_BLOCKNR when TXN has no image of it yet, so commit does not
+   log an empty shadow.  A pager write intercepted since the reservation
+   holds the page as it is, so that image stays in TXN instead.  */
+void
+journal_release_access (diskfs_transaction_t *txn, block_t fs_blocknr)
+{
+  journal_buffer_t *jb;
+
+  if (!ext2_journal || !txn)
+    return;
+
+  JOURNAL_LOCK (ext2_journal);
+  jb = journal_map_lookup (&txn->t_buffer_map, fs_blocknr);
+  if (!jb || !jb->jb_awaiting_data)
+    goto out;
+
+  if (jb->jb_intercepted_data)
+    {
+      memcpy (jb->jb_shadow_data, jb->jb_intercepted_data, block_size);
+      jb->jb_awaiting_data = 0;
+      goto out;
+    }
+
+  /* A buffer the stop-time sweep is copying must not be freed: the sweep
+     walks its own list of them unlocked.  The sweep only takes buffers
+     with needs_copy set, and an external reservation clears it.  It
+     would be set only if TXN had also reserved the block through the
+     disk cache, which a directory block never is.  */
+  assert_backtrace (!jb->needs_copy);
+  journal_map_remove (&txn->t_buffer_map, fs_blocknr);
+  if (!jb->jb_is_written && txn->t_outstanding_io > 0)
+    txn->t_outstanding_io--;
+  journal_logged_unref_locked (ext2_journal, fs_blocknr);
+  journal_free_buffer (ext2_journal, jb);
+out:
+  JOURNAL_UNLOCK (ext2_journal);
+}
+
 /* Two flags.  journal_thread_sync tells this thread's RPC tail to commit
    rather than stop.  txn->sync_needed tells the outermost stop to wake
    kjournald when t_active_threads reaches 0, for callers that never commit
@@ -2363,6 +2498,7 @@ journal_write_payload (journal_t *journal, const 
diskfs_transaction_t *txn)
   while ((jb = journal_map_iterate (&txn->t_buffer_map, &iter)) != NULL)
     {
       assert_backtrace (!jb->needs_copy);
+      assert_backtrace (!jb->jb_awaiting_data);
 
       /* If the descriptor block is full, flush the current batch first */
       if (tag_offset + sizeof (journal_block_tag_t) > block_size)
@@ -2790,7 +2926,10 @@ journal_create (struct node *journal_inode)
   j->j_min_free = j->j_max_transaction_buffers + JRNL_METADATA_OVERHEAD;
   j->j_descriptor_buf = malloc (block_size);
   j->j_commit_buf = malloc (block_size);
-  if (!j->j_descriptor_buf || !j->j_commit_buf)
+  /* Shared by every journal this translator starts.  */
+  if (!journal_external_scratch)
+    journal_external_scratch = malloc (block_size);
+  if (!j->j_descriptor_buf || !j->j_commit_buf || !journal_external_scratch)
     ext2_panic ("No RAM for commit buffers!");
 
   if (journal_load_superblock (j) != 0)
diff --git a/ext2fs/journal.h b/ext2fs/journal.h
index 06460445d..bb2cba104 100644
--- a/ext2fs/journal.h
+++ b/ext2fs/journal.h
@@ -80,6 +80,30 @@ error_t
 journal_store_read (block_t start_block, size_t length, void **buf,
                    size_t *read_amount);
 
+/**
+ * Same as journal_get_write_access, for a metadata block whose live bytes
+ * are outside the disk cache, such as a directory block edited through
+ * the directory's file pager.  The journal makes no copy of its own; the
+ * caller supplies the edited image through journal_mark_dirty_external,
+ * or gives the block up through journal_release_access.
+ */
+error_t journal_get_write_access_external (diskfs_transaction_t *txn,
+                                          block_t fs_blocknr);
+
+/**
+ * Hands the journal the edited image of FS_BLOCKNR, block_size bytes at
+ * DATA.  DATA may be a mapping of the block.  Returns EIO when DATA
+ * cannot be read.
+ */
+error_t journal_mark_dirty_external (diskfs_transaction_t *txn,
+                                    block_t fs_blocknr, const void *data);
+
+/**
+ * Gives up FS_BLOCKNR, reserved with journal_get_write_access_external,
+ * when the transaction has no image of it yet.
+ */
+void journal_release_access (diskfs_transaction_t *txn, block_t fs_blocknr);
+
 /**
  * Records a range of deleted blocks so they can be unpinned from older
  * checkpoint lists AFTER this transaction safely commits.  Returns 1 if
-- 
2.56.0


Reply via email to