Hello everyone,

Over the past week, I have been working on creating a prototype for a
journal inside ext2fs which is fully Linux compatible (binary JBD2
compatible). This enables standard Linux tools (e2fsck, tune2fs, debugfs,
etc.) to work seamlessly with Hurd partitions.

This means one can mount a Hurd image from Linux and fix any issues with
the drive using standard journaling tools if the need arises. While this is
currently a prototype with polish still required, it is functional.

Key Features:
    Log Replay: The driver writes JBD2-compliant transactions. I have
verified that after a hard crash of the Hurd guest, a Linux host running
e2fsck correctly replays the journal and restores filesystem metadata
consistency.
    Continuous Operation: The driver handles ring-buffer wrap-around and
checkpoints correctly. I have tested it with sustained loads (50,000+
transaction loops) without deadlocks or corruption.
    Crash Safety: I have verified via "sabotage tests" (modifying the disk
offline after a crash) that the journal accurately restores the correct
metadata state.
    Lightweight: The implementation consists of only a few new files and
~800 lines of code.

Implementation Details: I had to add one weak symbol in diskfs.h. For this
prototype, I am using a "safe sync" strategy (flushing the pager buckets)
because there is no explicit write barrier RPC in Mach to ensure strict
ordering between transaction commits and superblock updates.

If there is interest from the Hurd community in integrating this, I am
willing to work on adding a proper write barrier RPC into Mach and
productionizing the code adding whatever else is needed. There is a path to
a safer, more atomic Hurd and also retiring fsck fully if we integrate a
version of this.

How to Test It: To test this on your own images, you simply need to create
the journal using tune2fs. You will need the byte offset of your ext2
partition.
(WARNING: Please apply the patch first, otherwise your ext2 will refuse to
work with your image!!!)

The easiest way (for me) to get the offset is this:
)> parted debian-hurd-amd64-20251105.img unit B print
Output example:
Number  Start        End          Size         Type      File system
Flags
 1      1048576B     1000341503B  999292928B   primary   linux-swap(v1)
 swap
 2      1001389056B  9564061695B  8562672640B  extended                  lba
 5      1001390080B  9564061695B  8562671616B  logical   ext2
(the 1001390080B is the ext2 partition for me.)

now armed with that knowledge i do:
> sudo modprobe nbd max_part=8
> sudo qemu-nbd --connect=/dev/nbd0 --offset=1001390080 --format=raw
debian-hurd-amd64-20251105.img
> sudo tune2fs -j /dev/nbd0
> sudo qemu-nbd --disconnect /dev/nbd0

That's it! tune2fs has now allocated an empty journal inside your Hurd
filesystem.

The next time you boot into Hurd, you might notice a warning: ext2fs:
part:5:device:wd0: warning: mounting ext3 filesystem as ext2. This is
normal.

All tools will now recognize the partition as ext3. Even mounting the image
inside Linux will automatically replay the journal and fix any issues if
needed. Besides that everything should work seamlessly.

Let me know if this is something the community would like to see developed
further.

Thanks,
Milos
From e77b197789ee72be42304e705254abd4f35c0aa4 Mon Sep 17 00:00:00 2001
From: Milos Nikic <[email protected]>
Date: Sat, 17 Jan 2026 22:49:00 -0800
Subject: [PATCH] ext2: Prototype: Add JDB2 binary compliant journal

This prototype adds journal driver that captures some node
metadata inside a binary compliant place with binary
compliant content of the JBD2.

This way standard linux tools (e2fsck, debugfs, tune2fs etc)
all work and can understand and replay ext2 journal.
In fact all of them now recognize ext2 as ext3.
---
 ext2fs/Makefile           |   2 +-
 ext2fs/ext2_fs.h          |   3 +-
 ext2fs/ext2fs.c           |  24 ++
 ext2fs/ext2fs.h           |   4 +
 ext2fs/hyper.c            |   9 +
 ext2fs/inode.c            |  41 +-
 ext2fs/jbd2_format.h      | 101 +++++
 ext2fs/journal.c          | 843 ++++++++++++++++++++++++++++++++++++++
 ext2fs/journal.h          |  62 +++
 ext2fs/pager.c            |  31 ++
 libdiskfs/diskfs.h        |   5 +
 libdiskfs/node-modified.c |  28 ++
 libdiskfs/priv.h          |   6 +
 13 files changed, 1153 insertions(+), 6 deletions(-)
 create mode 100644 ext2fs/jbd2_format.h
 create mode 100644 ext2fs/journal.c
 create mode 100644 ext2fs/journal.h
 create mode 100644 libdiskfs/node-modified.c

diff --git a/ext2fs/Makefile b/ext2fs/Makefile
index 0c2f4a24..a2b0f1ee 100644
--- a/ext2fs/Makefile
+++ b/ext2fs/Makefile
@@ -22,7 +22,7 @@ makemode := server
 target = ext2fs
 SRCS = balloc.c dir.c ext2fs.c getblk.c hyper.c ialloc.c \
        inode.c pager.c pokel.c truncate.c storeinfo.c msg.c xinl.c \
-       xattr.c
+       xattr.c journal.c
 OBJS = $(SRCS:.c=.o)
 HURDLIBS = diskfs pager iohelp fshelp store ports ihash shouldbeinlibc
 LDLIBS = -lpthread $(and $(HAVE_LIBBZ2),-lbz2) $(and $(HAVE_LIBZ),-lz)
diff --git a/ext2fs/ext2_fs.h b/ext2fs/ext2_fs.h
index daa49543..a3a00c95 100644
--- a/ext2fs/ext2_fs.h
+++ b/ext2fs/ext2_fs.h
@@ -474,7 +474,8 @@ struct ext2_super_block {
 #define EXT2_FEATURE_INCOMPAT_ANY		0xffffffff
 
 #define EXT2_FEATURE_COMPAT_SUPP	EXT2_FEATURE_COMPAT_EXT_ATTR
-#define EXT2_FEATURE_INCOMPAT_SUPP	EXT2_FEATURE_INCOMPAT_FILETYPE
+#define EXT2_FEATURE_INCOMPAT_SUPP (EXT2_FEATURE_INCOMPAT_FILETYPE | \
+                                    EXT3_FEATURE_INCOMPAT_RECOVER)
 #define EXT2_FEATURE_RO_COMPAT_SUPP	(EXT2_FEATURE_RO_COMPAT_SPARSE_SUPER| \
 					 EXT2_FEATURE_RO_COMPAT_LARGE_FILE| \
 					 EXT2_FEATURE_RO_COMPAT_BTREE_DIR)
diff --git a/ext2fs/ext2fs.c b/ext2fs/ext2fs.c
index 3836bdf6..37a6cc2a 100644
--- a/ext2fs/ext2fs.c
+++ b/ext2fs/ext2fs.c
@@ -32,6 +32,7 @@
 #include <hurd/store.h>
 #include <version.h>
 #include "ext2fs.h"
+#include "journal.h"
 
 /* ---------------------------------------------------------------- */
 
@@ -80,6 +81,7 @@ unsigned long desc_per_block;
 unsigned long addr_per_block;
 
 unsigned long groups_count;
+struct journal *ext2_journal = NULL;
 
 /* ---------------------------------------------------------------- */
 
@@ -251,6 +253,28 @@ main (int argc, char **argv)
     ext2_panic ("no root node!");
   pthread_mutex_unlock (&diskfs_root_node->lock);
 
+  if (sblock->s_feature_compat & EXT3_FEATURE_COMPAT_HAS_JOURNAL)
+    {
+      JRNL_LOG_DEBUG ("\n[JOURNAL CHECK] >>> Inode 8 DETECTED! <<<");
+      JRNL_LOG_DEBUG ("[JOURNAL CHECK] s_journal_inum: %u (Expected: 8)",
+		      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 (8, &jnode);
+
+      if (!err && jnode) 
+      {
+	  ext2_journal = journal_create (jnode);
+	  JRNL_LOG_DEBUG ("Global Journal Initialized at %p", ext2_journal);
+	  diskfs_nput(jnode); 
+      }
+    }
+  else
+    {
+      JRNL_LOG_DEBUG ("\n[JOURNAL CHECK] No Journal flag found.");
+    }
+
   /* Now that we are all set up to handle requests, and diskfs_root_node is
      set properly, it is safe to export our fsys control port to the
      outside world.  */
diff --git a/ext2fs/ext2fs.h b/ext2fs/ext2fs.h
index 62ee9f77..5d9f4ee4 100644
--- a/ext2fs/ext2fs.h
+++ b/ext2fs/ext2fs.h
@@ -282,6 +282,10 @@ extern struct ext2_super_block *sblock;
 /* True if sblock has been modified.  */
 extern int sblock_dirty;
 
+/* Forward declaration prevents circular dependency with journal.h */
+struct journal; 
+extern struct journal *ext2_journal;
+
 /* Where the super-block is located on disk (at min-block 1).  */
 #define SBLOCK_BLOCK	1	/* Default location, second 1k block.  */
 #define SBLOCK_SIZE	(sizeof (struct ext2_super_block))
diff --git a/ext2fs/hyper.c b/ext2fs/hyper.c
index 2af7e870..821a4781 100644
--- a/ext2fs/hyper.c
+++ b/ext2fs/hyper.c
@@ -190,11 +190,20 @@ diskfs_set_hypermetadata (int wait, int clean)
     /* The filesystem is clean, so we need to set the clean flag.  */
     {
       sblock->s_state |= htole16 (EXT2_VALID_FS);
+      if (ext2_journal)
+       {
+	  sblock->s_feature_incompat &= htole32(~EXT3_FEATURE_INCOMPAT_RECOVER);
+       }
       sblock_dirty = 1;
     }
   else if (!clean && (sblock->s_state & htole16 (EXT2_VALID_FS)))
     /* The filesystem just became dirty, so clear the clean flag.  */
     {
+      if (ext2_journal && 
+          !(sblock->s_feature_incompat & htole32(EXT3_FEATURE_INCOMPAT_RECOVER)))
+	{
+           sblock->s_feature_incompat |= htole32(EXT3_FEATURE_INCOMPAT_RECOVER);
+        }
       sblock->s_state &= htole16 (~EXT2_VALID_FS);
       sblock_dirty = 1;
       wait = 1;
diff --git a/ext2fs/inode.c b/ext2fs/inode.c
index dc309ac8..84c1d56b 100644
--- a/ext2fs/inode.c
+++ b/ext2fs/inode.c
@@ -20,6 +20,7 @@
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */
 
 #include "ext2fs.h"
+#include "journal.h"
 #include <string.h>
 #include <unistd.h>
 #include <stdio.h>
@@ -524,14 +525,38 @@ write_all_disknodes (void)
 void
 diskfs_write_disknode (struct node *np, int wait)
 {
+  if (ext2_journal)
+  {
+    journal_start_transaction(ext2_journal);
+  }
+
   struct ext2_inode *di = write_node (np);
+
   if (di)
+  {
+    if (ext2_journal)
     {
-      if (wait)
-	sync_global_ptr (di, 1);
-      else
-	record_global_poke (di);
+      unsigned long ino = np->dn_stat.st_ino;
+      unsigned long group = inode_group_num(ino);
+      block_t table_start = le32toh (group_desc(group)->bg_inode_table);
+      unsigned long inodes_per_group = le32toh (sblock->s_inodes_per_group);
+      unsigned long inode_index = (ino - 1) % inodes_per_group;
+      unsigned long byte_offset = inode_index * le16toh (sblock->s_inode_size);
+      block_t block_num = table_start + (byte_offset / block_size);
+      void *block_ptr = bptr (block_num);
+      journal_dirty_block(ext2_journal, block_num, block_ptr);
     }
+
+    if (wait)
+      sync_global_ptr (di, 1);
+    else
+      record_global_poke (di);
+  }
+
+  if (ext2_journal)
+  {
+    journal_stop_transaction(ext2_journal);
+  }
 }
 
 /* Set *ST with appropriate values to reflect the current state of the
@@ -862,3 +887,11 @@ diskfs_shutdown_soft_ports (void)
   /* Should initiate termination of internally held pager ports
      (the only things that should be soft) XXX */
 }
+
+void
+diskfs_notify_change (struct node *np)
+{
+    /* If journaling is active, capture this metadata change immediately */
+    if (ext2_journal)
+        diskfs_node_update (np, 0);
+}
diff --git a/ext2fs/jbd2_format.h b/ext2fs/jbd2_format.h
new file mode 100644
index 00000000..a74a19d2
--- /dev/null
+++ b/ext2fs/jbd2_format.h
@@ -0,0 +1,101 @@
+/* JBD2 Standard On-Disk Layout 
+
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Written by Milos Nikic.
+
+   Converted for ext2fs by Miles Bader <[email protected]>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation; either version 2, or (at
+   your option) any later version.
+
+   This program 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
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */
+
+#ifndef _JBD2_FORMAT_H
+#define _JBD2_FORMAT_H
+
+#include <stdint.h>
+
+/** JBD2 Magic Numbers 
+ * If a block starts with this, it's a metadata block.
+ */
+#define JBD2_MAGIC_NUMBER 0xC03B3998U
+
+/* Block Types */
+#define JBD2_DESCRIPTOR_BLOCK 1
+#define JBD2_COMMIT_BLOCK     2
+#define JBD2_SUPERBLOCK_V1    3
+#define JBD2_SUPERBLOCK_V2    4
+#define JBD2_REVOKE_BLOCK     5
+
+#define JBD2_PACKED __attribute__((packed))
+
+/* * 
+ * The Journal Superblock 
+ * Lives at the very start of the journal partition (typically Inode 8).
+ */
+typedef struct JBD2_PACKED journal_superblock_s
+{
+  uint32_t s_header[3];		/* Standard header (magic, type, etc) */
+  uint32_t s_blocksize;		/* Journal device blocksize */
+  uint32_t s_maxlen;		/* Total blocks in journal file */
+  uint32_t s_first;		/* First block of log information */
+
+  uint32_t s_sequence;		/* First commit ID expected in log */
+  uint32_t s_start;		/* Block number of start of log */
+
+  uint32_t s_errno;		/* Error value, if any */
+
+  uint32_t s_feature_compat;
+  uint32_t s_feature_incompat;
+  uint32_t s_feature_ro_compat;
+
+  uint8_t s_uuid[16];		/* 128-bit uuid for journal */
+  uint32_t s_nr_users;		/* Nr of filesystems sharing log */
+  uint32_t s_dynsuper;		/* Blocknr of dynamic superblock copy */
+  uint32_t s_max_transaction;	/* Limit of handle size */
+  uint32_t s_max_trans_data;	/* Limit of data blocks per trans */
+  uint32_t s_checksum_type;	/* checksum type */
+  uint8_t s_padding2[42 * 4];
+  uint32_t s_checksum;		/* crc32c(superblock) */
+  uint8_t s_users[16 * 48];	/* ids of all filesystems sharing log */
+} journal_superblock_t;
+
+_Static_assert (sizeof (journal_superblock_t) == 1024,
+		"JBD2 Superblock size mismatch! Check padding.");
+
+/* * The Standard Header
+ * Every metadata block (Descriptor, Commit) starts with this.
+ */
+typedef struct JBD2_PACKED journal_header_s
+{
+  uint32_t h_magic;		/* 0xC03B3998 */
+  uint32_t h_blocktype;		/* Descriptor, Commit, etc. */
+  uint32_t h_sequence;		/* The Transaction ID */
+} journal_header_t;
+
+/* * The Block Tag
+ * Describes a data block that follows.
+ * Structure: [Descriptor Block] [Tag 1] [Tag 2] ... [Data 1] [Data 2] ...
+ */
+typedef struct JBD2_PACKED journal_block_tag_s
+{
+  uint32_t t_blocknr;		/* The 32-bit physical block number */
+  uint32_t t_flags;		/* See flags below */
+} journal_block_tag_t;
+
+/* Flags for the Block Tag */
+#define JBD2_FLAG_ESCAPE    1	/* The data block starts with magic number (escaped) */
+#define JBD2_FLAG_SAME_UUID 2	/* (Not needed for us usually) */
+#define JBD2_FLAG_DELETED   4	/* Block was deleted */
+#define JBD2_FLAG_LAST_TAG  8	/* This is the last tag in this descriptor block */
+
+#endif
diff --git a/ext2fs/journal.c b/ext2fs/journal.c
new file mode 100644
index 00000000..06f09e2f
--- /dev/null
+++ b/ext2fs/journal.c
@@ -0,0 +1,843 @@
+/* JBD2 binary compliant journal driver. 
+
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Written by Milos Nikic.
+
+   Converted for ext2fs by Miles Bader <[email protected]>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation; either version 2, or (at
+   your option) any later version.
+
+   This program 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
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "ext2fs.h"
+#include "jbd2_format.h"
+#include "journal.h"
+
+static pthread_t kjournald_tid;
+
+extern void journal_sync_everything (void);
+
+/**
+ * Represents one modified block (4KB) that needs to be written to the journal.
+ */
+typedef struct journal_buffer
+{
+  block_t jb_blocknr;			/* The physical block number on the filesystem */
+  void *jb_shadow_data;			/* 4KB Copy of the data to be logged */
+  struct journal_buffer *jb_next;	/* Linked list next pointer */
+  uint32_t jb_log_spot;
+} journal_buffer_t;
+
+/* The state of a transaction in memory */
+typedef enum
+{
+  T_RUNNING,				/* Accepting new handles/buffers */
+  T_LOCKED,				/* Locked, no new handles, waiting for updates to finish */
+  T_FLUSHING,				/* Writing to the journal ring buffer */
+  T_COMMIT,				/* Writing the commit block */
+  T_FINISHED				/* Done, waiting to be checkpointed */
+} transaction_state_t;
+
+/* The Transaction Object */
+struct journal_transaction
+{
+  uint32_t t_tid;			/* Transaction ID (Sequence Number) */
+  transaction_state_t t_state;
+
+  /* The Log Position */
+  uint32_t t_log_start;			/* Where this transaction starts in the ring */
+  uint32_t t_nr_blocks;			/* How many blocks it consumes */
+
+  uint32_t t_updates;			/* Refcount: How many threads are in this transaction? */
+  
+  /* The Payload (The Shadow Buffers) */
+  journal_buffer_t *t_buffers;		/* Linked List of dirty blocks */
+  int t_buffer_count;
+
+  /* Timing/Debug */
+  long t_start_time;
+};
+
+/* The Simple Mapper (Virtual -> Physical) */
+typedef struct journal_map
+{
+  block_t *phys_blocks;			/* The 64KB array we malloc'd */
+  uint32_t total_blocks;		/* 16384 */
+  struct node *inode;			/* Inode 8 (for keeping ref) */
+} journal_map_t;
+
+/* The Grand Abstraction */
+typedef struct journal
+{
+  /* The Physics of it (The Map) */
+  journal_map_t map;
+
+  /* The Ring Buffer State (The Logic) */
+  uint32_t j_head;			/* Where we are writing next */
+  uint32_t j_tail;			/* The oldest live transaction (checkpoint) */
+  uint32_t j_first;			/* First block of data (usually 1, after SB) */
+  uint32_t j_last;			/* Last block of data */
+  uint32_t j_free;			/* How many blocks left? */
+
+  /* The Sequence Counter */
+  uint32_t j_transaction_sequence;	/* Monotonic ID (e.g. 500, 501...) */
+
+  pthread_mutex_t j_state_lock;	/* Protects the pointers below */
+  /* The Transactions */
+  struct journal_transaction *j_running_transaction;	/* Currently filling */
+  struct journal_transaction *j_committing_transaction;	/* Flushing to journal */
+
+} journal_t;
+
+static void
+init_map (journal_t *journal, struct node *jnode)
+{
+  journal->map.total_blocks = jnode->allocsize / block_size;
+  journal->map.phys_blocks =
+    malloc (journal->map.total_blocks * sizeof (block_t));
+  if (!journal->map.phys_blocks)
+    ext2_panic ("No RAM for journal map");
+
+  for (uint32_t i = 0; i < journal->map.total_blocks; i++)
+    {
+      block_t phys = 0;
+
+      /* ext2_getblk handles the indirect blocks/fragmentation. */
+      error_t err = ext2_getblk (jnode, i, 0, &phys);
+
+      if (err || phys == 0)
+	{
+	  ext2_panic ("[JOURNAL] Gap in journal file at logical %u!", i);
+	}
+
+      journal->map.phys_blocks[i] = phys;
+    }
+
+  journal->map.inode = jnode;
+}
+
+static void
+destroy_map (journal_t *journal)
+{
+  free (journal->map.phys_blocks);
+  journal->map.total_blocks = 0;
+  if (journal->map.inode)
+    diskfs_nput (journal->map.inode);
+}
+
+static void *
+kjournald_thread (void *arg)
+{
+  journal_t *journal = (journal_t *) arg;
+  while (1)
+    {
+      sleep (5);
+
+      if (journal->j_running_transaction)
+	{
+	  JRNL_LOG_DEBUG ("Woke the journal up:\n"
+			  " - Sequence: %u\n"
+			  " - Start (Head): %u\n"
+			  " - First Data Block: %u\n"
+			  " - Total Blocks: %u",
+			  journal->j_transaction_sequence, journal->j_head,
+			  journal->j_first, journal->j_last);
+
+	  // "Lightweight" commit - only writes the log
+	  journal_commit_transaction (journal);
+	}
+    }
+  return NULL;
+}
+
+static block_t
+get_journal_phys_block (journal_t *journal, uint32_t idx)
+{
+  assert_backtrace (idx < journal->map.total_blocks);
+  return journal->map.phys_blocks[idx];
+}
+
+/* Centralized logic to map FS Block -> Store Offset */
+static store_offset_t
+journal_map_offset (journal_t *journal, uint32_t logical_idx)
+{
+  block_t phys_block = get_journal_phys_block (journal, logical_idx);
+  return phys_block << (log2_block_size - store->log2_block_size);
+}
+
+/**
+ * Reads the JBD2 superblock (Block 0 of the journal file)
+ * and initializes the journal_t state.
+ */
+error_t
+journal_load_superblock (journal_t *journal)
+{
+  error_t err;
+  journal_superblock_t *jsb;
+  void *buf;
+
+  buf = malloc (block_size);
+  if (!buf)
+    return ENOMEM;
+
+  /* journal_read_block handles all the store_read/vm_deallocate logic internally */
+  err = journal_read_block (journal, 0, buf);
+
+  if (err)
+    {
+      JRNL_LOG_DEBUG ("[JOURNAL] Failed to read SB. Err: %s", strerror (err));
+      free (buf);
+      return err;
+    }
+
+  /* Interpret as JBD2 Superblock and verify */
+  jsb = (journal_superblock_t *) buf;
+  uint32_t magic = be32toh (jsb->s_header[0]);
+  uint32_t type = be32toh (jsb->s_header[1]);
+  if (magic != JBD2_MAGIC_NUMBER)
+    {
+      ext2_warning ("[JOURNAL] Invalid Magic: %x (Expected %x)", magic,
+		    JBD2_MAGIC_NUMBER);
+      free (buf);
+      return EINVAL;
+    }
+  if (type != JBD2_SUPERBLOCK_V2 && type != JBD2_SUPERBLOCK_V1)
+    {
+      ext2_warning ("[JOURNAL] Invalid SB Type: %d", type);
+      free (buf);
+      return EINVAL;
+    }
+
+  /* Populate Journal Struct */
+  journal->j_first = be32toh (jsb->s_first);
+  journal->j_last = be32toh (jsb->s_maxlen);
+  journal->j_head = be32toh (jsb->s_start);
+  journal->j_tail = journal->j_head;
+  journal->j_transaction_sequence = be32toh (jsb->s_sequence);
+
+  /* Validate blocksize */
+  uint32_t j_bsize = be32toh (jsb->s_blocksize);
+  if (j_bsize != block_size)
+    {
+      ext2_warning ("[JOURNAL] Blocksize mismatch! Journal: %u, FS: %u",
+		    j_bsize, block_size);
+      free (buf);
+      return EINVAL;
+    }
+
+  JRNL_LOG_DEBUG ("Loaded JBD2 Superblock:\n"
+		  " - Sequence: %u\n"
+		  " - Start (Head): %u\n"
+		  " - First Data Block: %u\n"
+		  " - Total Blocks: %u",
+		  journal->j_transaction_sequence, journal->j_head,
+		  journal->j_first, journal->j_last);
+
+  free (buf);
+  return 0;
+}
+
+error_t
+journal_update_superblock (journal_t *journal, uint32_t sequence,
+			   uint32_t start)
+{
+  void *buf;
+  journal_superblock_t *jsb;
+  error_t err;
+
+  buf = malloc (block_size);
+  if (!buf)
+    return ENOMEM;
+
+  /* Read existing SB to preserve UUID/Features */
+  err = journal_read_block (journal, 0, buf);
+  if (err)
+    {
+      JRNL_LOG_DEBUG ("[SB] Critical: Failed to read SB. Aborting update.");
+      free (buf);
+      return err;
+    }
+
+  jsb = (journal_superblock_t *) buf;
+
+  /* Sanity Check Magic (Don't overwrite garbage with garbage) */
+  if (jsb->s_header[0] != htobe32 (JBD2_MAGIC_NUMBER))
+    {
+      JRNL_LOG_DEBUG ("[SB] Critical: On-disk magic invalid. Aborting.");
+      free (buf);
+      return EIO;
+    }
+
+  /* Update Dynamic Fields */
+  jsb->s_sequence = htobe32 (sequence);
+  jsb->s_start = htobe32 (start);
+
+  /* Ensure maxlen matches the map (self-correction) */
+  jsb->s_maxlen = htobe32 (journal->map.total_blocks);
+
+  JRNL_LOG_DEBUG ("[SB] Updating: Seq %u, Head %u", sequence, start);
+
+  /* Write Back */
+  err = journal_write_block (journal, 0, buf);
+
+  free (buf);
+  return err;
+}
+
+journal_t *
+journal_create (struct node *journal_inode)
+{
+  journal_t *j = calloc (1, sizeof (struct journal));
+  if (!j)
+    ext2_panic ("[JOURNAL] Cannot create journal struct.");
+
+  init_map (j, journal_inode);
+
+  /* Take ownership of the inode ref */
+  diskfs_nref (journal_inode);
+
+  /* Set generic defaults (Will be overwritten by Superblock read later) */
+  j->j_first = 1;		/* Skip SB block by default */
+  j->j_last = j->map.total_blocks - 1;
+  j->j_free = j->j_last - j->j_first;
+
+  if (journal_load_superblock (j) != 0)
+    {
+      ext2_panic ("[JOURNAL] Failed to load superblock!");
+    }
+  pthread_mutex_init (&j->j_state_lock, NULL);
+
+  if (pthread_create (&kjournald_tid, NULL, kjournald_thread, j) != 0)
+    {
+      JRNL_LOG_DEBUG ("Failed to create a flusher thread.");
+    }
+  else
+    {
+      JRNL_LOG_DEBUG ("Created flusher thread.");
+    }
+  return j;
+}
+
+/**
+ * Writes a full filesystem block (4096 bytes) to the journal.
+ * Handles the Logical -> Physical -> Store Offset conversion.
+ */
+error_t
+journal_write_block (journal_t *journal, uint32_t logical_idx, void *data)
+{
+  store_offset_t offset;
+  size_t written_amount = 0;
+  error_t err;
+
+  /* Safety Check */
+  if (logical_idx >= journal->map.total_blocks)
+    {
+      ext2_warning ("[JOURNAL] Write out of bounds! Index: %u, Max: %u",
+		    logical_idx, journal->map.total_blocks);
+      return EINVAL;
+    }
+
+  offset = journal_map_offset (journal, logical_idx);
+  err = store_write (store, offset, data, block_size, &written_amount);
+
+  if (err)
+    {
+      JRNL_LOG_DEBUG
+	("[JOURNAL] Write failed at logical %u. Err: %s",
+	 logical_idx, strerror (err));
+      return err;
+    }
+
+  if (written_amount != block_size)
+    {
+      JRNL_LOG_DEBUG ("[JOURNAL] Short write! Wanted %u, wrote %lu",
+		      block_size, written_amount);
+      return EIO;
+    }
+
+  return 0;
+}
+
+/**
+ * Reads a full filesystem block (4096 bytes) from the journal into 'out_buf'.
+ * out_buf must be at least block_size bytes.
+ */
+error_t
+journal_read_block (journal_t *journal, uint32_t logical_idx, void *out_buf)
+{
+  store_offset_t offset;
+  size_t read_amount = 0;
+  void *temp_buf = NULL;
+  error_t err;
+
+  if (!out_buf)
+    return EINVAL;
+
+  if (logical_idx >= journal->map.total_blocks)
+    {
+      ext2_warning ("[JOURNAL] Read out of bounds! Index: %u, Max: %u",
+		    logical_idx, journal->map.total_blocks);
+      return EINVAL;
+    }
+
+  offset = journal_map_offset (journal, logical_idx);
+
+  err = store_read (store, offset, block_size, &temp_buf, &read_amount);
+
+  if (err)
+    {
+      if (temp_buf)
+	vm_deallocate (mach_task_self (), (vm_address_t) temp_buf,
+		       read_amount);
+      return err;
+    }
+
+  if (read_amount != block_size)
+    {
+      JRNL_LOG_DEBUG ("[JOURNAL] Short read! Wanted %u, got %lu", block_size,
+		      read_amount);
+      if (temp_buf)
+	vm_deallocate (mach_task_self (), (vm_address_t) temp_buf,
+		       read_amount);
+      return EIO;
+    }
+
+  memcpy (out_buf, temp_buf, block_size);
+  vm_deallocate (mach_task_self (), (vm_address_t) temp_buf, read_amount);
+  return 0;
+}
+
+void
+journal_destroy (journal_t *journal)
+{
+  destroy_map (journal);
+  pthread_mutex_destroy (&journal->j_state_lock);
+
+  free (journal);
+}
+
+/**
+ * Called when we are running out of space.
+ * Since we do a version of sync() on every commit, we can safely declare all 
+ * previous transactions "checkpointed" and reset the log.
+ */
+static void
+journal_force_checkpoint (journal_t *journal, uint32_t current_tid)
+{
+  JRNL_LOG_DEBUG
+    ("[CHECKPOINT] Journal Full! Forcing Global Sync & Reset...");
+
+  journal_sync_everything ();
+
+  journal->j_tail = journal->j_head;
+  journal->j_free = journal->j_last - journal->j_first;
+
+  /* Update Superblock */
+  journal_update_superblock (journal, current_tid,
+			     journal->j_head);
+  JRNL_LOG_DEBUG ("[CHECKPOINT] after superblock...");
+
+  JRNL_LOG_DEBUG
+    ("[CHECKPOINT] Reset complete. Tail moved to %u. Free space restored.",
+     journal->j_tail);
+  sync_global (1);		// Make sure the superblock hit the disk
+}
+
+static uint32_t
+journal_next_log_block (journal_t *journal)
+{
+  journal->j_head++;
+  if (journal->j_head > journal->j_last)
+    {
+      journal->j_head = journal->j_first;
+    }
+  journal->j_free--;
+  return journal->j_head;
+}
+
+/* Helper to calculate where the next block is, handling the ring buffer wrap.
+   Must match journal_next_log_block logic exactly! */
+static uint32_t
+journal_next_after (journal_t *journal, uint32_t current_block)
+{
+  uint32_t next = current_block + 1;
+  /* Wrap around to the first usable block */
+  if (next > journal->j_last)
+    next = journal->j_first;
+  return next;
+}
+
+error_t
+journal_commit_transaction (journal_t *journal)
+{
+  struct journal_transaction *txn;
+  void *descriptor_buf = NULL, *commit_buf = NULL;
+  journal_header_t *hdr;
+  error_t err;
+  uint32_t descriptor_loc, commit_loc;
+  uint32_t tag_offset;
+  journal_buffer_t *jb;
+
+  pthread_mutex_lock (&journal->j_state_lock);
+  txn = journal->j_running_transaction;
+
+  if (!txn || txn->t_state != T_RUNNING)
+    {
+      pthread_mutex_unlock (&journal->j_state_lock);
+      return EINVAL;
+    }
+
+  /* Detach from global state (Stop NEW writers) */
+  journal->j_running_transaction = NULL;
+  txn->t_state = T_LOCKED;
+
+  journal->j_transaction_sequence = txn->t_tid + 1;
+
+  /* DRAIN WRITERS */
+  while (txn->t_updates > 0)
+    {
+      JRNL_LOG_DEBUG ("[COMMIT] Waiting for %u active handles...",
+		      txn->t_updates);
+      pthread_mutex_unlock (&journal->j_state_lock);
+      sched_yield ();
+      pthread_mutex_lock (&journal->j_state_lock);
+    }
+
+  txn->t_state = T_FLUSHING;
+
+  /* RESERVATION */
+  if (journal->j_free < txn->t_nr_blocks + 50)
+    {
+      if (txn->t_nr_blocks > (journal->j_last - journal->j_first))
+	{
+	  ext2_panic ("[COMMIT] Transaction too huge (%u) for journal!",
+		      txn->t_nr_blocks);
+	}
+
+      journal_force_checkpoint (journal, txn->t_tid);
+    }
+  /* Reserve Descriptor */
+  descriptor_loc = journal_next_log_block (journal);
+
+  /* Reserve Data */
+  jb = txn->t_buffers;
+  uint32_t expected = journal_next_after (journal, descriptor_loc);
+
+  while (jb)
+    {
+      jb->jb_log_spot = journal_next_log_block (journal);
+      if (jb->jb_log_spot != expected)
+	{
+	  ext2_panic ("[COMMIT] Layout Logic Error! Expected %u got %u",
+		      expected, jb->jb_log_spot);
+	}
+      expected = journal_next_after (journal, expected);
+
+      jb = jb->jb_next;
+    }
+
+  /* Reserve Commit */
+  commit_loc = journal_next_log_block (journal);
+  if (commit_loc != expected)
+    {
+      ext2_panic ("[COMMIT] Layout Logic Error! Commit expected %u got %u",
+		  expected, commit_loc);
+    }
+
+  pthread_mutex_unlock (&journal->j_state_lock);
+
+
+  /* WRITE DESCRIPTOR FIRST */
+  descriptor_buf = calloc (1, block_size);
+  if (!descriptor_buf)
+    {
+      err = ENOMEM;
+      goto out;
+    }
+
+  hdr = (journal_header_t *) descriptor_buf;
+  hdr->h_magic = htobe32 (JBD2_MAGIC_NUMBER);
+  hdr->h_blocktype = htobe32 (JBD2_DESCRIPTOR_BLOCK);
+  hdr->h_sequence = htobe32 (txn->t_tid);
+
+  tag_offset = sizeof (journal_header_t);
+  jb = txn->t_buffers;
+
+  while (jb)
+    {
+      if (tag_offset + sizeof (journal_block_tag_t) > block_size)
+	{
+	  err = E2BIG;
+	  goto out;
+	}
+
+      journal_block_tag_t *tag =
+	(journal_block_tag_t *) ((char *) descriptor_buf + tag_offset);
+      tag->t_blocknr = htobe32 (jb->jb_blocknr);
+
+      uint32_t flags = JBD2_FLAG_SAME_UUID;
+      if (jb->jb_next == NULL)
+	flags |= JBD2_FLAG_LAST_TAG;
+      tag->t_flags = htobe32 (flags);
+
+      jb = jb->jb_next;
+      tag_offset += sizeof (journal_block_tag_t);
+    }
+
+  JRNL_LOG_DEBUG ("[COMMIT] Writing Descriptor to %u", descriptor_loc);
+  err = journal_write_block (journal, descriptor_loc, descriptor_buf);
+  free (descriptor_buf);
+  descriptor_buf = NULL;
+  if (err)
+    goto out;
+
+
+  /* WRITE DATA BLOCKS NEXT */
+  jb = txn->t_buffers;
+  while (jb)
+    {
+      err =
+	journal_write_block (journal, jb->jb_log_spot, jb->jb_shadow_data);
+      if (err)
+	goto out;
+      jb = jb->jb_next;
+    }
+
+
+  /* ONLY THEN WRITE A COMMIT BLOCK */
+  commit_buf = calloc (1, block_size);
+  if (!commit_buf)
+    {
+      err = ENOMEM;
+      goto out;
+    }
+
+  hdr = (journal_header_t *) commit_buf;
+  hdr->h_magic = htobe32 (JBD2_MAGIC_NUMBER);
+  hdr->h_blocktype = htobe32 (JBD2_COMMIT_BLOCK);
+  hdr->h_sequence = htobe32 (txn->t_tid);
+
+  JRNL_LOG_DEBUG ("[COMMIT] Writing Commit Block at %u", commit_loc);
+  err = journal_write_block (journal, commit_loc, commit_buf);
+  JRNL_LOG_DEBUG ("[COMMIT] Done with Commit Block at %u", commit_loc);
+  free (commit_buf);
+  commit_buf = NULL;
+
+  /* BARRIER & UPDATE at the end */
+  if (!err)
+    {
+      journal_sync_everything ();
+      if (journal->j_tail == 0)
+	{
+	  JRNL_LOG_DEBUG ("[COMMIT] First Time: Anchoring Tail at Block %u",
+			  journal->j_first);
+
+	  journal_update_superblock (journal, txn->t_tid, journal->j_first);	/* Block 1 */
+	  sync_global (1);
+
+	  journal->j_tail = journal->j_first;
+	}
+    }
+
+out:
+  if (descriptor_buf)
+    free (descriptor_buf);
+  if (commit_buf)
+    free (commit_buf);
+
+  journal_buffer_t *curr = txn->t_buffers;
+  while (curr)
+    {
+      journal_buffer_t *next = curr->jb_next;
+      free (curr->jb_shadow_data);
+      free (curr);
+      curr = next;
+    }
+  free (txn);
+
+  return err;
+}
+
+/**
+ * Ensures there is a VALID running transaction to attach to.
+ * Returns 0 on success, or error code.
+ */
+error_t
+journal_start_transaction (journal_t *journal)
+{
+  struct journal_transaction *txn;
+
+  if (!journal)
+    return EINVAL;
+  pthread_mutex_lock (&journal->j_state_lock);
+  txn = journal->j_running_transaction;
+  if (txn && txn->t_state == T_RUNNING)
+    {
+      txn->t_updates++;
+    }
+  else
+    {
+      /* TODO: Is the *previous* transaction still committing?
+         If so, we might need to wait if we run out of space.
+         For now, assume we have infinite space.) */
+      txn = calloc (1, sizeof (struct journal_transaction));
+      if (!txn)
+	{
+	  pthread_mutex_unlock (&journal->j_state_lock);
+	  return ENOMEM;
+	}
+
+      txn->t_tid = journal->j_transaction_sequence;
+      txn->t_state = T_RUNNING;
+      txn->t_updates = 1;
+
+      journal->j_running_transaction = txn;
+      JRNL_LOG_DEBUG ("[TRX] Created NEW TID %u", txn->t_tid);
+    }
+
+  pthread_mutex_unlock (&journal->j_state_lock);
+  return 0;
+}
+
+void
+journal_stop_transaction (journal_t *journal)
+{
+  struct journal_transaction *txn;
+
+  if (!journal)
+    return;
+
+  pthread_mutex_lock (&journal->j_state_lock);
+
+  txn = journal->j_running_transaction;
+  if (!txn)
+    {
+      ext2_warning
+	("[TRX] stop_transaction called but no transaction running!");
+      pthread_mutex_unlock (&journal->j_state_lock);
+      return;
+    }
+
+  txn->t_updates--;
+  pthread_mutex_unlock (&journal->j_state_lock);
+}
+
+/**
+ * Adds a modified filesystem block to the current running transaction.
+ * Performs a "Shadow Copy" of the data immediately.
+ */
+error_t
+journal_dirty_block (journal_t *journal, block_t fs_blocknr, const void *data)
+{
+  struct journal_transaction *txn;
+  journal_buffer_t *jb;
+  journal_buffer_t *new_jb;
+
+  if (!journal || !data)
+    return EINVAL;
+
+  pthread_mutex_lock (&journal->j_state_lock);
+
+  txn = journal->j_running_transaction;
+
+  if (!txn || txn->t_state != T_RUNNING)
+    {
+      JRNL_LOG_DEBUG
+	("[ERROR] journal_dirty_block called outside of transaction!");
+      pthread_mutex_unlock (&journal->j_state_lock);
+      return EPERM;
+    }
+
+  /* TODO: Optimization: linear scan is fine for now. In prod use hash tables. */
+  jb = txn->t_buffers;
+  while (jb)
+    {
+      if (jb->jb_blocknr == fs_blocknr)
+	{
+	  memcpy (jb->jb_shadow_data, data, block_size);
+	  pthread_mutex_unlock (&journal->j_state_lock);
+	  return 0;
+	}
+      jb = jb->jb_next;
+    }
+
+  new_jb = malloc (sizeof (journal_buffer_t));
+  if (!new_jb)
+    {
+      pthread_mutex_unlock (&journal->j_state_lock);
+      return ENOMEM;
+    }
+
+  new_jb->jb_shadow_data = malloc (block_size);
+  if (!new_jb->jb_shadow_data)
+    {
+      free (new_jb);
+      pthread_mutex_unlock (&journal->j_state_lock);
+      return ENOMEM;
+    }
+
+  new_jb->jb_blocknr = fs_blocknr;
+  memcpy (new_jb->jb_shadow_data, data, block_size);
+
+  new_jb->jb_next = txn->t_buffers;
+  txn->t_buffers = new_jb;
+  txn->t_buffer_count++;
+  txn->t_nr_blocks++;
+
+  pthread_mutex_unlock (&journal->j_state_lock);
+  return 0;
+}
+
+/** 
+ * Check if a specific filesystem block is currently part of the
+ * Running Transaction.
+ * * Returns: 1 if the block is "pinned" (must not be written to disk yet),
+ * 0 if it is safe to write.
+ */
+int
+journal_block_is_active (journal_t *journal, block_t blocknr)
+{
+  struct journal_transaction *txn;
+  journal_buffer_t *jb;
+  int is_active = 0;
+
+  if (!journal)
+    return 0;
+
+  pthread_mutex_lock (&journal->j_state_lock);
+  txn = journal->j_running_transaction;
+
+  if (txn && txn->t_state == T_RUNNING)
+    {
+      /** TODO: hash map please! */
+      jb = txn->t_buffers;
+      while (jb)
+	{
+	  if (jb->jb_blocknr == blocknr)
+	    {
+	      is_active = 1;
+	      break;
+	    }
+	  jb = jb->jb_next;
+	}
+    }
+
+  pthread_mutex_unlock (&journal->j_state_lock);
+  return is_active;
+}
diff --git a/ext2fs/journal.h b/ext2fs/journal.h
new file mode 100644
index 00000000..b02e5791
--- /dev/null
+++ b/ext2fs/journal.h
@@ -0,0 +1,62 @@
+/* JBD2 binary compliant journal driver. 
+
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Written by Milos Nikic.
+
+   Converted for ext2fs by Miles Bader <[email protected]>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation; either version 2, or (at
+   your option) any later version.
+
+   This program 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
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */
+
+#ifndef  _JOURNAL_H
+#define _JOURNAL_H
+
+#include <stdio.h>
+
+#include "ext2fs.h"
+
+#ifndef JOURNAL_DEBUG
+#define JOURNAL_DEBUG 0		/* Set to enable (very chatty) debug messages. */
+#endif
+
+#if JOURNAL_DEBUG
+#define JRNL_LOG_DEBUG(fmt, ...)                                          \
+	do {                                                                       \
+		fprintf(stderr, "[JRNL][DEBUG] " fmt "\n", ##__VA_ARGS__);           \
+		fflush(stderr);                                                         \
+	} while (0)
+#else
+#define JRNL_LOG_DEBUG(fmt, ...) do { } while (0)
+#endif
+
+/* Forward declaration only. The struct contents are hidden. */
+typedef struct journal journal_t;
+
+journal_t *journal_create (struct node *journal_inode);
+error_t
+journal_write_block (journal_t * journal, uint32_t logical_idx, void *data);
+error_t
+journal_read_block (journal_t * journal, uint32_t logical_idx, void *out_buf);
+void journal_destroy (journal_t * journal);
+
+error_t journal_start_transaction (journal_t * journal);
+error_t journal_commit_transaction (journal_t * journal);
+void journal_stop_transaction (journal_t * journal);
+
+error_t
+journal_dirty_block (journal_t * journal, block_t fs_blocknr,
+		     const void *data);
+int journal_block_is_active (journal_t * journal, block_t blocknr);
+
+#endif //_JOURNAL_H
diff --git a/ext2fs/pager.c b/ext2fs/pager.c
index a7801bea..f76b6531 100644
--- a/ext2fs/pager.c
+++ b/ext2fs/pager.c
@@ -25,6 +25,7 @@
 #include <inttypes.h>
 #include <hurd/store.h>
 #include "ext2fs.h"
+#include "journal.h"
 
 /* XXX */
 #include "../libpager/priv.h"
@@ -648,6 +649,11 @@ disk_pager_write_page (vm_offset_t page, void *buf)
       while (length > 0 && !err)
 	{
 	  block_t block = boffs_block (offset);
+	  if (ext2_journal && journal_block_is_active(ext2_journal, block))
+	    {
+	       JRNL_LOG_DEBUG ("Pageout conflict on Block %u -> Forcing Commit", block);
+	       journal_commit_transaction(ext2_journal);
+	    }
 
 	  /* We don't clear the block modified bit here because this paging
 	     write request may not be the same one that actually set the bit,
@@ -1580,6 +1586,23 @@ diskfs_shutdown_pager (void)
      pager, just make sure it's synced. */
 }
 
+static error_t 
+journal_sync_one (void *v_p)
+{
+  struct pager *p = v_p;
+  pager_sync (p, 1);
+  return 0;
+}
+
+/* Sync all the pagers synchronously. */
+void
+journal_sync_everything (void)
+{
+  write_all_disknodes ();
+  ports_bucket_iterate (file_pager_bucket, journal_sync_one);
+  sync_global (1);
+}
+
 /* Sync all the pagers. */
 void
 diskfs_sync_everything (int wait)
@@ -1591,6 +1614,14 @@ diskfs_sync_everything (int wait)
       return 0;
     }
 
+  if (ext2_journal)
+    {
+      /* We only commit if we have a running transaction */
+      journal_commit_transaction (ext2_journal);
+      
+      /* Checkpoint: In a real system, we would flush the journal to the FS here.
+         For now, we rely on the standard paging below to do it lazily. */
+    }
   write_all_disknodes ();
   ports_bucket_iterate (file_pager_bucket, sync_one);
 
diff --git a/libdiskfs/diskfs.h b/libdiskfs/diskfs.h
index 5f832dd7..7f420725 100644
--- a/libdiskfs/diskfs.h
+++ b/libdiskfs/diskfs.h
@@ -507,6 +507,11 @@ error_t diskfs_validate_flags_change (struct node *np, int flags);
    changed to RDEV; otherwise return an error code. */
 error_t diskfs_validate_rdev_change (struct node *np, dev_t rdev);
 
+/* The user may define this function.  It is called immediately when
+   a node's metadata (stat info) is modified in memory, even if
+   diskfs_synchronous is false.  The default definition does nothing. */
+void diskfs_notify_change (struct node *np);
+
 /* The user must define this function.  Sync the info in NP->dn_stat
    and any associated format-specific information to disk.  If WAIT is true,
    then return only after the physicial media has been completely updated. */
diff --git a/libdiskfs/node-modified.c b/libdiskfs/node-modified.c
new file mode 100644
index 00000000..a29dc39f
--- /dev/null
+++ b/libdiskfs/node-modified.c
@@ -0,0 +1,28 @@
+/* Default version of diskfs_notify_change
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Written by Milos Nikic.
+
+   This file is part of the GNU Hurd.
+
+   The GNU Hurd is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation; either version 2, or (at
+   your option) any later version.
+
+   The GNU Hurd 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
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA. */
+
+#include "priv.h"
+#include "diskfs.h"
+
+void __attribute__ ((weak))
+diskfs_notify_change (struct node *np)
+{
+  // default function does nothing.
+}
diff --git a/libdiskfs/priv.h b/libdiskfs/priv.h
index ca3c23ca..e8186d49 100644
--- a/libdiskfs/priv.h
+++ b/libdiskfs/priv.h
@@ -140,7 +140,13 @@ extern fshelp_fetch_root_callback2_t _diskfs_translator_callback2;
   pthread_mutex_lock (&np->lock);					    \
   (OPERATION);								    \
   if (diskfs_synchronous)						    \
+   {  									    \
     diskfs_node_update (np, 1);						    \
+   }  									    \
+  else   								    \
+   {  									    \
+    diskfs_notify_change (np);  					    \
+   }  									    \
   pthread_mutex_unlock (&np->lock);					    \
   return err;								    \
 })
-- 
2.52.0

Reply via email to