ext2fs does not replay a journal; e2fsck does.  But ext2fs loaded the
journal superblock at mount, read-only or not, and kept that state.
The root filesystem is mounted read-only, e2fsck then replays the
journal and resets its superblock, and the remount read-write kept
writing at the old log head with the old sequence numbers.  After the
next crash e2fsck finds the log out of sequence at its tail and stops,
so every transaction since that boot was lost.  And a filesystem
mounted writable with a log that was never replayed had that log
overwritten.

Start the journal only when the filesystem is writable: at a writable
mount, or in diskfs_readonly_changed when it becomes writable, from the
journal superblock as it is on disk then.  When it becomes read-only,
shut the journal down like a clean unmount does, as ext4 does on a
read-only remount: the log is checkpointed and emptied and the
needs_recovery flag cleared, while the store is still writable.  The
root filesystem's remount read-only at shutdown thus leaves it clean.

While the journal is not running, look at its log at mount and on each
remount (diskfs_reload_global_state, which "fsysopts --update" reaches
while the filesystem is read-only).  If the log holds transactions that
were never replayed, keep the filesystem hard read-only, with a
warning, so that switching it to writable fails with EROFS until e2fsck
has replayed the log and a remount finds it empty.  A writable mount of
such a filesystem comes up read-only.  On a normal boot the root is
read-only until checkroot.sh has run fsck and remounted it, so nothing
changes there; in the install case, where runsystem makes the root
writable before fsck, it drops to single user when the log is not
empty.
---
 ext2fs/ext2fs.c  | 96 ++++++++++++++++++++++++++++++++++++++++--------
 ext2fs/ext2fs.h  |  8 ++++
 ext2fs/hyper.c   | 13 ++++++-
 ext2fs/journal.c | 34 +++++++++++++++++
 ext2fs/journal.h |  5 +++
 5 files changed, 139 insertions(+), 17 deletions(-)

diff --git a/ext2fs/ext2fs.c b/ext2fs/ext2fs.c
index 60af60f12..71ce4a45b 100644
--- a/ext2fs/ext2fs.c
+++ b/ext2fs/ext2fs.c
@@ -220,6 +220,73 @@ static const struct argp_child runtime_children[] =
   {{&diskfs_std_runtime_argp}, {0}};
 static struct argp runtime_argp = {options, parse_opt, 0, 0, runtime_children};
 
+/* Set while the journal's log holds transactions that were never
+   replayed.  ext2fs does not replay a journal, and writing would
+   overwrite the log, so the filesystem is kept hard read-only until a
+   remount, after e2fsck has replayed it, finds the log empty.  */
+static int journal_needs_replay;
+/* diskfs_hard_readonly before journal_needs_replay was set, for a store
+   that is read-only on its own.  */
+static int journal_saved_hard_readonly;
+
+/* Look at the journal's log, when the journal is not running: at mount,
+   and on each remount, which re-reads the superblock while the
+   filesystem is read-only.  */
+void
+ext2_check_journal_log (void)
+{
+  struct node *jnode = NULL;
+  int empty;
+
+  if (!(sblock->s_feature_compat & EXT3_FEATURE_COMPAT_HAS_JOURNAL)
+      || ext2_journal)
+    return;
+
+  empty = (!diskfs_cached_lookup (sblock->s_journal_inum, &jnode)
+          && jnode && journal_log_is_empty (jnode));
+  if (jnode)
+    diskfs_nput (jnode);
+
+  if (!empty)
+    {
+      if (!journal_needs_replay)
+       {
+         ext2_warning ("the journal needs recovery: read-only until e2fsck "
+                       "has replayed it and the filesystem is remounted");
+         journal_saved_hard_readonly = diskfs_hard_readonly;
+       }
+      journal_needs_replay = 1;
+      diskfs_hard_readonly = 1;
+    }
+  else if (journal_needs_replay)
+    {
+      journal_needs_replay = 0;
+      diskfs_hard_readonly = journal_saved_hard_readonly;
+    }
+}
+
+void
+ext2_start_journal (void)
+{
+  struct node *jnode = NULL;
+
+  if (diskfs_cached_lookup (sblock->s_journal_inum, &jnode) || !jnode)
+    {
+      ext2_warning ("cannot find the journal inode; journaling disabled");
+      return;
+    }
+
+  ext2_journal = journal_create (jnode);
+  diskfs_nput (jnode);
+  if (!ext2_journal)
+    return;
+
+  fprintf (stderr, "ext2fs: journaling enabled on %s\n", diskfs_disk_name);
+  JRNL_LOG_DEBUG ("Global Journal Initialized at %p", ext2_journal);
+  /* Before anything is journaled.  */
+  ext2_set_needs_recovery ();
+}
+
 struct argp *diskfs_runtime_argp = (struct argp *)&runtime_argp;
 
 int
@@ -261,21 +328,17 @@ main (int argc, char **argv)
                      sblock->s_journal_inum);
       JRNL_LOG_DEBUG ("[JOURNAL CHECK] s_journal_dev:  %u",
                      sblock->s_journal_dev);
-      struct node *jnode = NULL;
-      error_t err = diskfs_cached_lookup (sblock->s_journal_inum, &jnode);
-
-      if (!err && jnode)
-      {
-         ext2_journal = journal_create (jnode);
-         if (ext2_journal)
-           fprintf (stderr, "ext2fs: journaling enabled on %s\n", 
diskfs_disk_name);
-         /* Before anything is journaled.  A read-only filesystem gets
-            the flag when it becomes writable.  */
-         if (ext2_journal && !diskfs_readonly)
-           ext2_set_needs_recovery ();
-         JRNL_LOG_DEBUG ("Global Journal Initialized at %p", ext2_journal);
-         diskfs_nput(jnode);
-      }
+      /* A read-only filesystem gets its journal when it becomes
+        writable (diskfs_readonly_changed), from the journal superblock
+        as e2fsck may have left it by then.  */
+      ext2_check_journal_log ();
+      if (journal_needs_replay && !diskfs_readonly)
+       {
+         diskfs_readonly = 1;
+         diskfs_readonly_changed (1);
+       }
+      else if (!diskfs_readonly)
+       ext2_start_journal ();
     }
   else
     {
@@ -316,5 +379,8 @@ diskfs_reload_global_state (void)
 
   resume_ext2_pager ();
 
+  /* e2fsck may have replayed the journal since the last look.  */
+  ext2_check_journal_log ();
+
   return 0;
 }
diff --git a/ext2fs/ext2fs.h b/ext2fs/ext2fs.h
index 98e3d952a..d9d21d6d8 100644
--- a/ext2fs/ext2fs.h
+++ b/ext2fs/ext2fs.h
@@ -387,6 +387,14 @@ void ext2_set_needs_recovery (void);
 
 /* Clear the needs_recovery flag once the journal is empty on disk.  */
 void ext2_clear_needs_recovery (void);
+
+/* Start the journal from its superblock on disk, whose log must be
+   empty, and set the needs_recovery flag.  */
+void ext2_start_journal (void);
+
+/* Keep the filesystem read-only while the journal's log holds
+   transactions that were never replayed.  */
+void ext2_check_journal_log (void);
 
 /* ---------------------------------------------------------------- */
 /* Random stuff calculated from the super block.  */
diff --git a/ext2fs/hyper.c b/ext2fs/hyper.c
index 5035de819..a24563369 100644
--- a/ext2fs/hyper.c
+++ b/ext2fs/hyper.c
@@ -350,6 +350,12 @@ ext2_clear_needs_recovery (void)
 void
 diskfs_readonly_changed (int readonly)
 {
+  /* Like ext4 on a read-only remount: checkpoint the journal, empty the
+     log and clear the needs_recovery flag, while the store is still
+     writable.  diskfs_set_readonly has already synced.  */
+  if (readonly && ext2_journal)
+    diskfs_journal_shutdown ();
+
   allocate_mod_map ();
 
   (*(readonly ? store_set_flags : store_clear_flags)) (store, STORE_READONLY);
@@ -360,6 +366,9 @@ diskfs_readonly_changed (int readonly)
   if (!readonly && !(sblock->s_state & htole16 (EXT2_VALID_FS)))
     ext2_warning ("UNCLEANED FILESYSTEM NOW WRITABLE");
 
-  if (!readonly && ext2_journal)
-    ext2_set_needs_recovery ();
+  /* The log is empty: diskfs_hard_readonly keeps a filesystem whose
+     journal needs recovery from getting here.  */
+  if (!readonly && !ext2_journal
+      && (sblock->s_feature_compat & EXT3_FEATURE_COMPAT_HAS_JOURNAL))
+    ext2_start_journal ();
 }
diff --git a/ext2fs/journal.c b/ext2fs/journal.c
index fcff52c52..6979f7875 100644
--- a/ext2fs/journal.c
+++ b/ext2fs/journal.c
@@ -2903,6 +2903,40 @@ kjournald_thread (void *arg)
   return NULL;
 }
 
+int
+journal_log_is_empty (struct node *journal_inode)
+{
+  journal_superblock_t *jsb = NULL;
+  block_t phys = 0;
+  size_t amount = 0;
+  int empty = 0;
+  error_t err;
+
+  err = ext2_getblk (journal_inode, 0, 0, &phys);
+  if (!err && phys == 0)
+    err = EIO;
+  if (!err)
+    err = store_read (store,
+                     (store_offset_t) phys << log2_dev_blocks_per_fs_block,
+                     block_size, (void **) &jsb, &amount);
+  if (!err && amount < sizeof (journal_superblock_t))
+    err = EIO;
+
+  if (err)
+    JRNL_LOG_WARN ("Cannot read the journal superblock: %s", strerror (err));
+  else if (be32toh (jsb->s_header[0]) != JBD2_MAGIC_NUMBER
+          || (be32toh (jsb->s_header[1]) != JBD2_SUPERBLOCK_V1
+              && be32toh (jsb->s_header[1]) != JBD2_SUPERBLOCK_V2))
+    JRNL_LOG_WARN ("Bad journal superblock.");
+  else
+    /* s_start is 0 when the log is empty.  */
+    empty = jsb->s_start == 0;
+
+  if (jsb)
+    munmap (jsb, amount);
+  return empty;
+}
+
 journal_t *
 journal_create (struct node *journal_inode)
 {
diff --git a/ext2fs/journal.h b/ext2fs/journal.h
index bb2cba104..959f86455 100644
--- a/ext2fs/journal.h
+++ b/ext2fs/journal.h
@@ -53,6 +53,11 @@ typedef struct journal journal_t;
 /* Initialize the journal subsystem using the inode provided (usually Inode 
8). */
 journal_t *journal_create (struct node *journal_inode);
 
+/* Returns 1 if the log of the journal in JOURNAL_INODE is empty on disk,
+   and 0 if it holds transactions that were never replayed, or if its
+   superblock cannot be read.  JOURNAL_INODE is locked.  */
+int journal_log_is_empty (struct node *journal_inode);
+
 /**
  * A block device filter layer for the VFS pager's write path.
  * Replaces raw store_write calls to safely intercept deadlock hazards.
-- 
2.56.0


Reply via email to